- 英文原名:Domain-Driven Design: Tackling Complexity in the Heart of Software
- 作者:Eric Evans
- 译者:赵俐 盛海艳 刘霞 等
- 审校:任发科
第一章 消化知识
有效建模的要素
- 模型和实现的绑定
- 建立一种基于模型的语言
- 开发一个蕴含丰富知识的模型
- 提炼模型
- 头脑风暴和实验
第二章 交流与语言的使用
务必要记住模型不是图。图的目的是帮助表达和解释模型。
不能强制用图来表示全部模型或设计,因为这样会削弱图的清晰表达的能力。
过犹不及。图和表都有很强的表现力,但是如果试图将所有信息都塞进图表中,只会导致观看者陷入茫然。在使用图表时一定要记住这一点。
第三章 绑定模型和实现
如果模型不能直接帮助开发可运行的软件,那么这种纸上谈兵的模型又有什么意义呢?
模型是关联领域和开发的纽带,所以不能偏于一方。模型需要充分体现和说明领域自身的性质和领域对象间的关联,但同时也需要是能够用于实际开发的。
⭐️模式:模型驱动设计 Model-Driven Design (MDD)
建模范式和工具支持
在本小节中,作者比较了Java、Prolog、C、Fortran等一系列编程语言,核心角度是建模范式。Java有一套完整的面向对象的建模范式(可能更常用的一个词是设计模式),Prolog可以使用基于逻辑的建模范式,而Fortran在实现数学函数方面有它的优势。类似C这样的纯粹过程语言,没有一套适用的建模范式,因此一般无法用于模型驱动设计。
⭐️模式:亲身实践的建模者 Hands-On Modeler
将分析、建模、设计和编程工作过度分离会对MDD产生不良影响。
过度分工带来弊病。软件设计和开发中,角色分工也有“合久必分分久必合”的态势。在前后端分离成为常态的今天,全栈开发又成为了众多开发者追求的目标。测试驱动开发(TDD)让开发者承担起一部分测试职能,而DevOps的盛行,则让开发和运维更加紧密地结合在了一起。众多工具的诞生,也使得一个开发者独力承担设计、开发、测试和运维等工作变为可能。
具体到MDD中来说,如果建模者自己完全脱离了软件开发实践,自然难以了解实际开发中会遇到的种种问题和困难,也就难以设计出适应于开发人员需要的模型。这就好像说技术管理人员、架构师等不能完全停止编程,否则势必与实际开发脱节。
第四章 分离领域
⭐️模式:分层结构 Layered Architecture
本书中将软件架构分为四层,自上至下为:用户界面层、应用层、领域层、基础设施层。越靠上的层级与终端用户越为接近。<br />靠上的层级可以通过调用下层元素的公共接口,直接使用或操作下层元素;但如果下层元素需要与上层元素通信,则需要依赖回调/观察者等模式。
❓为什么要抽出单独的领域层?
如果与领域有关的代码分散在大量的其他代码之中,那么查看和分析领域代码就会变得异常困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而想要调整业务规则也很可能需要对用户界面代码、数据库操作代码或其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也会给自动化测试带来困难。
❓如何创建能够处理复杂任务的程序?
- 关注点分离。
- 分离的同时,也需要维持系统内部复杂的交互关系。
❓实现MDD的关键是?
领域层的分离。
❓基础设施层如何提供功能?
- 服务(Service)。例如:发送电子邮件、发送短信由基础设施层实现,应用层中的元素只需要直接请求发送消息,而不需要关心消息具体是如何发送的。
- 架构框架。例如:为领域对象提供抽象基类,以及关联机制(比如MVC等框架)。
❓如何最优化使用框架?
不妄求万全之策,而是有选择性地运用框架来解决难点问题,以避开框架的很多不足之处。明智而审慎地选择框架中最具价值的功能能够减少程序实现和框架之间的耦合,使随后的设计决策更加灵活。
也就是说,并不是用了框架,就要所有功能都使用框架提供的实现方式,或者按照框架的标准去实现。完全可以将其他的实现方式(在当前场景中更加合适或更加简单的)与框架相结合
🔴模式:The Smart UI
这是与DDD相反的一种设计模式,基本理念是把所有的业务逻辑、业务规则都在用户界面中实现。这种设计模式对于小型项目、能力不足的开发人员和开发团队来说,尤其有效。<br />
<br />❓为什么作者要在这里讲Smart UI?<br />软件开发过程中,设计方法、基础设施等的选择,一定要与项目的需求相匹配。
项目团队常犯的错误是采用了一种复杂的设计方法,却无法保证项目从头到尾始终使用它。另一种常见的也是代价高昂的错误则是为项目构建一种复杂的基础设施以及使用工业级的工具,而这样的项目根本不需要它们。
第五章 软件中所表示的模型
表示模型的三种模型元素模式
- Entity
- Value Object
- Service
模型中每个可遍历的关联,软件中都要有同样属性的机制。
❓现实中的关联往往非常复杂,如何对这些关联进行有效的控制?
- 规定一个遍历方向
- 添加限定符,以减少多重关联
- 消除不必要的关联
实例:
- 通过限定国家->人的单向关系,并加上时期作为限定符,可以有效控制国家->总统这一关联。
- 用股票名称作为投资的限定符,可以有效控制账户->投资这一关联。
⭐️模式:实体Entity(又称Reference Object)
很多对象不是通过它们的属性定义的,而是通过连续性和标识定义的。
最典型的例子:人
- 一个人5岁和50岁的长相、身高、体重都有极大差别,但是同一个人
- 两个人的名字、出生日期、出生地可能都一样,但是他们是两个不同的人
用最简单通俗的话来说,实体对象需要一个标识,或者说ID。标识必须是唯一的,不会冲突的。
有时,某些数据属性或属性组合可以确保它们在系统中具有唯一性,或者在这些属性上加一些简单约束就可以使其具有唯一性。
比如说日报、杂志等,一般来说就可以靠名称、日期等唯一确定。这种情况下,就可以不需要再额外增加一个标识,或者ID。
⭐️模式:值对象Value Object
很多对象没有概念上的标识,它们描述了一个事物的某种特征。
一个错误的做法是给所有对象都加上标识。
跟踪Entity的标识是非常重要的,但为其他对象也加上标识会影响系统性能并增加分析工作,而且会使模型变得混乱,因为所有对象看起来都是相同的。
值对象可以复制,也可以共享。Flyweight(享元)这一设计模式的核心就是值对象的共享。
❓什么情况更适合用共享?
- 节省数据库空间或者减少对象数量是一个关键要求
- 通信开销很低
- 共享对象被严格限定为不可变
❓什么情况下需要允许共享对象变更?
- Value频繁改变
- 创建或删除对象的开销很大
- 替换有可能会打乱集群
- Value的共享不多,或者共享对集群性能没有提升
⭐️模式:服务Service
有时,对象不是一个事物。
❓好的Service有什么特征?
- 与领域概念相关的操作不是Entity或Value Object的一个自然组成部分。
- 接口是根据领域模型的其他元素定义的。
- 操作是无状态的。
❓不同层级的Service如何划分?
- 基础设施层的Service不应该具有任何业务意义。比如发送邮件、发送短信等。
- 领域层的Service执行一些细粒度的领域对象操作,避免把领域知识泄漏到应用层中。领域层Service可以帮助保持应用层和领域层之间的界限。比如,检查一个转账请求是否合法。
- 应用层的Service执行具体的业务。比如,执行一个转账请求。
⭐️模式:模块Module(又称Package)
高内聚低耦合在Module的设计中尤为重要。
在分析一个Module的内容时,应当很少需要参考那些与之交互的其他Module。
建模范式
❓在面向对象为主的系统中混入非对象元素,需要注意哪些方面?
- 不要和实现范式对抗。
- 把通用语言作为依靠的基础。
- 不要一味依赖UML。
- 保持怀疑态度。
第六章 领域对象的生命周期
⭐️模式:聚合Aggregate
聚合实际上是构建边界的过程。经过聚合后,只有根Entity才具有全局标识,能够被外部对象直接访问。而聚合内部的Entity只具有本地标识(可能全局不唯一),必须经由根Entity进行访问。
⭐️模式:工厂Factory
如果创建对象或Aggregate的工作很复杂,或者暴露了过多的内部结构,则可以使用Factory进行封装。
这里讨论的Factory,包含了OO设计模式中的Factory Method、Abstract Factory、Builder等多种设计模式。
❓什么样的Factory是好的Factory?
- 创建必须是原子的,并且必须保证所创建对象或Aggregate的所有固定规则。
- 生成的Entity在创建完成后可以添加可选元素
- 生成的Immutable Value Object所有属性都初始化为正确的最终状态
- 如果出错,应该抛出异常,而不能创建一个错误的对象
- Factory应该被抽象为所需的类型,而不是所要创建的具体类。
⭐️模式:仓库Repository
我们可以通过对象之间的关联来找到对象。但当它处于生命周期的中间时,必须要有一个起点,以便从这个起点遍历到一个Entity或Value。
从数据库中检索一个已经存在的对象,是对象的重建过程。
大多数对象都不应该通过全局搜索来访问。
在设计之初,就需要确定哪些对象允许通过全局搜索进行访问。
相关的技术包括:
- 将SQL封装到Query Object中
- 使用Metadata Mapping Layer实现对象和表之间的转换
这些技术的问题在于,它们并没有考虑领域模型中的概念,而是对数据库操作的封装。
Repository可以用来封装这些解决方案,并将我们的注意力重新拉回到模型上。
Repository相当于它所对应的那个类型的所有对象的集合的一个替身/代理。
❓使用Repository的好处?
- 提供了一个获取持久化对象和管理对象生命周期的简单模型
- 使应用程序、领域设计、持久化技术解耦
- 体现了有关对象访问的设计决策
- 可以很容易地被替换为“哑实现”(dummy implementation),以便调试和测试之用
Repository中的查询分为两种:硬编码的和基于Specification的。原则上,应该同时提供这两类查询。对于常用的查询,比如根据ID查找用户,应该提供对应的硬编码查询接口;而同时,需要提供基于Specification的,具有一定灵活度的接口,以便用户实现一些更复杂的查询功能(条件查询、多表关联……)。
客户代码可以忽略Repository的实现,但开发人员不能。
对开发人员来说,必须考虑Repository底层的实现细节,考虑所使用的数据库等的特性。
❓使用Repository的注意点?
- 对类型进行抽象
- 充分利用与客户解耦的优点
- 将事务的控制权交给客户
第七章 使用语言:一个扩展的示例
以一个货物运输的应用为案例,重点体现了前述的分层结构、实体、值对象、模块、聚合、工厂、仓库等模式的应用。
第八章 突破
持续重构让事物逐步变得有序。
重构的原则是始终小步前进,始终保持系统正常运转。
但如果更改模型的益处远远超过了工期延长的代价,那就做吧!Just do it!
迭代的不仅仅是产品功能,还有领域模型。随着开发的不断深入,以及与领域专家的深入沟通,可以对领域有更加深入的认识,从而不断优化模型的设计。
第九章 将隐式概念转变为显式概念
⭐️模式:规格Specification
这一模式借鉴了逻辑编程范式中的谓词概念(可分离、可组合的规则对象)。<br />Specification实际创建的是一种特殊的Value Object。
❓Specification的用途?
- 验证对象是否满足某些需求
- 从集合中选择一个或一些对象
- 指定在创建新对象时需要满足某些需求
实际上,Specification最大的好处就是它把这三件事统一到了一起。
可以通过关键组件的模型驱动原型来缓解合作开发的僵局。
第十章 柔性设计
柔性设计是对深层建模的补充。
⭐️模式:释义命名接口Intention-Revealing Interfaces
如果开发人员为了使用一个组件而必须要去研究它的实现,那么就失去了封装的价值。
命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目的的。这样可以使客户开发人员不必去理解内部细节。
⭐️模式:无副作用函数Side-Effect-Free Function
只有实现了无副作用的函数,才能安全地对多个操作进行组合,而不必深入研究其实现细节。
⭐️模式:断言Assertion
借助断言,可以把操作的后置条件和类及Aggregate的固定规则表述清楚。
⭐️模式:概念轮廓Conceptual Coutour
把设计元素分解为内聚的单元,在连续的重构过程中观察变与不变的规律,寻找能够解释这些变化模式的底层规律,就可以得到概念轮廓。
⭐️模式:独立类Standalone Class
互相依赖使得模型和设计难以理解、测试和维护。
尽力把最复杂的计算提取到独立的类中。
⭐️模式:闭合操作Closure of Operation
让操作的返回类型与其参数类型相同。
函数式编程中的map、filter都是闭合操作。
声明式设计
❓如何实现声明式设计?
- 反射
- 代码生成
❓声明式设计的局限性?
- 声明式语言表达能力的局限
- 自动生成代码与手写代码并存,破坏了迭代循环
第十一章 应用分析模式
分析模式专注于一些最关键和最艰难的决策,并阐明了各种替代和选择方案。它们提前预测了一些后期结果,而如果单靠我们自己去发现这些结果,可能会付出高昂的代价。
第十二章 将设计模式应用于模型
⭐️模式:策略Strategy
把过程中的易变部分提取到模型的一个单独的“策略”对象中。
⭐️模式:组合Composite
第十三章 通过重构得到更深层的理解
有三件事是必须要关注的:
- 以邻域为本
- 用一种不同的方式来看待事物
- 始终坚持与领域专家对话
第十四章 保持模型的完整性
模型最基本的要求是应该保持内部一致:
- 术语总具有相同的意义
- 不包含互相矛盾的规则
如果两个团队使用了不同的模型,而并没有意识到这一点,他们的代码被组合到一起时,往往就会发生问题。
大型系统领域模型的完全统一既不可行,也不划算。
需要预先决定什么应该统一,什么不能统一。
⭐️模式:限界上下文Bounded Context
限界上下文定义了每个模型的应用范围。
Bounded Context不是Module。它们是具有不同动机的不同模式。Bounded Context往往借助Module来实现,但有时人们也会在一个Context内部划分出多个Module(这实际上可能导致模型分裂)。
⭐️模式:持续集成Continuous Integration
持续集成可以有两个级别的操作:
- 模型概念的集成
- 实现的集成
⭐️模式:上下文图Context Map
描述模型之间的联系点,明确所有通信需要的转换,并突出任何共享的内容。
对各个Bounded Context的联系点的测试特别重要。
——最好由负责不同Bounded Context的团队合作开发。
⭐️模式:共享内核Shared Kernel
共享内核实际上就是领域层的基础设施共享。通过把领域模型中不同团队都统一共享的子集提取出来作为一个独立的模块,可以以较小的代价获得较大的收益。
⭐️模式:客户/供应商Customer/Supplier
下游团队相当于上游团队的客户。
要实现好的客户/供应商模式,对两个团队领导者之间的配合有较高的要求。
⭐️模式:跟随者Conformist
如果上游设计的质量不是很差,而且风格也能兼容的话,那么最好不要再开发一个独立的模型。
如果不愿意兼容框架的风格和模型,那么为什么要选择使用这个框架呢?
与C/S模式相比,跟随者模式适用于上游团队没有意愿,或者客观上不可能适应下游团队的需求的情形。
⭐️模式:隔离层Anticorruption Layer
❓如果要与一个规模庞大的遗留系统进行集成(这时根本不存在一个“上游团队”,无法使用C/S模式;遗留系统往往存在大量问题,也难以使用跟随者模式),要怎么做?
创建一个隔离层,以便根据客户自己的领域模型来为客户提供相关功能。
一种可行的组织方式是结合Facade、Adapter和转换器。
- Facade属于另一个系统的Bounded Context。它帮助我们简化对所需特性的访问,并隐藏其他特性。
- Adapter用于包装对象,使其符合另一个系统的要求。
- 转换器服务于所属的Adapter,实际进行对象或数据的转换。
⭐️模式:各行其道Separate Way
集成未必是必要的。
⭐️模式:开放主机服务Open Host Service
定义一个协议,把你的子系统作为一组Service供其他系统访问。
⭐️模式:定义语言Published Language
定义一个专用的领域语言,来实现不同模型之间的数据交换。
如何决定Bounded Context的大小?
选择较大的Bounded Context:
- 用户任务之间的流动更加顺畅
- 一个内聚模型比起两个模型+映射要更容易理解
- 两个模型间的转换可能很困难
- 共享语言,沟通更清楚
选择较小的Bounded Context:
- 减少沟通开销(因为团队规模小了)
- 便于持续集成
- 对模型抽象层级(进而对人员建模技巧)的要求低
- 可以满足一些特殊需求
Context的迭代
- Separate Way -> Shared Kernel
- Shared Kernel -> Continuous Integration
- Anticorruption Layer -> 不再需要
- Open Host Service -> Published Language
第十五章 精炼
精炼是把一堆混杂在一起的组件分开的过程。
⭐️模式:核心领域Core Domain
⭐️模式:通用子领域Generic Subdomain
识别出那些与项目意图无关的内聚子领域。把这些子领域的通用模型提取出来,并放到单独的Module中。任何专有的东西都不应放在这些模块中。
❓如何进行Generic Subdomain的开发?
- 购买解决方案
- 使用开源方案
- 外包
- 自研
对于通用子领域而言,寻求外部的现成方案,往往是一个更好的选择。因为在这上面投入过多的精力,会分散对于核心领域的注意力,同时消耗过多的资源。
⭐️模式:领域愿景说明Domain Vision Statement
DVS面向的是开发者,而不是软件的使用者。务必注意这一点。
⭐️模式:突出核心Highlighted Core
编写一个非常简短的文档(3~7页),用于描述Core Domain以及Core元素之间的主要交互过程。
⭐️模式:内聚机制Cohesive Mechanism
把概念上的内聚机制分离到一个独立的轻量级框架中,用一个释义命名接口来暴露框架的功能。领域中的其他元素就可以只关心如何表达问题,而不需要关心具体实现。
❓Cohesive Mechanism与Generic Subdomain的区别?
- Generic Subdomain描述的是领域的一个方面,只是相对没那么重要
- Cohesive Mechanism并不表示领域,而是用来解决描述性模型所提出来的一些复杂的计算问题
⭐️模式:分离核心Segregated Core
⭐️模式:抽象核心Abstract Core
第十六章 大型结构
⭐️模式:演变结构Evolving Order
⭐️模式:系统隐喻System Metaphor
⭐️模式:职责分层Responsibility Layer
- 严格分层:上层只能访问紧邻的下层
- 松散分层:上层可以访问所有下层
⭐️模式:知识级别Knowledge Level
⭐️模式:可插拔组件框架Pluggable Component Framework
第十七章 领域驱动设计的综合运用
制定战略设计决策的6个要点
- 决策必须传达到整个团队
- 决策过程必须收集反馈意见
- 计划必须允许演变
- 架构团队不必把所有最好、最聪明的人员都吸收进来
- 战略设计需要遵守简约和谦逊的原则
- 对象的职责要专一,而开发人员应该是多面手