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的工厂吗?它会很容易测试吗?
解决方法
最重要的是,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下将保持打开实际连接.