仓储模式 - 使其可测试,适用于DI和IoC并且IDisposable

6

我所拥有的:

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的类可以轻松进行测试并且容易与IoC容器集成。然而,使用该类的人在调用从数据库获取数据的任何方法之前必须调用CreateConnection,否则会抛出异常。这本身是有好处的——我们不希望在应用程序中有持久性连接。因此,使用这个类,我像这样实现。

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

很遗憾,这不是一个好的解决方案,因为使用这个类的人(包括我自己!)经常会忘记在调用从数据库获取信息的方法之前调用_repository.CreateConnection()

为了解决这个问题,我看了 Mark Seemann 的博客文章 SUT Double,他以正确的方式实现了 Repository 模式。不幸的是,他使 Repository 实现了 IDisposable 接口,这意味着我不能简单地通过 IoC 和 DI 将其注入到类中并在使用后继续使用,因为在使用一次后它就会被释放。他每个请求使用一次,并利用 ASP.NET WebApi 的能力在请求处理完成后将其清除。但我有我的类实例一直在工作,它们需要使用 Repository。

在这里最好的解决方案是什么?我应该使用某种工厂来给我提供 IDisposable IRepository 吗?那样会容易进行测试吗?

5个回答

10

你的设计中存在一些问题。首先,你的 IRepository 接口实现了多层抽象。创建用户是比连接管理更高层次的概念。将这些行为放在一起会违反单一职责原则,该原则规定一个类应该只有一个职责,只有一个改变的原因。你还违反了接口隔离原则,该原则推动我们朝着窄角色接口的方向发展。

此外,CreateConnection()GetUser 方法之间存在时间耦合。时间耦合 是代码异味,你已经发现这是一个问题,因为你可能会忘记调用 CreateConnection

除此之外,每个存储库都需要创建连接,或从外部获取现有连接,并且每个业务逻辑都将开始看到这种情况。长期来看,这将变得难以维护。连接管理是一个跨越多个领域的关注点;你不希望业务逻辑涉及到这样低层次的关注点。

你应该首先将 IRepository 拆分为两个不同的接口:

public interface IRepository
{
    User GetUser();
}

public interface IConnectionFactory
{
    IDisposable CreateConnection();
}

不要让业务逻辑自己管理连接,您可以在更高的层面上管理事务。这可能是请求,但这可能粒度太大了。您需要在展示层代码和业务层代码之间的某个地方开始事务,但无需重复自己。换句话说,您希望能够透明地应用这个横切关注点,而无需一遍又一遍地编写它。

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

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的底层,实际的连接将保持打开状态。


2

所以,你已经提到了

我们不想在应用程序中拥有长时间的连接

这是完全正确的!

您需要在每个存储库方法实现中打开连接,针对数据库执行查询或命令,然后关闭连接。我不明白为什么您要向域层公开像连接这样的东西。换句话说,从存储库中删除CreateConnection()方法。它们是不必要的。每个方法在实现时都会在内部打开/关闭它。

有时您会希望将几个存储库方法调用包装成某些内容,但这仅涉及事务,而不是连接。在这种情况下,有两个答案:

  1. 检查您的存储库模式实现的正确性。您应该只为聚合根创建存储库。并非所有实体都符合聚合根的条件。聚合根是保证事务边界,因此您不必担心事务超出存储库的范围 - 每个存储库方法调用自然遵循边界,因为它仅处理一次聚合根。
  2. 如果您仍然需要在一次操作中执行多个聚合根的操作,则必须实现称为工作单元的模式。这本质上是业务层事务实现。对于此特定情况(一次性执行多个聚合),我不建议依赖存储技术内置的事务功能,因为它们因供应商而异(虽然关系型数据库可以保证一次性执行���个聚合根,但NoSQL数据库只能保证一次执行一个聚合根)。

从我的经验来看,您应该只需要一次修改单个聚合。工作单元是非常罕见的情况模式。因此,仅重新思考您的存储库和聚合根即可解决问题。

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


2
创建一个仓库工厂,用于创建 IDisposable 仓库。
public interface IRepository : IDisposable {
   User GetUser();
   //other methods, doesn't matter
}

public interface IRepositoryFactory {
    IRepository Create();
}

你可以在 using 块内创建它们,使用完毕后会自动释放。
using(var repository = factory.Create()) { 
    var user = repository.GetUser(); 
    //do something with user
}

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

请不要使用工厂模式。链接 - Steven

1

你在混淆概念,将苹果、桃子和橙子混在了一起。

这里涉及到三个概念:

  • 存储库契约
  • 实现细节
  • 存储库生命周期管理

你的存储库在概念上持有用户,但它有一个 CreateConnection() 方法,指明了实现细节(需要连接)。这不好。

你需要从接口中删除 CreateConnection() 方法。现在你有了一个真正的用户存储库定义(顺便说一下,你应该称之为 IUserRepository )。

接下来是实现细节:

你有一个与数据库通信的用户存储库,因此应该实现一个 DatabaseUserRepository 类。这里存储着创建连接和处理连接的细节。你可以决定为对象的生命周期保持一个开放的连接,或者为每个操作打开和关闭一个连接。

最后是对象的生命周期:

你有一个依赖容器。你可能已经决定将你的仓库用作单例,因为你的DatabaseUserRepository类实现了原子、线程安全的操作,或者你可能希望你的仓库是短暂的,这样就会创建一个新的实例,因为它实现了一个工作单元模式,这意味着所有的更改都是在一起保存的(例如EF.SaveChanges())。
看到区别了吗?
接口允许进行单元测试。任何需要从数据库获取数据的组件都可以使用一个加载内存中垃圾的模拟仓库(例如MemoryUserRepository)。
实现提供了一个将用户存储在数据库中的仓库。你甚至可以决定有两个版本的这个类,它们都实现了接口以及不同的策略或模式。
仓库的生命周期将根据依赖容器中的实现细节进行设置。

-1

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

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

现在将其作为依赖项添加到您的存储库中,并在您的存储库中使用它... 您不需要一个IDisposable存储库,您需要处理连接。

我正在使用手机,所以很难给您提供更详细的示例。如果需要,我可以稍后编辑并提供更详细的示例。


这是一个处理实现细节的具体类。解决方案在接口的抽象层面上。 - JuanR
通过连接工厂,他将能够仅处理连接,而不是存储库。那个-1没关系,那不是一个完整的答案。我本来想稍后编辑它,但我忘了。 - Rafael Marques

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接