C# - 对象组成 - 删除Boilerplate代码

Phi*_*tle 32 .net c# inheritance design-patterns composition

背景/问题

我已经处理过许多需要持久保存数据的.NET项目,并且通常最终使用了Repository模式.有没有人知道在不牺牲代码库可扩展性的情况下删除尽可能多的样板代码的好策略?

继承战略

因为很多存储库代码都是锅炉板并且需要重复,所以我通常会创建一个基类来覆盖基本知识,例如异常处理,日志记录和事务支持以及一些基本的CRUD方法:

public abstract class BaseRepository<T> where T : IEntity
{
    protected void ExecuteQuery(Action query)
    {
        //Do Transaction Support / Error Handling / Logging
        query();
    }       

    //CRUD Methods:
    public virtual T GetByID(int id){}
    public virtual IEnumerable<T> GetAll(int id){}
    public virtual void Add (T Entity){}
    public virtual void Update(T Entity){}
    public virtual void Delete(T Entity){}
}
Run Code Online (Sandbox Code Playgroud)

因此,当我有一个简单的域时,这很有效,我可以为每个实体快速创建一个DRY存储库类.但是,当域变得更复杂时,这会开始崩溃.假设引入了一个不允许更新的新实体.我可以拆分基类并将Update方法移动到另一个类中:

public abstract class BaseRepositorySimple<T> where T : IEntity
{
    protected void ExecuteQuery(Action query);

    public virtual T GetByID(int id){}
    public virtual IEnumerable<T> GetAll(int id){}
    public virtual void Add (T entity){}
    public void Delete(T entity){}
}

public abstract class BaseRepositoryWithUpdate<T> :
    BaseRepositorySimple<T> where T : IEntity
{
     public virtual void Update(T entity){}
}
Run Code Online (Sandbox Code Playgroud)

该解决方案不能很好地扩展.假设我有几个实体有一个共同的方法:public virtual void Archive(T entity){}

但是一些可以存档的实体也可以更新,而其他实体则不能.所以我的继承解决方案崩溃了,我必须创建两个新的基类来处理这个场景.

组合策略

我已经探索了Compositon模式,但这似乎留下了很多锅炉板代码:

public class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity>
{
    private Archiver<MyEntity> _archiveWrapper;      
    private GetByIDRetriever<MyEntity> _getByIDWrapper;

    public MyEntityRepository()
    {
         //initialize wrappers (or pull them in
         //using Constructor Injection and DI)
    }

    public MyEntity GetByID(int id)
    {
         return _getByIDWrapper(id).GetByID(id);
    }

    public void Archive(MyEntity entity)
    {
         _archiveWrapper.Archive(entity)'
    }
} 
Run Code Online (Sandbox Code Playgroud)

MyEntityRepository现在加载了样板代码.是否有可用于自动生成此工具/模式的工具/模式?

如果我可以将MyEntityRepository变成这样的东西,我认为到目前为止理想:

[Implement(Interface=typeof(IGetByID<MyEntity>), 
    Using = GetByIDRetriever<MyEntity>)]      
[Implement(Interface=typeof(IArchive<MyEntity>), 
    Using = Archiver<MyEntity>)
public class MyEntityRepository
{
    public MyEntityRepository()
    {
         //initialize wrappers (or pull them in
         //using Constructor Injection and DI)
    }
}
Run Code Online (Sandbox Code Playgroud)

面向方面编程

我考虑使用AOP框架,特别是PostSharp及其Composition Aspect,看起来应该可以解决这个问题,但是为了使用Repository,我必须调用Post.Cast <>(),这会增加一个代码很奇怪.任何人都知道是否有更好的方法来使用AOP来帮助摆脱compositor样板代码?

定制代码生成器

如果所有其他方法都失败了,我想我可以创建一个自定义代码生成器Visual Studio插件,可以将锅炉板代码生成为部分代码文件.是否已经有一个工具可以做到这一点?

[Implement(Interface=typeof(IGetByID<MyEntity>), 
    Using = GetByIDRetriever<MyEntity>)]      
[Implement(Interface=typeof(IArchive<MyEntity>), 
    Using = Archiver<MyEntity>)
public partial class MyEntityRepository
{
    public MyEntityRepository()
    {
         //initialize wrappers (or pull them in
         //using Constructor Injection and DI)
    }
} 

//Generated Class file
public partial class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity>
{
    private Archiver<MyEntity> _archiveWrapper;      
    private GetByIDRetriever<MyEntity> _getByIDWrapper;

    public MyEntity GetByID(int id)
    {
         return _getByIDWrapper(id).GetByID(id);
    }

    public void Archive(MyEntity entity)
    {
         _archiveWrapper.Archive(entity)'
    }
} 
Run Code Online (Sandbox Code Playgroud)

扩展方法

当我最初写这个问题时,忘记添加这个(对不起).我也尝试过扩展方法:

public static class GetByIDExtenions
{
     public T GetByID<T>(this IGetByID<T> repository, int id){ }        
}
Run Code Online (Sandbox Code Playgroud)

但是,这有两个问题,a)我必须记住扩展方法类的名称空间并将其添加到任何地方b)扩展方法不能满足接口依赖性:

public interface IMyEntityRepository : IGetByID<MyEntity>{}
public class MyEntityRepository : IMyEntityRepository{}
Run Code Online (Sandbox Code Playgroud)

更新:T4模板是否可能成为解决方案?

why*_*eee 11

我有一个通用的存储库接口,它只为特定的数据存储实现一次.这里是:

public interface IRepository<T> where T : class
{
    IQueryable<T> GetAll();
    T Get(object id);
    void Save(T item);
    void Delete(T item);
}
Run Code Online (Sandbox Code Playgroud)

我为EntityFramework,NHibernate,RavenDB存储实现了它.我还有一个用于单元测试的内存实现.

例如,以下是基于内存集合的存储库的一部分:

public class InMemoryRepository<T> : IRepository<T> where T : class
{
    protected readonly List<T> _list = new List<T>();

    public virtual IQueryable<T> GetAll()
    {
        return _list.AsReadOnly().AsQueryable();
    }

    public virtual T Get(object id)
    {
        return _list.FirstOrDefault(x => GetId(x).Equals(id));
    }

    public virtual void Save(T item)
    {
        if (_list.Any(x => EqualsById(x, item)))
        {
            Delete(item);
        }

        _list.Add(item);
    }

    public virtual void Delete(T item)
    {
        var itemInRepo = _list.FirstOrDefault(x => EqualsById(x, item));

        if (itemInRepo != null)
        {
            _list.Remove(itemInRepo);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通用存储库接口使我无法创建类似类的批次.您只有一个通用的存储库实现,但也可以自由查询.

IQueryable<T>GetAll()方法的结果允许我对数据进行任何我想要的查询,并将它们与特定于存储的代码分开.所有流行的.NET ORM都有自己的LINQ提供程序,它们都应该有这种神奇的GetAll()方法 - 所以这里没有问题.

我使用IoC容器在组合根中指定存储库实现:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
Run Code Online (Sandbox Code Playgroud)

在测试中,我正在使用它的内存替换:

ioc.Bind(typeof (IRepository<>)).To(typeof (InMemoryRepository<>));
Run Code Online (Sandbox Code Playgroud)

如果我想为存储库添加更多特定于业务的查询,我将添加一个扩展方法(类似于答案中的扩展方法):

public static class ShopQueries
{
    public IQueryable<Product> SelectVegetables(this IQueryable<Product> query)
    {
        return query.Where(x => x.Type == "Vegetable");
    }

    public IQueryable<Product> FreshOnly(this IQueryable<Product> query)
    {
        return query.Where(x => x.PackTime >= DateTime.Now.AddDays(-1));
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,您可以在业务逻辑层查询中使用和混合这些方法,从而节省可测试性和存储库实现的简便性,例如:

var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly();
Run Code Online (Sandbox Code Playgroud)

如果你不想为那些扩展方法(比如我)使用不同的命名空间 - 好吧,把它们放在存储库实现所在的同一个命名空间里(比如MyProject.Data),或者更好的是,放到一些现有的业务特定命名空间(比如MyProject.ProductsMyProject.Data.Products).现在无需记住其他命名空间.

如果某些实体具有某些特定的存储库逻辑,请创建一个覆盖所需方法的派生存储库类.例如,如果只能通过ProductNumber代替Id而不支持删除来找到产品,则可以创建此类:

public class ProductRepository : RavenDbRepository<Product>
{
    public override Product Get(object id)
    {
        return GetAll().FirstOrDefault(x => x.ProductNumber == id);
    }

    public override Delete(Product item)
    {
        throw new NotSupportedException("Products can't be deleted from db");
    }
}
Run Code Online (Sandbox Code Playgroud)

并使IoC返回产品的特定存储库实现:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
ioc.Bind<IRepository<Product>>().To<ProductRepository>();
Run Code Online (Sandbox Code Playgroud)

这就是我与我的存储库分开的方式;)

  • 我反对在存储库中使用Iqueryable,因为它与存储库无关.IQueryable指定如何构建查询.你没有告诉存储库如何完成它的工作,你告诉它该怎么做.您最多可以传递查询的选择条件.Generic存储库最适用于DDD,其中每个聚合根都是序列化的.对于所有其他情况,它有点失败,它只是对ORM的一个useles抽象(它本身就是一个抽象). (5认同)
  • @whylee - (跑出房间)1)编写抛出NotSupported或NotImplemented异常的虚拟方法违反了OOP.如果基类定义了一个方法,那么任何子类(从本质上继承自基类)都会签署一个承诺,它将实现该方法.现在,责任在于使用子类的人记住该方法实际上没有实现.最大的问题是,你是否有松散的编译时支持和(可能)引入运行时错误. (4认同)