我有什么:
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的工厂吗?它会很容易测试吗?
答案 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个答案:
根据我的经验,您应该只需要一次修改单个聚合。工作单位是一种非常罕见的案例模式。所以,只需重新考虑你的存储库和聚合根,这应该可以帮到你。
只是为了完整答案 - 您需要拥有已有的存储库接口。因此,您的方法已经可以进行单元测试。
答案 3 :(得分:1)
你正在把苹果与橘子和桃子混合。
这里有三个概念:
您的存储库在概念上包含用户,但它具有CreateConnection()方法,该方法指示实现的详细信息(需要连接)。不好。
您需要做的是从界面中删除CreateConnection()方法。现在您对用户存储库的内容有一个真正的定义(顺便说一下,您应该将其称为IUserRepository)。
关于实施细节:
您有一个与数据库通信的用户存储库,因此您应该实现DatabaseUserRepository类。这是存储创建连接和处理连接的详细信息。您可以决定在对象的生命周期内保持打开连接,或者您可以决定最好为每个操作打开和关闭连接。
到对象的生命周期:
您有一个依赖项容器。您可能已经决定将存储库用作单例,因为您的DatabaseUserRepository类实现了原子的,线程安全的操作,或者您可能希望您的存储库是瞬态的,因此创建了一个新的实例,因为它实现了一个工作单元模式表示所有更改都保存在一起(例如EF.SaveChanges())。
现在看到差异?
界面允许进行单元测试。任何需要数据库数据的组件都可以使用从内存加载垃圾的模拟存储库(例如MemoryUserRepository)。
该实现提供了一个存储库,用于将用户存储在数据库中。您甚至可能决定使用此类的两个版本来实现接口以及不同的策略或模式。
将根据依赖项容器中的实现细节设置存储库的生命周期。
答案 4 :(得分:-1)
我会创建一个连接工厂......
public class ConnectionFactory
{
public IDbConnection Create()
{
// your logic here
}
}
现在使它成为您的存储库的依赖项,并在您的存储库中使用它...您不需要IDisposable存储库,您需要处置连接。 我在手机上,所以很难给你一个更详细的例子。如果您需要,我可以稍后使用更详细的示例进行编辑。