本文对DDD的各种模式进行归纳总结。
首先,需要对模式这个概念进行一些解释。
模式是人们在某些领域对最佳实践的一些经验总结,从而形成的一种概念,方便交流及能通过这种最佳实践快速高效解决相关问题。在DDD中描述的这些模式很多都不是一些技术上的模式,可能是关于工作方式或思考方式上的模式,如通用语言模式和领域驱动设计模式。当然,也会包括一些技术上的模式,如实体、值对象等。
模式:通用语言(UBIQUITOUS LANGUAGE)
将模型作为通用语言的核心,确保团队所有交流以及代码编写都坚持使用这种语言。在画图、写东西,特别是讲话时也要使用这种语言。
通过尝试不同的表示方法(它们反映了备选模型)来消除难点。然后重构代码,重新命名类、方法和模块,以便与新模型保持一致。解决交谈中的术语混淆问题,就像我们对普通词汇形成一致的理解一样。
要认识到,UBIQUITOUS LANGUAGE的更改就是对模型的更改。
领域专家应该抵制不合适或无法充分表达领域理解的术语或结构,开发人员应该密切关注那些将妨碍设计的有歧义和不一致的地方。
通用语言成为开发人员、领域专家和软件产品之间传递信息的渠道。
模式:领域驱动设计(MODEL-DRIVEN DESIGN)
在领域建模过程中,需要坚持通用语言模式,软件系统各个部分的设计应该忠实地反映领域模型,以便体现出二者之间的明确对应关系。我们应该反复检查并修改模型,以便软件可以更加自然地实现模型,即使想让模型反映出更深层次的领域概念时也应如此。
从模型中获取用于程序设计和基本职责分配的术语。让程序代码成为模型的表达,代码的改变可能会是模型的改变。
个人见解:将通用语言注入到领域建模过程中,模型既要抽象出关键的领域知识,也要支持有效的实现,保证领域、模型、实现在某一时刻的一致性,对领域不断深入的理解,将会导致模型的改变,模型的改变必将导致代码的修改。
模式:建模者应亲力亲为(HANDS-ON MODELER)
*任何参与建模的技术人员,不管在项目中的主要职责是什么,都必须花时间了解代码。任何负责修改代码的人员则必须学会用代码来表达模型。每一个开发人员都必须不同程序地参与模型讨论并且与领域专家保持联系。参与不同工作的人都必须有意识地通过通用语言与接触代码的人及时交换关于模型的想法。
主要是为了不要让模型和实现脱节。
模式:分层架构(LAYERED ARCHITECTURE)
给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也不需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识。
这种分层概念有点老旧了,对领域模型适用性好的架构应该是六边形架构。
选择框架的基本依据:框架应能很好地服务领域模型建模,而不应掺杂进与领域无关的内容。
把领域隔离出来的最大好处是可以真正专注于领域设计,而不用考虑其他方面。
关联
对象之间的关联使得建模与实现之间的交互更为复杂。
对象之间的关联可以看作是对象指针引用,也可以是数据库查询的一种封装。例如,一对多关联可以用一个集合类型的实例变量来实现,也可以使用一个访问方法来查询数据库的方式。
至少有3种方法可以使得关联更易于控制:
- 按领域所倾向的方向,规定一个遍历方向。如限定多对多关联的遍历方向可以有效地将其实现简化为一对多关联。
- 添加一个限定符,以便有效地减少多重关联。如美国有过很多位总统,这是一对多关联,如果在这个关联上加上任期,即一个任期只会有一个总统,这就转化为一对一关联。
- 消除不必要的关联。
模式:实体(ENTITY)
很多对象不是通过它们的属性定义的,而是通过连续性和标识定义的。
实体有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。为了有效地跟踪这些对象,必须定义它们的标识。
实体是有标识的,并且在整个生命周期具有连续性。对“连续性”的理解有两个方面,一是标识在整个生命周期内是不变的,二是相同的标识代表相同的事物。
当一个对象由其标识(而不是属性)区分时,那么在模型中应该主要通过标识来确定该对象的定义。使类定义变得简单,并集中关注生命周期的连续性和标识。定义一种区分每个对象的方式,这种方式应该与其形式和历史无关。要格外注意那些需要通过属性来匹配对象的需求。在定义标识操作时,要确保这种操作为每个对象生成唯一的结果,这可以通过附加一个保证唯一性的符号来实现。这种定义标识的方法可能来自外部,也可能是由系统创建的任意标识符,但它在模型中必须是唯一的标识。模型必须定义出“符合什么条件才算是相同的事物”。
实体建模
实体最基本的职责是确保连续性,保持实体的简练是实现这一责任的关键。这要求我们只添加那些对概念至关重要的行为和这些行为所必需的属性,尤其是那些用于识别、查找或匹配对象的特征。此外,应该将行为和属性转移到与核心实体关联的其他对象中。除了标识问题之外,实体往往通过协调其关联对象的操作来完成自己的职责。
模式:值对象(VALUE OBJECT)
用于描述领域的某个方面而本身没有概念标识的对象称为值对象。这里的某个方面可以是尺度描述,也可以是行为描述(如抽象成接口),总之,整个值对象应该形成一个概念整体。
值对象可以由多个对象组成,甚至是引用实体对象。
值对象可以用作ENTITY的属性。
值对象经常作为参数在对象之间传递消息。它们常常是临时对象,在一次操作中被创建,然后丢弃。
值对象设计
我们并不关心使用的是值对象的哪个实例。由于不受这方面的约束,设计可以获得更大的自由,因此可以简化设计或优化性能。在设计值对象时有多种选择,包括复制,共享或不可变对象。
当值对象可以限定为不可变对象时,使用共享方式可以节省更多资源开销,这时一般可以使用享元模式。反过来说也是成立的,如果要共享一个值对象,那么最好将其限定为不可变对象。
当值对象作为参数或返回值传递给另一个对象时,为了防止值对象被修改,也可以将其限定为不可变对象或通过复制传递一个副本。
保持值对象不变可以极大地简化实现,并确保共享和引用传递的安全性,而且这样做也符合值的意义。
通过复制值对象优化数据库
如果一个对象被许多对象引用,其中有些对象将不会在它附近(不在同一分页上),这就需要通过额外的物理操作来获取数据。通过复制(而不是共享对同一个实例的引用),可以将这种作为很多ENTITY属性的值对象存在在ENTITY所在的同一分页上。这种存储相同数据的多个副本的技术称为非规范化(denormalization),当访问时间比存储空间或维护的简单性更重要时,通常使用这种技术。比如,在关系数据库中,将具体的值放到拥有此值的ENTITY表中。
ENTITY之间的双向关联很难维护,但两个值对象之前的双向关联则完全没有意义。
模式:领域服务(DOMAIN SERVICE)
在某些情况下,最清楚、最实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是领域服务。
一些领域概念不适合被建模为对象。如果勉强把这些重要的领域功能归为ENTITY或VALUE OBJECT的职责,那么不是歪曲了基于模型的对象的定义,就是人为地增加了一些无意义的对象。
使用领域服务时应谨慎,它们不应该替代ENTITY和VALUE OBJECT。但是,当操作实际上是一个重要的领域概念时,领域服务很自然就会成为MODEL-DRIVEN DESIGN中的一部分。
好的领域服务有以下3个特征:
- 与领域概念相关的操作不是ENTITY或VALUE OBJECT的一个自然组成部分。
- 接口是根据领域模型的其他元素定义的。
- 操作是无状态的。
当领域中的某个重要的过程或转换操作不是ENTITY或VALUE OBJECT的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为SERVICE。定义接口时要使用模型语言,并确保操作名称是通用语言中的术语。此外,应该使SERVICE成为无状态的。
粒度
领域服务除了可以表达一个特殊领域概念操作之外,它还可以控制领域层中的接口粒度,并且避免客户端与ENTITY和VALUE OBJECT耦合。领域对象的行为有时候粒度过小,导致客户端不得不处理领域对象之间复杂、细致的交互,从而使得领域逻辑蔓延到应用层。这时候领域服务就可以为这些交互打包成更大粒度的功能。
模式:模块(MODULE)
使用模块有一些技术上的原因,但主要原因却是“认知超载”。MODULE为人们提供了两种观察模型的方式,一是可以在MODULE中查看细节,而不会被整个模型淹没,二是观察MODULE之间的关系,而不考虑其内部细节。
MODULE之间应该是低耦合的,而在MODULE的内部则是高内聚的。耦合和内聚的解释使得MODULE更像是一种技术指标,仿佛是根据关联和交互的分布情况来机械地判断它们。然而,MODULE并不仅仅是代码的划分,而且也是概念的划分。一个人一次考虑的事情是有限的(因此才要低耦合)。不连贯的思想和“一锅粥”似的思想同样难于理解(因此才要高内聚)。
选择能够描述系统的MODULE,并使之包含一个内聚的概念集合。这通常会实现MODULE之间的低耦合,但如果效果不理想,则应寻找一种更改模型的方式来消除概念之间的耦合,或者找到一个可作为MODULE基础的概念(这个概念先前可能被忽视了),基于这个概念组织的MODULE可以以一种有意义的方式将元素集中到一起。找到一种低耦合的概念组织方式,从而可以相互独立地理解和分析这些概念。对模型进行精化,直到可以概念高层领域概念对模型进行划分,同时相应的代码也不会产生耦合。MODULE的名称应该是通用语言中的术语,应反映出领域的深层知识。
模式:聚合(AGGREGATE)
我们应该将ENTITY和VALUE OBJECT分门别类地聚集到AGGREGATE中,并定义每个AGGREGATE的边界。在每个AGGREGATE中,选择一个ENTITY作为根,并通过根来控制对边界内其他对象的所有访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保AGGREGATE中的对象满足所有固定规则,也可以确保在任何状态变化时AGGREGATE作为一个整体满足固定规则。
固定规则(invariant),也称为不变条件,是指在数据变化时必须保持的一致性规则,其涉及AGGREGATE成员之间的内部关系。而任何跨越AGGREGATE的规则将不要求每时每刻都保持最新状态。通过事件处理、批处理或其他更新机制,这些依赖会在一定的时间内得以解决。但在每个事务完成时,AGGREGATE内部所应用的固定规则必须得到满足。
总的来说,要实现这种概念上的AGGREGATE,需要对所有事务应用一组规则。
- 根ENTITY具有全局标识,它最终负责检查固定规则。
- 根ENTITY具有全局标识。边界内的ENTITY具有本地标识,这些标识只在AGGREGATE内部才是唯一的。
- AGGREGATE外部的对象不能引用除根ENTITY之外的任何内部对象。根ENTITY可以把对内部ENTITY的引用传递给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个VALUE OBJECT的副本传递给另一个对象,而不必关心它发生什么变化,因为它只是一个VALUE,不再与AGGREGATE有任何关联。
- 作为上一条规则的推论,只有AGGREGATE的根才能直接通过数据库查询获取。所有其他对象必须通过遍历关联来发现。
- AGGREGATE内部的对象可以保持对其他AGGREGATE根的引用
- 删除操作必须一次删除AGGREGATE边界之内的所有对象。
- 当提交对AGGREGATE边界内部的任何对象的修改时,整个AGGREGATE的所有固定规则都必须被满足。
模式:工厂(FACTORY)
应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一个整体,并确保它满足固定规则。
任何好的工厂都需满足以下两个基本需求:
- 每个创建方法都是原子的,而且要保证被创建对象或AGGREGATE的所有固定规则。
- FACTORY应该被抽象为所需的类型,而不是所要创建的具体类。
工厂的应用位置
FACTORY的作用是隐藏创建对象的细节,而且我们把FACTORY用在那些需要隐藏细节的地方。例如,如果需要向一个已存在的AGGREGATE添加元素,可以在AGGREGATE的根上创建一个FACTORY METHOD。又比如当一个对象的创建主要使用另一个对象的数据(或者还有规则)时,则可以在后者的对象上创建一个FACTORY METHOD,这样就不必将后者的信息提取到其他地方来创建前者。
FACTORY应该只被关联到与被构建对象有着密切联系的对象上。
在设计FACTORY方法签名时,应注意FACTORY与其参数发生的耦合。
模式:REPOSITORY
为每种需要全局访问的对象类型创建一个对象,这个对象相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的全局接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体条件来挑选对象的方法,并返回属性值满足查询条件的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的AGGREGATE根提供REPOSITORY。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给REPOSITORY来完成。