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

时间:2016-10-18 17:32:27

标签: c# unit-testing inversion-of-control repository-pattern 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的类可以轻松测试并且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 个答案:

答案 0 :(得分:9)

您的设计中存在一些问题。首先,您的IRepository接口实现了多个级别的抽象。创建用户是一个比连接管理更高级别的概念。通过将这些行为放在一起,你打破了Single Responsibility Principle,它决定了一个班级应该只有一个责任,一个改变的理由。你也违反了推动我们走向狭窄角色界面的Interface Segregation Principle

最重要的是,CreateConnection()和GetUser方法是时间耦合的。 Temporal Coupling代码异味,您已经看到这是一个问题,因为您可以忘记对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下,.NET将保持打开实际连接。

答案 1 :(得分:2)

创建一个存储库工厂,用于创建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
}

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

答案 2 :(得分:2)

所以,你已经提到了

  

我们不希望在应用程序中建立持久的连接

这绝对是对的!

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

有时您希望将多个存储库方法调用包装成某些内容,但这只与 事务 相关,而不是与连接相关。在这种情况下,有2个答案:

  1. 检查here实施的正确性。您应该只为Repository pattern拥有存储库。并非每个实体都符合聚合根。聚合根是保证的事务边界,因此您不应该担心存储库中的事务 - 每个存储库方法调用自然会遵循边界,因为它一次只处理一个聚合根。
  2. 如果您仍然需要一次针对多个聚合根执行操作,那么您将必须实现一个名为Aggregate Roots的模式。这实际上是业务层事务实现。我建议不要将内置的事务功能依赖于这种特定情况的存储技术(一次性使用多个聚合),因为它们因供应商而异(而关系数据库可以一次保证多个聚合根,仅限NoSQL DB)一次保证单一聚合。)
  3. 根据我的经验,您应该只需要一次修改单个聚合。工作单位是一种非常罕见的案例模式。所以,只需重新考虑你的存储库和聚合根,这应该可以帮到你。

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

答案 3 :(得分:1)

你正在把苹果与橘子和桃子混合。

这里有三个概念:

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

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

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

关于实施细节:

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

到对象的生命周期:

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

现在看到差异?

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

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

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

答案 4 :(得分:-1)

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

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

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