在大型项目中使用通用存储库/工作单元模式

本文关键字:工作 单元 模式 大型项目 存储 | 更新日期: 2023-09-27 18:14:14

我正在开发一个相当大的应用程序。该领域有大约20-30种类型,实现为ORM类(例如EF Code First或XPO,这与问题无关)。我已经阅读了一些关于存储库模式的通用实现的文章和建议,并将其与工作单元模式相结合,产生如下代码:

public interface IRepository<T> {
  IQueryable<T> AsQueryable();
  IEnumerable<T> GetAll(Expression<Func<T, bool>> filter);
  T GetByID(int id);
  T Create();
  void Save(T);
  void Delete(T);
}
public interface IMyUnitOfWork : IDisposable {
  void CommitChanges();
  void DropChanges();
  IRepository<Product> Products { get; }
  IRepository<Customer> Customers { get; }
}

这个模式适合真正的大型应用程序吗?每个示例在工作单元中大约有2个,最多3个存储库。就我对该模式的理解而言,在一天结束时,存储库引用的数量(在实现中延迟初始化)等于(或接近等于)域实体类的数量,这样就可以将工作单元用于复杂的业务逻辑实现。例如,让我们像这样扩展上面的代码:

public interface IMyUnitOfWork : IDisposable {
  ...
  IRepository<Customer> Customers { get; }
  IRepository<Product> Products { get; }
  IRepository<Orders> Orders { get; }
  IRepository<ProductCategory> ProductCategories { get; }
  IRepository<Tag> Tags { get; }
  IRepository<CustomerStatistics> CustomerStatistics  { get; }
  IRepository<User> Users { get; }
  IRepository<UserGroup> UserGroups { get; }
  IRepository<Event> Events { get; }
  ...   
}

在考虑代码气味之前,有多少存储库可以被引用?还是这种模式完全正常?我可能会把这个接口分成2到3个不同的接口来实现IUnitOfWork,但这样使用起来就不那么舒服了。

更新

我检查了@qujck推荐的一个基本不错的解决方案。我对动态存储库注册和"基于字典"方法的问题是,我想要享受对存储库的直接引用,因为一些存储库将具有特殊行为。因此,当我编写业务代码时,我希望能够像这样使用它例如:

using (var uow = new MyUnitOfWork()) {
  var allowedUsers = uow.Users.GetUsersInRolw("myRole");
  // ... or
  var clothes = uow.Products.GetInCategories("scarf", "hat", "trousers");
}

所以在这里我受益于我有一个强类型的IRepository和IRepository引用,因此我可以使用特殊的方法(作为扩展方法实现或通过继承基接口)。如果我使用动态存储库注册和检索方法,我认为我将失去这个,或者至少必须一直做一些丑陋的强制类型转换。

对于DI而言,我会尝试将存储库工厂注入到我的实际工作单元中,这样它就可以惰性地实例化存储库。

在大型项目中使用通用存储库/工作单元模式

建立在我上面的评论和这里的答案之上。

使用稍微修改过的工作抽象单元

public interface IMyUnitOfWork
{
    void CommitChanges();
    void DropChanges();
    IRepository<T> Repository<T>();
}

可以使用扩展方法

公开命名的存储库和特定的存储库方法
public static class MyRepositories
{
    public static IRepository<User> Users(this IMyUnitOfWork uow)
    {
        return uow.Repository<User>();
    }
    public static IRepository<Product> Products(this IMyUnitOfWork uow)
    {
        return uow.Repository<Product>();
    }
    public static IEnumerable<User> GetUsersInRole(
        this IRepository<User> users, string role)
    {
        return users.AsQueryable().Where(x => true).ToList();
    }
    public static IEnumerable<Product> GetInCategories(
        this IRepository<Product> products, params string[] categories)
    {
        return products.AsQueryable().Where(x => true).ToList();
    }
}

提供所需的数据访问

using(var uow = new MyUnitOfWork())
{
    var allowedUsers = uow.Users().GetUsersInRole("myRole");
    var result = uow.Products().GetInCategories("scarf", "hat", "trousers");
}

我倾向于采用的方法是将类型约束从存储库类移动到其中的方法。这就意味着:

public interface IMyUnitOfWork : IDisposable
{
    IRepository<Customer> Customers { get; }
    IRepository<Product> Products { get; }
    IRepository<Orders> Orders { get; }
    ...
}

我有这样的东西:

public interface IMyUnitOfWork : IDisposable
{
    Get<T>(/* some kind of filter expression in T */);
    Add<T>(T);
    Update<T>(T);
    Delete<T>(/* some kind of filter expression in T */);
    ...
}

这样做的主要好处是在您的工作单元上只需要一个数据访问对象。缺点是您不再有像Products.GetInCategories()这样的特定于类型的方法。这可能会有问题,所以我的解决方案通常是两种方法之一。

关注点分离

首先,您可以重新考虑"数据访问"answers";以及"商业逻辑";因此,您有一个逻辑层类ProductService,它具有可以执行以下操作的方法GetInCategory():

using (var uow = new MyUnitOfWork())
{
    var productsInCategory = GetAll<Product>(p => ["scarf", "hat", "trousers"].Contains(u.Category));
}

你的数据访问和业务逻辑代码仍然是分开的。

查询的封装

或者,您可以实现一个规范模式,这样您就可以有一个名称空间MyProject.Specifications,其中有一个基类Specification<T>,它在内部某处有一个过滤器表达式,这样您就可以将它传递给工作对象单元,并且UoW可以使用过滤器表达式。这让你有派生的规范,你可以传递,现在你可以这样写:

using (var uow = new MyUnitOfWork())
{
    var searchCategories = new Specifications.Products.GetInCategories("scarf", "hat", "trousers");
    var productsInCategories = GetAll<Product>(searchCategories);
}

如果你想要一个中央位置来保存常用的逻辑,比如"get user by role";或者"获取类别中的产品",而不是将其保存在存储库中(严格来说,这应该是纯粹的数据访问),然后您可以在对象本身上使用这些扩展方法。例如,Product可以有一个方法或扩展方法InCategory(string),它返回Specification<Product>,甚至只是一个过滤器,如Expression<Func<Product, bool>>,允许您这样编写查询:

using (var uow = new MyUnitOfWork())
{
    var productsInCategory = GetAll(Product.InCategories("scarf", "hat", "trousers");
}

(注意,这仍然是一个泛型方法,但类型推断将为您处理它。)

这保留了被查询对象(或该对象的扩展类)上的所有查询逻辑,这仍然使您的数据和逻辑代码按照类和文件很好地分开,同时允许您像以前共享IRepository<T>扩展一样共享它。

例子为了给出一个更具体的例子,我在EF中使用了这种模式。我没有为规格操心;我在逻辑层中只有服务类,它们对每个逻辑操作使用单个工作单元("添加新用户"、"获取产品类别"、"保存对产品的更改")。等等)。它的核心看起来是这样的(为了简洁而省略了实现,因为它们非常琐碎):
public class EFUnitOfWork: IUnitOfWork
{
    private DbContext _db;
    public EntityFrameworkSourceAdapter(DbContext context) {...}
    public void Add<T>(T item) where T : class, new() {...}
    public void AddAll<T>(IEnumerable<T> items) where T : class, new() {...}
    public T Get<T>(Expression<Func<T, bool>> filter) where T : class, new() {...}
    public IQueryable<T> GetAll<T>(Expression<Func<T, bool>> filter = null) where T : class, new() {...}
    public void Update<T>(T item) where T : class, new() {...}
    public void Remove<T>(Expression<Func<T, bool>> filter) where T : class, new() {...}
    public void Commit() {...}
    public void Dispose() {...}
}

这些方法大多使用_db.Set<T>()来获取相关的DbSet,然后使用LINQ使用提供的Expression<Func<T, bool>>进行查询。