EntityFramework之领域驱动设计实践(七)

模型对象的生命周期 - 仓储

上文中已经提到了管理领域模型对象生命周期的两大角色,即工厂与仓储,并对工厂的EntityFramework实践作了详细的描述。本节主要介绍仓储的概念,由于仓储的内容比较多,我将在接下来的两节中具体讲解仓储的架构设计与实践经验。

仓储(Repository),顾名思义,就是一个仓库,这个仓库保存着领域模型的实体对象。在业务处理的过程中,我们有可能需要把正在参与处理过程的对象保存到仓储中,也有可能会从仓储中读取需要的实体对象,抑或将对象直接从仓储中删除。上文也用一张简要的状态图描述了仓储在管理领域模型对象生命周期中所处的位置。

与工厂相同,仓储的关注对象也应该是聚合根,而不是聚合中的某个实体,更不应该是值对象。或许你会说,我当然可以针对销售订单行(Order Line)进行增删改查等操作,而无需跟销售订单(Sales Order)打交道。当然,你的确可以这样做,但如果你一定要坚持自己的观点,那么你就是把销售订单行(Order Line)当成是聚合根了,也就是说,你默许Order Line在你的领域模型中,是一种具有独立概念的实体。关于这个问题,在领域驱动设计的社区中,有人发表了更为“强势”的观点:

One interesting DDD rule is: you should create repositories only for aggregate roots.
When I read about it the first time I interpreted it this way: create repositories at least for all aggregate roots, but when you need a little repository for something else go ahead and implement it (and nobody will know what you did).
So I was thinking that the rule is somehow flexible. It turns out that it's not, and this is good: it keeps the domain stable and coherent. If entity A is an aggregate root, entity B is part of that aggregate, and you need to load B separated from the concept of A, this is a sign that the implementation does not reflect the business needs (anymore). In this case, B should probably become the root of its own aggregate

意思是说,如果实体A是聚合根,而B是该聚合中的一个实体,而你的设计希望绕过A而直接从仓储中获得B,那么,这就是一个信号,预示着你的设计可能存在问题,也就是说,B很有可能被当成是另一个聚合的根,而这个聚合只有一个对象,就是B本身。由此看来,聚合的划分与仓储的设计,在领域驱动设计的实践中是非常重要的内容。

工厂是从无到有地创建对象,从代码上看,工厂里充斥着new关键字,用以创建对象,当然,工厂的职责并不完全是new出一个对象那么简单。而仓储则更偏向于对象的保存和获得,在获得的时候,同样也会有新的对象产生,这个新的对象与保存进去的对象相比,引用不同了,但数据和业务ID值(也就是我们常说的实体键)是不变的,因此,在领域层看来,从仓储中读取得到的对象与当时保存进去的对象并没有什么两样。

你可能已经体会到,仓储就是一个数据库,它与数据库一样,有读取、保存、查询、删除的操作。我只能说,你已经了解到仓储的职能,并没有了解到它的角色。仓储是领域层与基础结构层的一个衔接组件,领域层通过仓储访问外部存储机制,这样就使得领域层无需关心任何技术架构上的实现细节。因此,仓储这个角色的职责不仅仅是读取、保存、查询、删除,它还解耦了领域层与基础结构层。在实践中,可以使用依赖注入的方式,将仓储实例注入到领域层,从而获得灵活的体系结构。

下面是我们案例中,仓储接口的代码:

隐藏行号 复制代码 仓储接口
  1. public interface IRepository<TEntity>
    
  2.     where TEntity : EntityObject, IAggregateRoot
    
  3. {
    
  4.     void Add(TEntity entity);
    
  5.     TEntity GetByKey(int id);
    
  6.     IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec);
    
  7.     void Remove(TEntity entity);
    
  8.     void Update(TEntity entity);
    
  9. }
    

IRepository是一个泛型接口,泛型类型被where子句限定为EntityFramework中的EntityObject,与此同时,where子句还限定了泛型类型必须实现IAggregateRoot接口。换句话讲,IRepository接口的泛型类型必须是继承于EntityObject类,并实现了IAggregateRoot接口的引用类型。根据我们在 “聚合”一文中的表述,我们可以实现针对Customer、Order以及Category实体类的仓储类。

这里只给出了仓储实现的一个引子,至少到目前为止我们已经简单地定义了仓储实现的一个框架,也就是上面这个IRepository泛型接口。接口中具体要包括哪些方法,不是本系列文章要讨论的关键问题。为了描述与演示,我们只为IRepository接口设计如上四个方法,即Add、GetByKey、Remove和Update。接下来,我将详细描述在基于实体框架(EntityFramework)的仓储设计中所遇到的困难,以及如何在实践中解决这些困难。

 

-----【以下为原文网友评论及回复信息】-----
 

Re:实体框架之领域驱动实践(七)

[ 2010-2-2 14:00:00 | By: Webcopy(游客) ]

这里的 IRepository接口被where子句限定为EF中的EntityObject,这就与EF框架紧耦合了.
个人觉得IResository接口定义应该是在领域模型中,不应该与具体的框架或类库关联.试想,如果我用NHibernate对 IResository接口进行实现,就与EF无关了.
也许我理解的不正确.....

以下为blog主人的回复:
我个人的意见还是具体问题具体分析。NHibernate是不需要实体类去继承任何类或者实现任何接口的,因此,在这里将IRepository的泛型类型约束为EntityObject也无妨。换句话说,即使你的类继承了EntityObject,NHibernate照样可以帮你完成持久化等操作。但如果你所挑选的ORM不是NHibernate,那就视情况而定了。你完全可以定义一个IEntity的接口并将其用作泛型约束。由于C#的单类多接口继承的特性,多出一个IEntity的接口并不会对你的设计造成任何影响。
你考虑问题的思路是正确的,至少你考虑到了框架的扩展性。当然,在实际中,这些基础结构的技术框架是很少变动的,也就是说,一旦选用了EF,就不太可能在今后的系统生命周期中再去换其它的ORM,除非是有特定的需求。在接下来的两篇博文中,我将介绍基于EF的仓储实现方式,以及一种与EF无关的通用仓储框架,在这两篇文章中,你还将体会到依赖注入带给我们的机遇。
(顺便BS下自己,最近太忙了,一直没有机会更新博客,但我一定会将本系列文章写完,敬请期待!)

Re:实体框架之领域驱动实践(七)

[ 2010-2-11 11:07:00 | By: haojie77 ]

Concrete Repository一般都定义在Infrastructure层, 而EF为我们生成的Entity Model又在Domain层, Domain依赖于Infrastucture, 这样一来Concrete Repository是不是也要依赖于Domain中的Entity Model 来生成entity? 这样的双向依赖在.Net中貌似不行. 当我们需要得到一个符合某些特定条件的聚合根集合的时候(比如要显示所有在中国并且姓"郑"的Customer), 那么CustomerRepository.FindBySpec(spec)中如何得到在Domain中定义的Customer? 如果在FindBySpec中只用根据spec拼接出来的sql那就没有这个问题. 换种说法就是 CustomerRepository.FindBySpec的返回类型如果是List<Customer>而不是 List<EntityObject>, Customer又由EF生成在Domain层中, 那么如何解决这个问题? 还是说把CustomerRepository也定义在Domain层中?(我看到您的StoreDDD貌似就是这个意思 Domain.Repositories) 因为那时你用的是ActiveRecord实现的, "有违反DDD经验的嫌疑"是否就是指把Repository定义在了Domain中? 现在如果用EF是否有办法能解决这个问题?
我甚至想过把EF自动生成的EntityObject当作DTO放在Infrastructure中, 而自己在Domain层中去定义真正的Entity去继承(或者是内聚更合理)EntityObject, 再给与它行为. 但是DDD中说Repository是用来唤醒Entity的, 这么做只能是拿到一个静态的DTO仍旧需要在Domain的Service中去构建 ConcreteEntity(ConcreteEntityObject), 一样有违反DDD经验的嫌疑(Entity并不是由Repository得到). 这只是我随意想到的, 可能思路已经不对了.

以下为blog主人的回复:
你的问题非常有价值。首先应该认识到产生依赖的根源,就是你把Concrete Repository也当做是整个系统架构的一个部分。然而事实是,Concrete Repository是一个可替代组件。要解决你的问题,你需要引入三件事情:接口、泛型以及依赖注入。
首先,将Repository的接口定义在领域层,接口采用泛型定义,也就是类似我在本文中的定义方式。注意:在领域层里的仅仅是一个接口而已,它跟技术架构没有任何关系,就像领域事件(Domain Events)一样,将一切与技术架构相关的具体实现委托给Concrete Repository,这样仍然能够保持领域模型的纯净度;其次,在定义Concrete Repository的Assembly上直接引用Domain Model,这样做是合理的,因为Concrete Repository不是系统架构中的一部分,而仅仅是一个可替换的组件,因此其实现方式是可以任意的,不会影响整个系统架构;再次,使用依赖注入,将 Concrete Repository注入到Domain Model中,至此,Domain Model一直是在使用Repository的接口,而没有关心这个接口的背后是否是Concrete Repository,当然,在你的Concrete Repository中也可以使用规约模式对对象进行筛选和验证。常用的依赖注入框架有Spring.NET和Castle Windsor。
根据你的提问,我临时开发了一个Sample,下载地址是:http://www.sunnychen.org/attachments /RepositoryStorm.rar。通过这个例子,你可以看到一个非常纯净的领域模型。

Re:实体框架之领域驱动实践(七)

[ 2010-2-11 12:55:00 | By: haojie77 ]

关于我说的把EF生成的 EntityObject放入Infrastructure中, 代码上貌似还是可以实现. Entity Data Model 从名字上看,微软本身对其的定义还是基于数据的. 所以我才想到把EF生成的Entity当做DTO, 这样很多情况下也不用另外构建DTO,直接用EntityObject就可以了. 这里可能扯得有点远了, 也许我这个想法本身有不少疏漏和错误. 在这里希望能学习到各方面的知识:)
namespace DomainModel.Model
{
class Customer : RealEntity
{
public Customer(EntityObject eObject) : base(eObject)
{
}
}
}
namsapce Infrasturcture.DomainBase
{
public abstract class RealEntity
{
private EntityObject _EntityObject;
public RealEntity(EntityObject eObject)
{
_EntityObject = eObject;
}
}
}
namespace Infrasturcture.Repositories
{
class CustomerRespository<T> : IRepository<T> where T : RealEntity
{
public T FindBy(object key)
{
EntityObject eObject = null;// Use linq to entity to get it
return Activator.CreateInstance(typeof(T), eObject) as T;
}
}
}

以下为blog主人的回复:
你有这方面的考虑是很好的。我只能说是Microsoft对Entity的定义使你在理解上产生了偏差。如我前面的文章所述,EF最大的一个缺点就是不支持实体行为,即使有,也是硬生生地从SQL Stored Procedure来产生的,但这不能成为实体行为。于是,在EF中,Entity就成了充斥着getter/setter的数据对象了,也就是DDD所提到的贫血模型。
从我个人的角度,我仍然会将EntityObject放在领域层,这样就不需要你定义的那个RealEntity类;EntityObject是要有行为的,才能参与领域活动,如何使其具有行为?我的解决方案在前文也有讲述:使用C#的partial关键字。
通常情况下,DTO是用来在层与层之间交换数据用的,在DDD中属于值对象,你将EntityObject用作DTO是不合适的,因为 EntityObject具有EF所给定的实体键,应该视为实体,既然是实体,就不能穿越层的界定线。我更偏向于在Domain和UI之间使用DTO,而不是Domain和Repository之间。因为Repository维护的是实体的生命周期,而不是DTO。
最后,关于您的Domain和Repository解耦的问题,我已经在您的上个评论中留下了一个案例的下载地址,您可以通过下载这个案例来了解如何在 DDD中引入Repository。

posted @ 2010-07-07 09:43  dax.net  阅读(22910)  评论(22编辑  收藏  举报