工作单元和存储库相互依赖

时间:2013-01-07 22:26:19

标签: repository-pattern unit-of-work

我看过很多关于UnitOfWork和Repository循环的帖子(和辩论!)。我赞成的一种存储库模式是类型化的通用存储库模式,但我担心这会导致一些干净的代码和可测试性问题。采用以下存储库接口和泛型类:

public interface IDataEntityRepository<T> : IDisposable where T : IDataEntity
{
   // CRUD
   int Create(T createObject);
   // etc.
}


public class DataEntityRepository<T> : IDataEntityRepository<T> where T : class, IDataEntity
{
   private IDbContext Context { get; set; }

   public DataEntityRepository (IDbContext context)
   {
     Context = context;
   }

   private IDbSet<T> DbSet { get { return Context.Set<T>(); } }   

   public int Create(T CreateObject)
   {
      DbSet.Add(createObject);
   }

   // etc.

}

// where

public interface IDbContext
{
   IDbSet<T> Set<T>() where T : class;
   DbEntityEntry<T> Entry<T>(T readObject) where T : class;

   int SaveChanges();
   void Dispose();
}

所以基本上我在每个模式中使用Context属性来访问底层上下文。 我现在的问题是:当我创建我的工作单元时,它实际上将成为我需要存储库知道的上下文的包装器。所以,如果我的工作单位声明如下:

public UserUnitOfWork(
    IDataEntityRepository<User> userRepository,
    IDataEntityRepository<Role> roleRepository)
{
    _userRepository = userRepository;
    _roleRepository = roleRepository;
}

private readonly IDataEntityRepository<User> _userRepository;

public IDataEntityRepository<User> UserRepository
{
    get { return _userRepository; }
}

private readonly IDataEntityRepository<Role> _roleRepository;

public IDataEntityRepository<Role> RoleRepository
{
    get { return _roleRepository; }
}

我遇到的问题是,我传递的两个存储库都需要使用它们传递给它的工作单元进行实例化。显然,我可以在构造函数中实例化存储库并传入“this”,但是将我的工作单元紧密地耦合到存储库的特定具体实例,并使单元测试更加困难。 我很想知道是否有其他人沿着这条路走下去并撞到了同一堵墙。这两种模式对我来说都是新的,所以我很可能会做一些根本错误的事情。任何想法将不胜感激!

更新(对@MikeSW的回应)

嗨,迈克,非常感谢你的投入。我正在使用EF Code First,但我想抽象某些元素,因此我可以根据需要切换到不同的数据源或ORM,因为我(尝试!)将自己推向TDD路线并使用Mocking和IOC。我想我已经意识到某些元素不能在纯粹的意义上进行单元测试,但可以进行集成测试!我想提出你关于存储库使用业务对象或视图模型等的观点。也许我误解了但是如果我有我认为的核心业务对象(PO​​CO),那么我想使用ORM,如EF代码首先要包装这些实体以便创建数据库,然后与数据库进行交互(并且,我可以在ViewModel中重新使用这些实体),我希望Repository可以直接在上下文中处理这些实体。一些CRUD操作。实体根本不了解持久层,任何ViewModel都不知道。我的工作单元只是实例化并保存所需的存储库,允许执行事务提交(跨多个存储库但相同的上下文/会话)。我在我的解决方案中所做的是从UnitOfWork构造函数中删除IDataEntityRepository ...等的注入,因为这是一个必须知道它应该创建的一种且只有一种类型的IDataEntityRepository的具体类(在本例中是DataEntityRepository) ,真的应该更好的名称为EFDataEntityRepository)。我无法对此进行单元测试,因为整个单元逻辑将建立具有上下文(本身)到某个数据库的存储库。它只需要一个集成测试。希望有意义吗?!

3 个答案:

答案 0 :(得分:3)

为避免依赖工作单元中的每个存储库,您可以使用基于此合同的提供程序:

public interface IRepositoryProvider
{
    DbContext DbContext { get; set; }
    IRepository<T> GetRepositoryForEntityType<T>() where T : class;
    T GetRepository<T>(Func<DbContext, object> factory = null) where T : class;
    void SetRepository<T>(T repository);
}

然后你可以把它注入你的UoW,看起来像这样:

public class UserUnitOfWork: IUserUnitOfWork
{
    public UserUnitOfWork(IRepositoryProvider repositoryProvider)
    {
        RepositoryProvider = repositoryProvider;
    }

    protected IDataEntityRepository<T> GetRepo<T>() where T : class
    {
        return RepositoryProvider.GetRepositoryForEntityType<T>();
    }

    public IDataEntityRepository<User> Users { get { return GetRepo<User>(); } }        
    public IDataEntityRepository<Role> Roles { get { return GetRepo<Role>(); } }        
...

答案 1 :(得分:3)

对我的回应迟到的道歉 - 我一直在尝试各种方法。我已经在上面标出了答案,因为我同意所做的评论。

这是其中一个问题,其中有多个答案,而且非常依赖于整体方法。虽然我同意EF有效地提供了现成的工作单元模式,但我决定创建自己的工作单元和存储库层是为了能够控制对数据库实体的访问。

我努力工作的地方在于需要能够将存储库注入工作单元。我意识到的是,在EF的情况下,我的工作单元实际上是一个包含Commit(SaveChanges)方法的多个存储库的瘦包装器。它不负责执行FindCustomer等特定操作。

所以我决定将一个工作单元紧密耦合到其特定类型的DataRepository模式。为了确保我有一个可测试的模式,我引入了一个服务层,它提供了用于执行特定操作的外观,例如CreateCustomer,FindCustomers等。这些服务接受了IUnitOfWork构造函数参数,该参数提供了对存储库(作为接口)的访问以及提交方法。

然后,我可以为测试目的创建工作单元和/或存储库的假货。这让我想到了什么可以用假货进行单元测试,以及需要对具体实例进行集成测试。

这也使我有机会控制对数据库执行的操作及其执行方式。

我确信有很多方法可以为这只特殊的猫提供皮肤,但提供一个可测试的干净界面的目标几乎已经达到了这种方法。

感谢g1ga和Mike的投入。

答案 2 :(得分:2)

使用实体框架(EF)(我假设您正在使用)时,您已经拥有了通用存储库IDbSet。为了调用EF方法,在顶部另一层上添加它是没用的。

此外,存储库使用应用程序对象(通常是业务对象,但它们可以是视图模型或对象状态)。如果您只是使用数据库实体,那么您将失去存储库模式的目的(将业务流程与数据库隔离开来)。原始模式只处理业务对象,但它也是业务层之外的有用模式。

关键是EF实体是Persistence对象,并且(或者应该)与业务对象无关。您希望使用存储库模式将业务对象“转换”为持久性对象,反之亦然。

有时可能会发生应用程序对象(如viewmodel)与持久性实体相同(在这种情况下,您可以直接使用EF对象),但这是巧合。

关于工作单位(UoW),让我们说这很棘手。就个人而言,我更喜欢使用DDD(域驱动设计)方法,并认为发送到repoistory的任何业务对象(BO)都是UoW,因此它将被包装在事务中。

如果我需要更新多个BO,我将使用消息驱动架构将命令发送到相关BO。当然,这更复杂,需要对最终一致性的概念感到放心,但我并不依赖于特定的RDBMS。

如果您知道您将使用特定的RDBMS且永远不会更改,您可以启动事务并将关联的连接传递到每个存储库,最后提交(即UoW)。如果您处于Web设置中,则更容易,请求开始时启动事务,请求结束时提交(您可以使用ActionFilter for ASp.Net Mvc)。

但是,此解决方案与一个RDBMS绑定,因此它不适用于NoSql或任何不支持事务的存储。对于这些情况,消息驱动方式是最好的。