存储库模式 - 使其可测试、DI 和 IoC 友好且 IDisposable

Repository pattern - make it testable, DI and IoC friendly and IDisposable

我有:

public interface IRepository
{
   IDisposable CreateConnection();
   User GetUser();
   //other methods, doesnt matter
}

public class Repository
{
   private SqlConnection _connection;

   IDisposable CreateConnection()
   {
      _connection = new SqlConnection();
      _connection.Open();
      return _connection;
   }

   User GetUser()
   {
      //using _connection gets User from Database
      //assumes _connection is not null and open
   }
   //other methods, doesnt matter 
}

这使得使用 IRepository 的 classes 易于测试并且 IoC 容器友好。但是,使用此 class 的人必须在调用任何从数据库获取内容的方法之前调用 CreateConnection,否则将抛出异常。这本身有点好——我们不希望在应用程序中有持久的连接。所以使用这个 class 我是这样做的。

using(_repository.CreateConnection())
{
    var user = _repository.GetUser();
    //do something with user
}

不幸的是,这不是很好的解决方案,因为使用此 class 的人(甚至包括我!)经常忘记在调用方法从数据库中获取内容之前调用 _repository.CreateConnection()

为了解决这个问题,我查看了 Mark Seemann 的博客 post SUT Double,他在其中以正确的方式实现了存储库模式。不幸的是,他让 Repository 实现了 IDisposable,这意味着我不能简单地通过 IoC 和 DI 将它注入 classes 并在之后使用它,因为在使用一次之后它就会被处理掉。他每个请求使用一次,并使用 ASP.NET WebApi 功能在请求处理完成后处理它。这是我无法做到的事情,因为我的 classes 实例一直在使用 Repository。

最好的解决方案是什么?我应该使用某种可以给我 IDisposable IRepository 的工厂吗?那么它会很容易测试吗?

创建一个用于创建 IDisposable 个存储库的存储库工厂。

public interface IRepository : IDisposable {
   User GetUser();
   //other methods, doesn't matter
}

public interface IRepositoryFactory {
    IRepository Create();
}

您在一次使用中创建它们,并在完成后将其丢弃。

using(var repository = factory.Create()) { 
    var user = repository.GetUser(); 
    //do something with user
}

您可以根据需要注入工厂并创建存储库。

我会创建一个连接工厂...

public class ConnectionFactory
{
    public IDbConnection Create()
    { 
        // your logic here
    }
}

现在让它成为您的存储库的依赖项,并在您的存储库中使用它...您不需要 IDisposable 存储库,您需要处理连接。 我在手机上,所以很难给你一个更详细的例子。如果你需要,我可以稍后用更详细的例子编辑它。

您的设计中存在一些问题。首先,您的 IRepository 接口实现了多级抽象。创建用户是比连接管理更高层次的概念。通过将这些行为放在一起,您正在打破将我们推向狭窄角色界面的 Single Responsibility Principle which dictates that a class should only have one responsibility, one reason to change. You are also violating the Interface Segregation Principle

最重要的是,CreateConnection() 和 GetUser 方法是时间耦合的。 Temporal Coupling 是一种代码味道,您已经看到这是一个问题,因为您可以忘记对 CreateConnection.

的调用

除此之外,您将开始在系统中的每个存储库上看到连接的创建,并且每个业务逻辑都需要创建连接或从外部获取现有连接。这在长 运行 中变得不可维护。然而,连接管理是一个横切关注点;您不希望业务逻辑受到如此低级别的关注。

您应该首先将 IRepository 拆分为两个不同的界面:

public interface IRepository
{
    User GetUser();
}

public interface IConnectionFactory
{
    IDisposable CreateConnection();
}

您可以在更高级别管理事务,而不是让业务逻辑本身管理连接。这可能是请求,但这可能过于粗糙。您需要的是在表示层代码和业务层代码之间的某处开始事务,但不必自己复制。换句话说,您希望能够透明地应用这个横切关注点,而不必一遍又一遍地编写它。

这是我几年前开始使用 here 所描述的应用程序设计的众多原因之一,其中业务操作是使用消息对象定义的,其相应的业务逻辑隐藏在通用接口后面.应用这些模式后,您将有一个非常清晰的拦截点,您可以在其中启动事务及其对应的连接,并让整个业务操作 运行 在同一个事务中。例如,您可以使用以下可应用于应用程序中每个业务逻辑的通用代码:

public class TransactionCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;    
    public TransactionCommandHandlerDecorator(ICommandHandler<TCommand> decorated) {
        this.decorated = decorated;
    }

    public void Handle(TCommand command) {
        using (var scope = new TransactionScope()) {
            this.decorated.Handle(command);
            scope.Complete();
        }
    }   
}

此代码将所有内容都包裹在 TransactionScope 周围。这允许您的存储库简单地打开和关闭连接;这个包装器将确保仍然使用相同的连接。通过这种方式,您可以将 IConnectionFactory 抽象注入到您的存储库中,并让存储库在其方法调用结束时直接关闭连接,而在幕后,.NET 将保持真正的连接打开。

所以,您已经提到

we dont want to have long lasting connections in application

完全正确!

您需要在每个存储库方法实现中打开连接,对数据库执行查询或命令,然后关闭连接。我不明白你为什么要公开任何类似于域层的连接。换句话说,从存储库中删除 CreateConnection() 方法。不需要它们。每个方法在执行时都会open/close它里面。

有时您希望将多个存储库方法调用包装到某个东西中,但这只与 transaction 有关,而不是联系。在这种情况下,有 2 个答案:

  1. 检查您的 Repository pattern implementation. You should have repositories only for Aggregate Roots 是否正确。并非每个实体都有资格作为聚合根。聚合根是有保证的事务边界,所以你不应该担心存储库之外的事务 - 每个存储库方法调用自然会遵循边界,因为它一次只处理一个聚合根。
  2. 如果您仍然需要一次对多个聚合根执行操作,那么您将不得不实现一种称为 Unit of Work 的模式。这本质上是一个业务层的事务实现。我不建议在这种特定情况下将内置事务功能依赖到存储技术中(一次进行多个聚合),因为它们因供应商而异(而关系数据库可以一次保证多个聚合根,仅 NoSQL 数据库一次保证一个聚合)。

根据我的经验,您应该一次只需要修改一个聚合。工作单元是一种非常罕见的案例模式。因此,只需重新考虑您的存储库和聚合根,这应该可以解决您的问题。

只是为了回答的完整性 - 您确实需要有存储库接口,您已经有了。因此,您的方法已经可以进行单元测试了。

您正在将苹果与橙子和桃子混合。

这里涉及三个概念:

  • 存储库合约
  • 实施细节
  • 存储库生命周期管理

您的存储库在概念上包含用户,但它有一个 CreateConnection() 方法来指示实现的详细信息(需要连接)。不好。

您需要做的是从界面中删除 CreateConnection() 方法。现在您对什么是用户存储库有了一个真正的定义(顺便说一下,您应该称它为 IUserRepository)。

关于实施细节:

您有一个与数据库对话的用户存储库,因此您应该实现一个 DatabaseUserRepository class。这是存储创建连接和处理它的详细信息的地方。您可能决定在对象的生命周期内保持打开的连接,或者您可能决定最好为每个操作打开和关闭连接。

关于对象的生命周期:

你有一个依赖容器。您可能已经决定要将存储库用作单例,因为您的 DatabaseUserRepository class 实现了原子的、线程安全的操作,或者您可能希望存储库是瞬态的,因此创建了一个新实例,因为它实现了一个单元工作模式,这意味着所有更改都保存在一起(例如 EF.SaveChanges())。

现在看到区别了吗?

接口允许进行单元测试。任何需要数据库数据的组件都可以使用从内存加载垃圾的模拟存储库(例如 MemoryUserRepository)。

该实现提供了一个将用户存储在数据库中的存储库。您甚至可以决定使用此 class 的两个版本来实现接口以及不同的策略或模式。

存储库的生命周期将根据依赖容器中的实现细节进行设置。