【转载】实体框架之领域驱动实践(七):模型对象的生命周期 - 仓储
上文中已经提到了管理领域模型对象生命周期的两大角色,即工厂与仓储,并对工厂的Entity Framework实践作了详细的描述。本节主要介绍仓储的概念,由于仓储的内容比较多,我将在接下来的两节中具体讲解仓储的架构设计与实践经验。
仓储(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值(也就是我们常说的实体键)是不变的,因此,在领域层看来,从仓储中读取得到的对象与当时保存进去的对象并没有什么两样。
你可能已经体会到,仓储就是一个数据库,它与数据库一样,有读取、保存、查询、删除的操作。我只能说,你已经了解到仓储的职能,并没有了解到它的角色。仓储是领域层与基础结构层的一个衔接组件,领域层通过仓储访问外部存储机制,这样就使得领域层无需关心任何技术架构上的实现细节。因此,仓储这个角色的职责不仅仅是读取、保存、查询、删除,它还解耦了领域层与基础结构层。在实践中,可以使用依赖注入的方式,将仓储实例注入到领域层,从而获得灵活的体系结构。
下面是我们案例中,仓储接口的代码:
IRepository是一个泛型委托接口,泛型类型被where子句限定为Entity Framework中的EntityObject,与此同时,where子句还限定了泛型类型必须实现IAggregateRoot接口。换句话讲,IRepository接口的泛型类型必须是继承于EntityObject类,并实现了IAggregateRoot接口的引用类型。根据我们在“聚合”一文中的表述,我们可以实现针对Customer、Order以及Category实体类的仓储类。
这里只给出了仓储实现的一个引子,至少到目前为止我们已经简单地定义了仓储实现的一个框架,也就是上面这个IRepository泛型接口。接口中具体要包括哪些方法,不是本系列文章要讨论的关键问题。为了描述与演示,我们只为IRepository接口设计如上四个方法,即Add、GetByKey、Remove和Update。接下来,我将详细描述在基于实体框架(Entity Framework)的仓储设计中所遇到的困难,以及如何在实践中解决这些困难。