32

背景/问题

我参与过许多需要持久化数据并且通常最终使用存储库模式的 .NET 项目。有谁知道在不牺牲代码库可扩展性的情况下删除尽可能多的样板代码的好策略?

继承策略

因为很多存储库代码都是样板并且需要重复,所以我通常创建一个基类来涵盖异常处理、日志记录和事务支持等基础知识以及一些基本的 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){}
}

所以当我有一个简单的域时,这很有效,我可以为每个实体快速创建一个 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){}
}

此解决方案不能很好地扩展。假设我有几个具有通用方法的实体: 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)'
    }
} 

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)
    }
}

面向方面的编程

我研究了为此使用 AOP 框架,特别是PostSharp和他们的Composition Aspect,看起来它应该可以解决问题,但是为了使用存储库,我必须调用 Post.Cast<>(),它添加了一个非常奇怪的代码气味。任何人都知道是否有更好的方法来使用 AOP 来帮助摆脱合成器样板代码?

自定义代码生成器

如果一切都失败了,我想我可以创建一个自定义代码生成器 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)'
    }
} 

扩展方法

当我最初写这个问题时忘记添加这个(对不起)。我还尝试了扩展方法:

public static class GetByIDExtenions
{
     public T GetByID<T>(this IGetByID<T> repository, int id){ }        
}

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

public interface IMyEntityRepository : IGetByID<MyEntity>{}
public class MyEntityRepository : IMyEntityRepository{}

更新T4 模板会是一个可能的解决方案吗?

4

5 回答 5

11

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

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

我为 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);
        }
    }
}

通用存储库接口使我免于创建大量类似的类。您只有一个通用存储库实现,但也可以自由查询。

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

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

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));

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

ioc.Bind(typeof (IRepository<>)).To(typeof (InMemoryRepository<>));

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

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));
    }
}

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

var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly();

如果您不想为这些扩展方法使用不同的命名空间(比如我) - 好的,请将它们放在存储库实现所在的同一命名空间中(比如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");
    }
}

并让 IoC 为产品返回这个特定的存储库实现:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
ioc.Bind<IRepository<Product>>().To<ProductRepository>();

这就是我与我的存储库一起离开的方式;)

于 2013-03-16T22:42:12.560 回答
4

签出 T4 文件以生成代码。T4 内置于 Visual Studio 中。在此处查看教程

我通过检查 LINQ DBML 和它们的存储库创建了用于生成 POCO 实体的代码的 T4 文件,我认为它会在这里很好地为您服务。如果您使用 T4 文件生成部分类,您可以只为特殊情况编写代码。

于 2013-04-11T19:32:50.833 回答
2

对我来说,您似乎将基类划分为一个继承类,然后将它们的功能都包含在一个继承类中。在这种情况下,组合是选择。如果 C# 支持,多类继承也会很好。但是,因为我觉得继承更好并且可重用性仍然很好,所以我的第一选择会选择它。

选项1

我宁愿多一个基类而不是两者的组合。可重用性也可以通过静态方法而不是继承来解决:

可重复使用的部分在外面是不可见的。无需记住命名空间。

static class Commons
{
    internal static void Update(/*receive all necessary params*/) 
    { 
        /*execute and return result*/
    }

    internal static void Archive(/*receive all necessary params*/) 
    { 
        /*execute and return result*/
    }
}

class Basic 
{
    public void SelectAll() { Console.WriteLine("SelectAll"); }
}

class ChildWithUpdate : Basic
{
    public void Update() { Commons.Update(); }
}

class ChildWithArchive : Basic
{
    public void Archive() { Commons.Archive(); }
}

class ChildWithUpdateAndArchive: Basic
{
    public void Update() { Commons.Update(); }
    public void Archive() { Commons.Archive(); }
}

当然有一些小的重复代码,但那只是调用公共库中的现成函数。

选项 2

我的组合实现(或模仿多重继承):

public class Composite<TFirst, TSecond>
{
    private TFirst _first;
    private TSecond _second;

    public Composite(TFirst first, TSecond second)
    {
        _first = first;
        _second = second;
    }

    public static implicit operator TFirst(Composite<TFirst, TSecond> @this)
    {
        return @this._first;
    }

    public static implicit operator TSecond(Composite<TFirst, TSecond> @this)
    {
        return @this._second;
    }

    public bool Implements<T>() 
    {
        var tType = typeof(T);
        return tType == typeof(TFirst) || tType == typeof(TSecond);
    }
}

继承与组合(下):

class Basic 
{
    public void SelectAll() { Console.WriteLine("SelectAll"); }
}

class ChildWithUpdate : Basic
{
    public void Update() { Console.WriteLine("Update"); }
}

class ChildWithArchive : Basic
{
    public void Archive() { Console.WriteLine("Archive"); }
}

作品。不确定这是否足以说明不存在样板代码。

class ChildWithUpdateAndArchive : Composite<ChildWithUpdate, ChildWithArchive>
{
    public ChildWithUpdateAndArchive(ChildWithUpdate cwu, ChildWithArchive cwa)
        : base(cwu, cwa)
    {
    }
}

使用所有这些的代码看起来还不错,但在分配中仍然不寻常(不可见)类型转换。这是减少样板代码的回报:

ChildWithUpdate b;
ChildWithArchive c;
ChildWithUpdateAndArchive d;

d = new ChildWithUpdateAndArchive(new ChildWithUpdate(), new ChildWithArchive());
//now call separated methods.
b = d;
b.Update();
c = d;
c.Archive();
于 2013-04-17T19:59:00.183 回答
1

这是我的版本:

interface IGetById
{
    T GetById<T>(object id);
}

interface IGetAll
{
    IEnumerable<T> GetAll<T>();
}

interface ISave
{
    void Save<T>(T item) where T : IHasId; //you can go with Save<T>(object id, T item) if you want pure pure POCOs
}

interface IDelete
{
    void Delete<T>(object id);
}

interface IHasId
{
    object Id { get; set; }
}

我不喜欢通用存储库接口,因为它设置了额外的限制并使其以后更难使用。我改用通用方法。

我没有为存储库使用标头接口,而是为每个存储库方法使用角色接口。这让我可以向存储库方法添加额外的功能,例如日志记录、发布对 PubSub 的更改等。

我不使用存储库进行自定义查询,因为我还没有找到适合任何数据库的任何好的和简单的查询抽象。我的存储库版本只能通过 id 获取项目或获取相同类型的所有项目。其他查询在内存中完成(如果性能足够好)或者我有一些其他机制。

为方便起见,可以引入 IRepository 接口,这样您就不必为诸如 crud 控制器之类的东西不断编写 4 个接口

interface IRepository : IGetById, IGetAll, ISave, IDelete { }

class Repository : IRepository
{
    private readonly IGetById getter;

    private readonly IGetAll allGetter;

    private readonly ISave saver;

    private readonly IDelete deleter;

    public Repository(IGetById getter, IGetAll allGetter, ISave saver, IDelete deleter)
    {
        this.getter = getter;
        this.allGetter = allGetter;
        this.saver = saver;
        this.deleter = deleter;
    }

    public T GetById<T>(object id)
    {
        return getter.GetById<T>(id);
    }

    public IEnumerable<T> GetAll<T>()
    {
        return allGetter.GetAll<T>();
    }

    public void Save<T>(T item) where T : IHasId
    {
        saver.Save(item);
    }

    public void Delete<T>(object id)
    {
        deleter.Delete<T>(id);
    }
}

我提到使用角色接口我可以添加额外的行为,这里有几个使用装饰器的例子

class LogSaving : ISave
{
    private readonly ILog logger;

    private readonly ISave next;

    public LogSaving(ILog logger, ISave next)
    {
        this.logger = logger;
        this.next = next;
    }

    public void Save<T>(T item) where T : IHasId
    {
        this.logger.Info(string.Format("Start saving {0} : {1}", item.ToJson()));
        next.Save(item);
        this.logger.Info(string.Format("Finished saving {0}", item.Id));
    }
}

class PublishChanges : ISave, IDelete
{
    private readonly IPublish publisher;

    private readonly ISave nextSave;

    private readonly IDelete nextDelete;

    private readonly IGetById getter;

    public PublishChanges(IPublish publisher, ISave nextSave, IDelete nextDelete, IGetById getter)
    {
        this.publisher = publisher;
        this.nextSave = nextSave;
        this.nextDelete = nextDelete;
        this.getter = getter;
    }

    public void Save<T>(T item) where T : IHasId
    {
        nextSave.Save(item);
        publisher.PublishSave(item);
    }

    public void Delete<T>(object id)
    {
        var item = getter.GetById<T>(id);
        nextDelete.Delete<T>(id);
        publisher.PublishDelete(item);
    }
}

在内存存储中实现测试并不难

class InMemoryStore : IRepository
{
    private readonly IDictionary<Type, Dictionary<object, object>> db;

    public InMemoryStore(IDictionary<Type, Dictionary<object, object>> db)
    {
        this.db = db;
    }

    ...
}

最后把所有东西放在一起

var db = new Dictionary<Type, Dictionary<object, object>>();
var store = new InMemoryStore(db);
var storePublish = new PublishChanges(new Publisher(...), store, store, store);
var logSavePublish = new LogSaving(new Logger(), storePublish);
var repo = new Repository(store, store, logSavePublish, storePublish);
于 2014-11-25T11:21:33.263 回答
1

您可以使用访问者模式,在此处阅读实现,因此您只能实现必要的功能。

这是想法:

public class Customer : IAcceptVisitor
{
    private readonly string _id;
    private readonly List<string> _items = new List<string>();
    public Customer(string id)
    {
        _id = id;
    }

    public void AddItems(string item)
    {
        if (item == null) throw new ArgumentNullException(nameof(item));
        if(_items.Contains(item)) throw new InvalidOperationException();
        _items.Add(item);
    }

    public void Accept(ICustomerVisitor visitor)
    {
        if (visitor == null) throw new ArgumentNullException(nameof(visitor));
        visitor.VisitCustomer(_items);
    }
}
public interface IAcceptVisitor
{
    void Accept(ICustomerVisitor visitor);
}

public interface ICustomerVisitor
{
    void VisitCustomer(List<string> items);
}

public class PersistanceCustomerItemsVisitor : ICustomerVisitor
{
    public int Count { get; set; }
    public List<string> Items { get; set; }
    public void VisitCustomer(List<string> items)
    {
        if (items == null) throw new ArgumentNullException(nameof(items));
        Count = items.Count;
        Items = items;
    }
}

因此,您可以在域逻辑和基础架构之间应用关注点分离,应用访问者模式以实现持久性。问候!

于 2016-12-05T18:32:19.230 回答