http://www.cnblogs.com/netfocus/archive/2012/02/12/2347938.html
http://qinghua.github.io/ddd/
http://www.cnblogs.com/netfocus/p/4492486.html
1. 聚合根、实体、值对象的区别?
从标识的角度:
聚合根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法;
从是否只读的角度:
聚合根除了唯一标识外,其他所有状态信息都理论上可变;实体是可变的;值对象是只读的;
从生命周期的角度:
聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体完全由其所属的聚合根负责管理维护;值对象无生命周期可言,因为只是一个值;
2. 聚合根、实体、值对象对象之间如何建立关联?
聚合根到聚合根:通过ID关联;
聚合根到其内部的实体,直接对象引用;
聚合根到值对象,直接对象引用;
实体对其他对象的引用规则:1)能引用其所属聚合内的聚合根、实体、值对象;2)能引用外部聚合根,但推荐以ID的方式关联,另外也可以关联某个外部聚合内的实体,但必须是ID关联,否则就出现同一个实体的引用被两个聚合根持有,这是不允许的,一个实体的引用只能被其所属的聚合根持有;
值对象对其他对象的引用规则:只需确保值对象是只读的即可,推荐值对象的所有属性都尽量是值对象;
3. 如何识别聚合与聚合根?
明确含义:一个Bounded Context(界定的上下文)可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根;
识别顺序:先找出哪些实体可能是聚合根,再逐个分析每个聚合根的边界,即该聚合根应该聚合哪些实体或值对象;最后再划分Bounded Context;
聚合边界确定法则:根据不变性约束规则(Invariant)。不变性规则有两类:1)聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合;2)聚合内的某些对象的状态必须满足某个业务规则;
例子分析1:订单模型
Order(一 个订单)必须有对应的客户信息,否则就不能称为一个有效的Order;同理,Order对OrderLineItem有不变性约束,Order也必须至少有一个OrderLineItem(一条订单明细),否 则就不能称为一个有效的Order;另外,Order中的任何OrderLineItem的数量都不能为0,否则认为该OrderLineItem是无效 的,同时可以推理出Order也可能是无效的。因为如果允许一个OrderLineItem的数量为0的话,就意味着可能会出现所有 OrderLineItem的数量都为0,这就导致整个Order的总价为0,这是没有任何意义的,是不允许的,从而导致Order无效;所以,必须要求Order中所有的OrderLineItem的数量都不能为0;那么现在可以确定的是Order必须包含一些OrderLineItem,那么应该是通 过引用的方式还是ID关联的方式来表达这种包含关系呢?这就需要引出另外一个问题,那就是先要分析出是OrderLineItem是否是一个独立的聚合 根。回答了这个问题,那么根据上面的规则就知道应该用对象引用还是用ID关联了。那么OrderLineItem是否是一个独立的聚合根呢?因为聚合根意 味着是某个聚合的根,而聚合有代表着某个上下文边界,而一个上下文边界又代表着某个独立的业务场景,这个业务场景操作的唯一对象总是该上下文边界内的聚合 根。想到这里,我们就可以想想,有没有什么场景是会绕开订单直接对某个订单明细进行操作的。也就是在这种情况下,我们 是以OrderLineItem为主体,完全是在面向OrderLineItem在做业务操作。有这种业务场景吗?没有,我们对 OrderLineItem的所有的操作都是以Order为出发点,我们总是会面向整个Order在做业务操作,比如向Order中增加明细,修改 Order的某个明细对应的商品的购买数量,从Order中移除某个明细,等等类似操作,我们从来不会从OrderlineItem为出发点去执行一些业 务操作;另外,从生命周期的角度去理解,那么OrderLineItem离开Order没有任何存在的意义,也就是说OrderLineItem的生命周 期是从属于Order的。所以,我们可以很确信的回答,OrderLineItem是一个实体。
例子分析2:帖子与回复的模型,做个对比,以便更好地理解。
不 变性分析:帖子和回复之间有不变性规则吗?似乎我们只知道一点是肯定的,那就是帖子和回复之间的关系,1:N的关系;除了这个之外,我们看不到任何其他的 不变性规则。那么这个1:N的对象关系是一种不变性规则吗?不是!首先,一个帖子可以没有任何回复,帖子也不对它的回复有任何规则约束,它甚至都不知道自 己有多少个回复;再次,发表了一个回复和帖子也没有任何关系;其次,发表回复对帖子没有任何改变;从业务场景的角度去分析,我们有发表帖子的场景,有发表 回复的场景。当在发表回复的时候,是以回复为主体的,帖子只是这个回复里所包含的必要信息,用于说明这个回复是对哪个帖子的回复。这些都说明帖子和回复之 间找不出任何不变性约束的规则;因为帖子和回复都有各自独立的业务场景的需要,所以可以很容易理解它们都是独立的聚合根;那也很容易知道该如何建立他们之 间的关联了,但是我们要尽量减少关联,所以只保留回复对帖子的关联即可;帖子没有任何必要去保存一个回复的ID的列表;那么你可能会说,当我删除一个帖子 后,回复应该是没有存在的意义的呀?不对,不是没有存在的意义,而是删除了帖子后导致了回复对帖子的关联信息的缺失,导致数据不一致。这是因为帖子和回复 之间有一种必然的联系(1:N),回复一定会有一个对应的帖子;但是回复有其自己的生命周期,不应该随着帖子的删除而级联删除。这种情况下,如果你删除了 帖子,就导致回复也成为了一条无效的数据;所以,我们绝对不允许删除任何聚合根,因为一旦你删除了聚合根,那就意味着与该聚合根相关的其他任何聚合根都会 有外键引用缺失的问题,会导致整个领域模型数据的不一致;所以,永远都不要删除聚合根;
http://qinghua.github.io/ddd/
《领域驱动设计》书里写的是:用户会把软件程序应用于某个主体区域,这个区域就是软件的领域。简单来说,就认为是公司的某块业务好了。如果领域比较大,可以将其拆分为多个子域(Subdomain),子域包含核心域(Core Domain)和支撑子域(Supporting Subdomain),核心域顾名思义,是最重要的子域,我们应该把关注点集中在它上面;其余的子域都是支撑子域。支撑子域里有一类特殊的用于解决通用问题的子域,称为通用子域(Generic Subdomain),例如用户和权限等。不过这些都是相对而言的,对于消费方来说,他的支撑子域有可能就是你的核心域。个别子域可能会有交集,称为共享内核(Shared Kernel),目的是减少重复,但是仍保持两个独立的上下文。由于不同子域的开发团队可能会同时修改共享内核,所以需要小心并注意沟通。
通用语言(Ubiquitous Language)
其实写软件就像是翻译,把领域上的业务需求翻译成软件的各个功能。业务需求来自领域专家(Domain Expert),程序员们需要把领域专家的语言翻译成程序。如果程序员们翻译的时候使用的是自己的语言,而领域专家使用自己的行话,导致术语不一致,就会使得沟通不顺畅,难于消化知识。所以团队需要一种通用语言来进行沟通。这样的通用语言尽量以业务语言为主,而非技术语言。一开始的通用语言可能不尽完美,但它就像是代码一样,经常需要重构。例如:“创建一个订单”就比“插入一条订单数据”更容易让领域专家明白谈话的背景。
限界上下文(Bounded Context)
通用语言里,同一个名词在不同的场景里不一定有相同的意思。比如用户,在推荐好友(可能关注年龄、性别、地域)或是浏览商品(可能关注喜好、历史购买记录)的时候有着不同的含义。所谓的不同的场景,其实就是不同的限界上下文。子域在限界上下文中完成开发。限界上下文主要用来封装通用语言和领域模型,显式地定义了领域模型的边界。不同的限界上下文,都会有一套自己的通用语言。通过这样的划分方式,来让每个限界上下文都尽量保持简单,也算是SRP原则在不同粒度上的一个体现。如果不去做这样的划分,可能最终这个软件就会演变成一个大泥球,或者说是单块系统。尽管如此,对于比较小的业务或项目来说,可能只会有一个限界上下文。现在流行的微服务,很大程度上就是按照限界上下文来划分服务。例如:商品上下文,订单上下文,物流上下文等。当然,如果子域很小,不见得非得微服务化。
模型
实体(Entity)
所谓领域,反映到代码里就是模型。模型分为实体和值对象两种。实体是有标识(Identity)的,两个拥有相同属性的实体不是相等的,除非它们的标识相等;而不同实体的标识不能相等。例如:某人下了两个相同的订单,里面都购买了相同的商品。这两个订单就是有标识(订单号)的两个实体,虽然内容相同,但它们是两个不同的实体。常用的标识有自增数字、Guid、自然标识(如邮箱、身份证号)等。实体具有生命周期,它们的内容可能在这期间会发生改变,但是标识是永远不会变化的。实体作为领域模型的主体,需要拥有自己的方法,方法名来自于通用语言。通过这些方法来保证自己始终是一致的状态,而非被调用者set来set去。例如:
people.runTo(x, y)
,而非people.setX(x);people.setY(y);
值对象(Value Object)
实体用来表示领域中的一个东西,而值对象只用于描述或度量一个东西。值对象没有任何标识,只要两个值对象的属性相等,那么它们就是相等的。值对象是不可变的,如果要改变值对象的内容,那就重新创建一个值对象。值对象没有生命周期,因为它只是值而已。例如:金额(含数值和货币单位),颜色(含rgb值)等。因为不需要标识,所以它们其实比实体要简单许多
不同的领域需求可能会催生不同的建模。例如:考虑一下演出的售票系统。如果需求是对号入座,那么座位就是实体,一旦某张演出票关联了某个座位,那么这个座位就再也不能被其它的演出票所关联了。如果需求是先到先坐,那么座位就是值对象,我们只关心卖了多少张演出票,不要超过座位上限即可,而并不用关心哪个座位被哪张票所关联了。
DDD的一个反模式就是拥有一堆
get
和set
方法的贫血领域对象(Anemic Domain Object)。这样的对象只是一个数据持有器(data holder),而非我们想要的领域模型。值对象和实体一样,都需要有自己的方法。例如:金额值对象,有一个Add
的方法,接受一个金额参数,返回一个新的值对象。
实体里可以包含值对象,值对象里也可以包含实体。
领域服务(Domain Service)
有些操作不属于实体或者值对象,那就不用强塞给它们,创建领域服务来提供这些操作吧。留意通用语言,如果里面出现了名词,那一般就是实体或值对象;如果里面出现了动词,那通常就意味着领域服务。例如:支付,这是一个比较明显的业务操作。另外,如果有什么操作会让实体变得臃肿,也可以使用领域服务来解决。但是,不能把所有的东西都堆到领域服务里,过度使用领域服务会导致贫血对象的产生。
据Eric Evans所言,设计良好的领域服务具有以下三个特征:
- 操作不是实体/值对象的一个自然的部分
- 接口根据领域模型的其它元素定义
- 操作无状态
还需要注意的是,不要把领域服务和应用服务混起来了。我们在领域服务里处理业务逻辑,而并不在应用服务里处理。应用服务是领域模型的直接客户,负责处理事务、安全等操作。
领域事件(Domain Event)
《领域驱动设计》一书出版之后,DDD社区并没有停止前进的步伐。领域事件就是在那之后提出来的。领域事件是一个定义了领域专家所关心的事件的对象。当关心的状态由于模型行为而发生改变时,系统将发布领域事件。如果通用语言里出现了:“当……的时候,需要……”通常就意味着一个领域事件。例如:当订单完成支付时,商品需要出库。这里的订单完成支付就预示着一个OrderPaidEvent,里面持有着这个订单的标识。领域事件代表的是已经发生的事,所以命名上通常都使用过去时(如Paid)。对领域事件的处理就像是一个观察者模式,由领域事件的订阅方来决定。订阅方既可以是本地的限界上下文,也可以是外部的限界上下文。
生命周期
聚合(Aggregate)
聚合就是一组应该呆在一起的对象,聚合根(Aggregate Root)就是聚合在一起的基础,并提供对这个聚合的操作。聚合除了聚合根以外,还有自己的边界(boundary),即聚合里有什么。例如:一个订单可以有多个订单明细,订单明细不可能脱离订单而存在,而订单也不可能没有订单明细。这种情况下,订单和订单明细就是一个聚合,而订单就是这个聚合的聚合根,订单和订单明细就处于这个聚合的边界之内。如果要变更订单明细,我们需要通过操作聚合根订单来实现,如
order.changeItemCount()
,而非订单明细自身。另外一个例子:一名客户可以有多个订单,订单不可能脱离客户而存在,而客户却可以没有订单。这种情况下,客户和订单就是不同的两个聚合,一个聚合以客户为聚合根,另一个聚合以订单为聚合根,引用客户的标识。客户里并不引用订单的标识,这样将关联减至最少有助于简化对象的关系网。但是带来的一个麻烦就是如果要查找某位客户的所有订单,就不得不从所有的订单里查,而不能从客户这个聚合里直接获得。最后再举一个多对多的例子:一个班级可以有多名学生,学生可以脱离这个班级而存在,而班级不能没有学生,学生也不能不在班级里。这种情况下,班级和学生也是不同的两个聚合,一个聚合以班级为聚合根,引用学生的标识;另一个聚合以学生为聚合根,引用班级的标识,将多对多转换成两个一对多。
聚合是持久化的一个单位,我们需要保证以聚合为单位的数据一致性。如果聚合太大,那就会导致并发修改困难,多人并发修改同一个聚合里的不同项目,结果就是只有第一个提交的人成功修改,其它人不得不重新刷新聚合才能再次修改。大聚合还会导致性能问题,因为操作实体时会将整个大聚合同时加载进内存。珍爱生命,拒绝大聚合。
聚合根必须是实体而非值对象,因为它需要整体持久化,所以一定会有标识。而聚合根里的各个元素,既可能是实体,也可能是值对象。例如:一个订单(聚合根)一般会有订单明细(实体)和送货地址(值对象)。这些元素里可以有对聚合根的引用,但是不能相互引用。任何对其它元素的操作都必须通过聚合根来进行。聚合根里的标识是全局的,聚合根里的实体标识是聚合里唯一的本地标识,因为对它的访问都是通过聚合根来操作的。聚合根拥有自己独立的生命周期,其实体的生命周期从属于其所属的聚合,值对象因为只是值而已,并没有生命周期。
工厂(Factory)
工厂是生命周期的开始阶段,它可以用来创建复杂的对象或是一整个聚合。复杂对象的创建是领域层的职责,但它并不属于被创建的对象自身的职责。实体和值对象的工厂不太一样,因为值对象是不可变的,所以需要工厂一次性创建一个完整的值对象出来。而实体工厂则可以选择创建之后再补充一些细节。
资源库(Repository)
资源库是生命周期的结束,它封装了基础设施以提供查询和持久化聚合的操作。这样能够让我们始终聚焦于模型,而把对象的存储和访问都委托给资源库来完成。以订单和订单明细的聚合为例,因为一定是通过订单这个聚合根来获取订单明细,所以可以有订单的资源库,但是不能有订单明细的资源库。也就是说,只有聚合才拥有资源库。需要注意的是,资源库并不是数据库的封装,而是领域层与基础设施之间的桥梁。DDD关心的是领域内的模型,而并非是数据库的操作。理想的资源库对客户(而非开发者)隐藏了内部的工作细节,委托基础设施层来干那些脏活,到关系型数据库、NOSQL、甚至内存里读取和存储数据。
- 领域驱动设计(DDD)是一种基于模型驱动的软件设计方式。它以领域为核心,分析领域中的问题,通过建立一个领域模型来有效的解决领域中的核心的复杂问题。Eric Ivans为领域驱动设计提出了大量的最佳实践和经验技巧。只有对领域的不断深入认识,才能得到一个解决领域核心问题的领域模型。如果一个应用的复杂性不是在技术方面的,而是在领域本身,即领域内的业务很复杂,那这种应用,使用领域驱动设计的价值就越大。
- 领域驱动开发也是一种敏捷开发过程(极限编程,XP),强调迭代开发。在迭代过程中,强调开发人员与领域专家需要保持密切的合作关系。极限编程假设我们能通过不断快速重构完善设计。所以,对开发人员的要求非常高。
- 领域驱动设计提出了一套核心构造块(Building Blocks,如聚合、实体、值对象、领域服务、领域工厂、仓储、领域事件,等),这些构造块是对面向对象领域建模的一些核心最佳实践的浓缩。这些构造块可以使得我们的设计更加标准、有序。
- 统一语言(Ubiquitous Language),是领域驱动设计中一个非常重要的概念。任何一个领域驱动设计的项目,都需要一种通用语言,一套通用的词汇。因为没有通用的语言,就没有一致的概念,沟通就会遇到障碍,最后的领域模型和软件也就无法满足领域内的真实业务需求。通用语言是领域专家和开发人员在对领域问题的沟通、需求的讨论、开发计划的制定、领域模型的设计,以及开发人员之间对领域模型的具体编码落地实现,等一系列过程中,所有人员使用的一种通用语言。话句话说,就是无论是沟通时所用的词汇、还是领域模型中的概念、还是代码中出现的类名与方法,只要是相同的意思,那就应该使用相同的词汇。可以看出,这种通用语言不是一下子就可以形成,而是在一个各方人员讨论的过程中,不断发现、明确,与精炼出来的。
- 领域模型是领域驱动设计的核心。统一语言中的所有关键词汇,在领域模型上应该都能找到。各方人员沟通时,都应该以领域模型为基础。通过讨论的不断深入,大家对领域的认识也会不断深入,领域模型也会不断得到完善,统一语言的词汇也会不断丰富和精准。需要特别强调的是,开发人员应该尽量保证代码实现和领域模型相绑定,时刻保持代码与模型的一致。如果不绑定,那代码就会慢慢和模型相脱节,就会出现像我们以前那样的设计文档和代码相脱节一样的问题,甚至模型还会起到误导作用。通过这样一种思路,我们确保语言、模型、代码三者紧密绑定,确保最后实现出来的软件可以准确无误的实现业务需求,并且还能让我们的软件可以快速的和业务同时演进。而不像传统的开发方式那样,分析、设计、实现三个阶段完全脱节,最后出来的软件没有很好的满足业务需求,也不能在未来很快的跟业务需求一起演进。所以,领域模型同时承载了分析的结果和设计的结果,这里的分析是指对领域内业务需求的分析,设计是指对模型的设计以及软件的设计。所以,我们的领域模型,不能只考虑业务需求,还要同时考虑软件设计的原则,是一种综合考虑的、平衡的设计结果。
- 领域模型可以复用,因为特定的领域模型解决的都是某个特定的问题域;比如淘宝网有个商品中心,有个商品模型,核心概念有商品分类、商品;商品模型负责解决电子商务领域中的商品目录(Product Catalog)子域。后来阿里又出了个天猫,也会有商品中心,但是这两个商品中心基本是一样的问题域。所以,我们可以复用之前淘宝实现的商品中心领域模型,并复用之前淘宝商品中心的解决方案,来解决天猫的商品维护和展示。当然,这个只是我个人的认识,一个例子。具体阿里是否是一个商品中心同时解决淘宝和天猫的业务,没具体调研过。
Domain-Driven Design (or DDD) is a software development approach that focuses on the domain model first. Techniques within DDD allow teams to separate various "domains" (related parts of the application) from one another with the resulting benefit of trading away generic models that hold the behavior for many contexts with models designed to a specific context.