领域驱动设计
一、运用领域模型
1、充分了解消化领域知识,当然是渐进式持续的学习,谁也不能保证在编码之前将领域知识吃透。
2、根据已知知识建模,随着对领域的深入理解,逐渐改进模型。
3、团队使用统一的语言,一个词,要保证整个团队对它的认知是一样的。后续词的意思变更势必要传达给团队中的每一个人,这也必将影响到模型和代码。
4、使用模型和业务人员或领域专家交流,对于模型中双方有歧义或感觉不对的点,深入交流,有可能就会发现更深层的模型。
5、要将交流的结果落入文档,文档中最好文字为主,图为辅。图最好是手绘的,很容易表明这是临时,粗略的而不是正式的、生产的设计。文档和图都不要过于涉及细节,只把控最要流程和领域知识,让代码去实现细节。
6、文档要保持更新,任何涉及领域知识的改动都要对文档做更新。
二、模型驱动设计的构造
1、Layered Architecture
将复杂的应用层序往往采用分层的架构模式,模型驱动设计中提倡把领域模型提取到单独的领域层中,通常情况下,一个程序将有以下部分构成:
- 表示层:即用户界面,这里的用户不一定是使用界面的人,也可以是另一个计算机系统。
- 应用层:定义软件要完成的任务,协调领域层对象解决问题,但该层要尽量简单,不包含业务规则或知识,只是为下一层的领域对象分配任务使它们相互协作。
- 领域层:业务软件的核心,负责表达业务概念的核心。
- 基础设施层:为上面各层提供通用的技术能力。
领域层是整个程序的精髓部分
2、Entity And Value Object
Entity 是一种对象,不过这类对象不是由其属性定义的,而是由一连串的事件或标识定义的。
相对而言
Value Object 就是一种由其属性定义的对象。
Entity 侧重定义一个需要跟踪其变化的事物,通常它会有一个唯一标识,无论它怎样变化,可根据唯一标识来判断它就是它。如不同的人可以有相同的名字、身高但其身份标识肯定是不同的。
Value Object 一般来说没有标识,是一个不变对象。如颜色对象 rgb(15, 91, 153),我们一般不会关心它是如何变化的,我们只需要将它传递给函数即可。
3、Service
有一些领域概念不适合被建模为对象,就应该在模型中添加一个作为独立接口的操作,并声明为 Service,好的 Service 通常有以下特征:
- 该操作不是 Entity 或 Value Object 的一个自然组成部分。
- 接口是根据领域模型的其他元素定义的。
- 操作是无状态的。
这里的无状态是指任何客户都可以使用 Service 的任何实例,而不必关心其历史状态。它可以产生副作用,但不影响其本身的行为状态。
4、Aggregate
顾名思义就是聚合,将一些强相关(业务逻辑紧密、生命周期包含或一致)的 Entity 聚合到一起,找出一个 Root-Entity,该聚合外的对象只能访问 Root-Entity,聚合内的对象只能由 Root-Entity 操作。当对聚合内部的任何对象修改时,聚合内的规则应该被满足。
聚合的好处就是可以在复杂的程序中,保持聚合内部对象的正确性。
5、Factory
当创建一个对象或一个 Aggregate 时,如果创建工作很复杂,或暴露了过多的内部细节,则可以使用 Factory 进行封装。
6、Repository
对 Entity、Value Object 等进行查询或持久化的对象。通常情况下一个 Aggregate 只提供一个 Root-Entity Repository。
三、重构
1、将隐式概念转变为显式概念
倾听领域专家的语言,有没有术语可以将复杂的概念简洁的表达出来?他们有没有纠正你的用词或当你说某个词的时候,他们露出疑惑的表情?这些都暗示了某个概念也许可以改进模型。
不断的重构,检查不足之处,思考矛盾之处或者借助设计模式也是发现隐式概念的途径。
2、柔性设计
- Intention-Revealing Interfaces 即类、方法和其他元素的名称既表达了初始开发人员创建他们的目的,也反映出他们将会为客户开发人员带来的价值。
- Side-Effect-Free Function 即无副作用函数,将命令(有副作用通常不会返回值)和查询(无副作用通常会返回值)分开,尽可能把命令隔离到不返回领域信息、非常简单的操作中。
- Assertion 即使用断言来判断接口是否满足规格和需求。
- Conceptual Contour 即概念轮廓,从单个方法、模块和大型结构的开发设计中遵从高内聚低耦合的原则,你会较为容易的发现一些功能单元具有逻辑一致性,赋予它们现实意义上的概念,可以使得设计既灵活又易懂。
3、应用分析模式
在特定领域中经常出现的通用模式或设计解决方案。这些模式可以帮助开发团队更有效地理解和处理领域中的常见问题,从而提高软件系统的设计质量和可维护性。
分析模式的核心思想是通过抽象出领域中通用的概念和关系,以一种模式化的方式来构建领域模型。这样做有助于避免重复性的设计工作,并且可以借鉴已经被验证的设计解决方案
4、通过重构加深理解
从重构中获得新的领域模型,往往非一日之功而是通过不断的重构来达到一个突破的契机。
四、战略设计
在开发设计一些大型结构,或不同模型需要交互的程序中,我们往往需要一些战略上的设计才能保持结构和职责清晰不混乱,可维护。
1、Bounded Context
明确定义模型应用的上下文,根据团队组织,软件的使用方法等设置模型的边界,边界之内严格保持一致性,保持它的纯粹而不受外界的干扰。
2、Context Map
画出上下文的范围,给出不同上下文之间联系的总体视图。边界应该有各自的名称以便我们讨论它。适时的将 Context Map 文档化,以便我们的认知保持一致。
根据团队间合作的困难度选择不同的应对方式(同一公司内部)
3、Continuous Integration
合作困难度:简单
一个上下文中要保持模型的一致性是不容易的,特别是团队人员较多时,不同人的理解不同。一个开发人员可能对方法做了轻微的修改,却改变了其他人员的开发意图而导致该方法失去了原有的作用。又或者一个开发人员正在实现已经实现了的概念,而造成的重复性实现。
保持上下文中模型的一致性就是持续集成的意图:建立一个把所有代码和其他实现工件频繁地合并到一起的过程,并通过自动化测试来快速查明模型的分裂问题。严格坚持使用UBIQUITOUS LANGUAGE,以便在不同人的头脑中演变出不能概念时,使所有人对模型都能达成一个共识。
4、Shared Kernel
合作困难度:一般
当无法持续集成或者持续集成的的开销非常高时可以考虑共享核心模式。
从模型中选出两个团队都同意共享的一个子集。一个团队在未与另一个团队商量的情况下,不能对该子集做任何修改。功能系统要经常集成,集成频率应该比持续集成低,进行集成时两个团队要运行测试。
5、Customer/Supplier Teams
合作困难度:比较困难
如果两个模型上下文有依赖关系且分属于不同的团队,那么很自然的就有上游和下游的关系,如果下游团队对变更具有否决权,或请求变更的程序太复杂,那么上游团队的开发自由度就会受到限制。由于担心破坏下游系统,上游团队甚至会受到抑制。同时,由于上游团队掌握优先权,下游团队有时也会无能为力。
在两个团队之间建立一种明确的客户/供应商关系。在计划会议中,下游团队相当于上游团队的客户。根据下游团队的需求来协商需要执行的任务并为这些任务做预算,以便每个人都知道双方的约定和进度。
两个团队共同开发自动化验收测试,用来验证预期的接口。把这些测试添加到上游团队的测试套件中,以便作为其持续集成的一部分来运行。这些测试使上游团队在做出修改时不必担心对下游团队产生副作用。
6、Conformist、Separate Way、Anticorruption Layer
合作困难度:困难
这种情况下,上游团队没有动力完成下游的需求,可能会做出承诺但兑现承诺往往遥遥无期。作者提出有三种方法来解决:
第一种:Separate Way, 完全放弃对上游的使用,自力更生。
第二种:Anticorruption Layer,上游的价值很大无法完全放弃,但下游团队仍需要开发自己的模型,下游团队就要担负起开发转换层的全部责任,这个层可能会非常复杂。其本质为在上下游中间创建一个创建一个隔离层来负责和上游模型的交互。该方法可能是我们更想使用的一种,作者介绍了一些好的实现,建议直接看书本讲解。
第三种:Conformist,做一个跟随者,严格的遵从上游团队的模型,同时你的模型功能也会受限。
7、Open Host Service
合作困难度:一般
当一个系统必须与大量其他系统进行集成时,为每个集成都定制一个转换层可能会减缓团队的工作速度。需要维护的东西会越来越多,而进行修改的时候担心的事情也会越来越多。
定义一个协议,把你的子系统作为一组 Service 供其他系统访问。开放这个协次,以便所有需要与你的子系统集成的人都可以使用它。当有新的集成需求时,就增强并扩展这个协议, 但个别团队的特殊需求除外。满足这种特殊需求的方法是使用一次性的转换器来扩充协议,以便使共享协议简单且内聚。
8、Published Language
把一个良好文档化的、能够表达出所需领域信息的共享语言作为公共的通信媒介,必要时在其他信息与该语言之间进行转换。
五、精炼
关注于核心问题而不被次要问题淹没。
1、Core Domain
核心领域:不同的项目其核心是不同的,识别出你所参与项目的核心是很重要的一部分
2、Generic Subdomain
模型中有些部分除了增加复杂性以外并没有捕捉或传递任何专门的知识。任何外来因素都会使CORE DONAN愈发的难以分辦和理解。模型中充斥着大量众所周知的一般原则,或者是专门的细节,这些细节并不是我们的主要关注点,而只是起到支持作用。然而,无论它们是多么通用的元素,它们对实现系统功能和充分表达模型都是极为重要的。 识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的MODULE中。任何专有的东西都不应放在这些模块中。 把它们分离出来以后,在继续开发的过程中,它们的优先级应低于CORE DOMAIN的优先级,并且不要分派核心开发人员来完成这些任务(因为他们很少能够从这些任务中获得领域知识)。
3、Domain Vision Statement
领域愿景说明:写一份CORE DOMAN的简短描述(大约一页纸)以及它将会创造的价值,也就是“价值主张”。那些不能将你的领域模型与其他领域模型区分开的方面就不要写了。展示出领域模型是如何实现和均衡各方利益的。这价描述要尽量精简。尽早把它写出来,随着新的理解随时修改它。
4、Highlighted Core
精炼文档,标明核心部分。代码库中通过包结果表明核心组件。
5、Cohesive Mechanism
封装机制:把概念上的COHESIVE MECHANISM分离到一个单独的轻量级框架中。要特别注意公式或那些有完备文档的算法。用一个INTENTION-REVEALING INTERFACE来暴露这个框架的功能。现在,领域中的其他元素就可以只专注于如何表达问题(做什么)了,而把解决方案的复杂细节(如何做)转移给了框架。
6、Segregated Core
对模型进行重构,把核心概念从支持性元素(包括定义得不清楚的那些元素) 中分离出来,并增强CORE的内聚性,同时减少它与其他代码的耦合。把所有通用元素或支持性元素提取到其他对象中,并把这些对象放到其他的包中一一即使这会把一些紧密糯合的元素分开。
一般步骤如下:
- 识別出一个CORE子领城(可能是从精炼文档中得到的)。
- 把相关的类移到新的MODULE中,并根据与这些类有关的概念为模块命名。
- 对代码进行重构,把那些不直接表示概念的数据和功能分离出来。把分离出来的元素放到其他包的类(可以是新的类)中。尽量把它们与概念 上相关的任务放在一起,但不要为了追球完美而浪费太长时间。把注意力放在提炼CORE子领域上,并且使CORE子领域对其他包的引用变得更明显且易于理解。
- 对新的SEGREGATED CORE MODULE进行重构,使其中的关系和交互变得更简单、表达得更清楚,并且最大限度地减少并澄清它与其他MODULE的关系(这将是一个持续进行的重构目标)。
- 对另一个CORE子领域重复这个过程,直到完成SECREGATED CORE的工作。
7、Abstract Core
把模型中最基本的概念识别出来,并分离到不同的类、抽象类或接口中。设计这个抽象模型,使之能够表达出重要组件之间的大部分交互。把这个完整的抽象模型放到它自己的MODULE中,而专用的、详细的实现类则留在由子领域定义的MODULE中。
注意这并不是机械的一个过程,它需要你深入理解关键概念以及它们在系统的主要交互中扮演的角色。
六、大型结构
在一个大的系统中,如果缺少一种全局性的原则而使人们无法根据元素在模式中的角色来解释这些元素,那么开发人员就会陷入只见树木不见森林的境地。
因此设计一种应用于整个系统的规则模式,使人们可以通过它在一定程度上了解各个部分在整体中所处的位置,即使在不知道各个部分的详细职责情况下。
当然如果没有找到一种符合整个系统的规则模式,就不要为了使用而使用,要记住宁缺毋滥的原则。
可以借助以下结构作为指导
1、System Metaphor
系统隐喻:对领域的一种类比,如防火墙,其本身只是现实中防止火灾蔓延的设计,软件中指的是保护局部网络免受来自更大的外部网的破坏。隐喻并不总是恰当的,但有时其可以传达整个设计的中心主题,并能够在团队所有成员中形成共同理解。
2、Responsibility Layer
为了保持大模型的一致,有必要在职责分配上实施一定的结构化控制。
3、Knowledge Level
知识级别:其应用场景是用户需要对模型的一部分有所控制,同时模型又必须满足更大的一组规则。其含义是指使软件具有可配置的行为,其中实体和角色的关系必须在安装时甚至运行时进行修改。
4、Pluggable Component Framework
可插拔组件式框架:从接口和交互中提炼出一个ABSTRACT CORE, 并创建一个框架,这个框架要允许这些接口的各种不同实现被自由替换。同样,无论是什么应用程序,只要它严格地通过ABSTRACT CORE的接口进行操作,那么就可以允许它使用这些组件。
七、总结
本书并不是一些具体的实现说明,更多的是一种战略方面指导。开发软件的本质是什么?不就是用软件来表示对应领域的知识嘛。其实本书和《Unit Testing》非常契合,两本书一块儿读会有更多的感悟。