但是大多数人实现
IUnitOfWork
接口的唯一目的是实现更容易的可测试性-我就是其中之一。我不是在购买“伙计,我可以交换我的ORM,而无需接触我的应用程序的其余部分”,这并不是因为它经常是一种理论上的优势,即使不是简单的谎言。所以我喜欢让它保持愚蠢的简单(KISS),并且仅执行启用我的
DbContext
类的模拟所需的最少操作-这是我在最近的项目中编写的真实的IUnitOfWork
接口:< IUnitOfWork接口
public interface IUnitOfWork
{
int SaveChanges();
IDbSet<TEntity> Repository<TEntity>() where TEntity : class;
}
实体框架的
DbContext
是工作单元,而IDbSet<TEntity>
是存储库。因此,不是全力以赴编写通用存储库并添加大量无用的复杂性(在我不需要启用它来模拟DbContext的意义上是无用的),而是让我的DbContext
派生类实现了我的IUnitOfWork
接口:CallAssistantContext类
[DbConfigurationType(typeof(MySqlEFConfiguration))]
public class CallAssistantContext : DbContext, IUnitOfWork
{
public CallAssistantContext(string connectionString)
: base(connectionString)
{ }
public IDbSet<TEntity> Repository<TEntity>() where TEntity : class
{
return base.Set<TEntity>();
}
public IDbSet<CustomerServiceCall> CustomerServiceCalls { get; set; }
public IDbSet<CallType> CallTypes { get; set; }
public IDbSet<ContactType> ContactTypes { get; set; }
public IDbSet<IssueType> IssueTypes { get; set; }
public IDbSet<Customer> Customers { get; set; }
public IDbSet<CustomerStore> CustomerStores { get; set; }
public IDbSet<SalesRep> SalesReps { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
“客户端代码”取决于抽象-
IUnitOfWork
接口,并且可以很容易地在单元测试中通过模拟实现轻松构造函数注入:看起来也不太复杂:
public class MainWindowViewModel : ViewModelBase
{
private readonly IUnitOfWork _context;
public MainWindowViewModel(IUnitOfWork context)
{
_context = context;
}
那么,这种方法公然有错吗?在这里,我将UoW / context直接注入到需要的地方-在一个更大的项目中,我将创建一个“服务”,并用构造函数将UoW注入那里。
#1 楼
我是第二个TopinFrassi的回答,这或多或少是明智的方法,并且实现非常好。但是,存在一些潜在的问题,我将从Craftworkgames的评论和您的答复中进行扩展。因为我真的找不到任何需要批评的东西,所以这将完全集中在设计上。传递一个声称它可以提供的接口有两个主要问题
IDbSet<TEntity> Repository<TEntity>() where TEntity : class;
你承诺很多
你将其暴露在与数据访问直接相关的代码之外(即使它在使用中)类,这些类不太可能与DA直接相关,除非在很小的应用程序中)
这可能引起的问题是:不仅仅是消费者的需求。任何只需要处理
SalesRep
的类都不应该传递给在Customer
上执行任意粗体运算的方法。这违反了接口隔离原则。模拟的困难。一个可以提供很多接口的接口意味着需要大量模拟。您可能可以缩小单个测试中实际需要模拟的范围,但是通常,其实用性必须与您并未真正尝试测试的SUT的实现细节相结合。测试更加脆弱。
泄漏抽象。抽象不仅仅是交换实现的能力,这意味着即使您对此没有兴趣,泄漏的抽象仍然是一个问题。在这种情况下,您需要将大型EF陷阱暴露给必须处理实体的每个类:
IQueryable
。它给IEnumerable
留下了很好的印象,但是随后抛出了IEnumerable
会很乐意处理的LINQ扩展的很多参数的异常。必须记住,代码需要转换为SQL并不是您应该在整个服务层中暴露的问题。缺少特定于实体的方法。除了为您的所有服务类提供过多的常规方法之外,您可能还没有为它们提供足够的特定方法。您很有可能会在多个位置(包括多个服务类)使用针对特定实体类型的查询或命令,但是这些查询或命令的长度/复杂度足以重复整个查询或命令。违反DRY。
解决
最后一个问题是针对特定答案大喊大叫的一个问题,所以让我们开始吧。假设我们已经编写了一个带有私有方法的服务类,该方法为我们执行了复杂的查询。现在,我们正在编写另一个服务类,尽管功能并不相似,但我们发现自己需要与该私有方法中的查询相同的查询。进入自己的类:
public class FirstFooService()
{
public FirstFooService(IUnitOfWork unitOfWork, FooQueryHelper queryHelper) { //... }
//...
}
public class SecondFooService()
{
public SecondFooService(IUnitOfWork unitOfWork, FooQueryHelper queryHelper) { //... }
//...
}
但是现在看来,这真是一团糟。我们依靠两种不同的东西-工作单元和查询助手-在两种不同的抽象级别上进行数据访问。我们实际上想要哪个级别?
好吧,我认为答案很简单。我们添加
FooQueryHelper
特别是因为我们发现自己想要抽象而不是直接访问IUnitOfWork
。此外,如果我们看一下以上问题,FooQueryHelper
可以解决所有这四个问题。它本质上可以求解1,2和4,并且可以简单地解决3,只需将其返回类型保留为IEnumerable
而不是IQueryable
,并且不公开任何采用任意Func
或Expression
作为过滤器的方法。 > 剩下的就是将我们消耗的
IUnitOfWork
方法提取到FooQueryHelper
上,缩小它们的范围并使其更加具体,以使它们尽可能保持正确的抽象级别。您可能会看到妙语,我们最终得到的不是FooQueryHelper
,而是FooRepository
。这可以遵循您将在各处找到的标准通用存储库模式。如果回头看根本问题,您也会看到这些问题也得到了解决:现在,所有问题都依赖于更小,更有针对性的界面,并且数据访问问题仍保留在数据访问类中,而不是扩展到每个关心的服务实体。结论
但是,KISS呢?可能您已经知道了通用存储库,并且由于其不必要的复杂性和间接性,您希望避免使用它。好吧,是的,与以往一样,在一个小型,简单的应用程序中会进行权衡取舍,在什么时候进一步巩固代码消耗的开发资源比买回去还要多?那是您必须做出的判断电话。但是希望我给出了一些其他原因,而不仅仅是“让我可以交换我的ORM”,原因是您为什么选择这种更复杂的模式。实施起来非常复杂或繁琐。它实际上只是作为
IDbContext
的薄包装而开始的。但是不要被误导!存储库不仅应该是从IDbSet
到您控制的接口的适配器,还应该是功能完善的抽象层!因此,如果您发现自己添加了更多方法,则可能证明了该模式的选择。评论
\ $ \ begingroup \ $
这个特定的应用程序很小,但是答案非常棒,非常有用,而且非常有价值。
\ $ \ endgroup \ $
– Mathieu Guindon♦
2014年9月24日上午11:23
\ $ \ begingroup \ $
@ Mat'sMug很高兴听到它,非常感谢您的反馈!
\ $ \ endgroup \ $
–本·亚伦森
2014年9月24日上午11:54
\ $ \ begingroup \ $
FooQueryHelper会包含UoW的查询逻辑是吗?辅助程序是否特定于UoW的实现,因为它需要IQueryable
\ $ \ endgroup \ $
–IEatBagels
2014年9月24日12:19在
\ $ \ begingroup \ $
@TopinFrassi对。帮助程序将使用IQueryable,但不会返回它,因此该实现特定于EF,但它公开的接口不是。万一答案还不清楚,FooQueryHelper是一个中间阶段,展示了重构如何将您带到(通用)存储库模式。
\ $ \ endgroup \ $
–本·亚伦森
2014-09-24 13:17
\ $ \ begingroup \ $
那就是我的想法,但是我想确保自己理解正确:)
\ $ \ endgroup \ $
–IEatBagels
2014-09-24 13:47
#2 楼
您的代码对我来说似乎真的很不错,我无话可说,但是我删除了OnModelCreating
覆盖,因为它没有任何用途。
评论
漂亮的标题!!!!!我在github.com/imranbaloch/ASPNETIdentityWithOnion
中有一个示例
是的,在某些时候,您显然必须依赖EF,但其想法是将不可嘲笑的代码量保持在最低水平。这通常发生在系统的边界,例如数据库,文件系统,UI,系统时钟,随机。
这实际上取决于您的应用程序范围。我认为诀窍是先尝试编写单元测试。如果代码无法正常工作,那么在构建代码时,它很快就会变得很痛苦。那时您可以重构。
确实是@DanPantry Bummer。也许我很幸运,但是二十多年来,无论大小我都不需要交换数据访问层。当然,请更改数据库本身,但不要更改ORM。