DDD Super Quickly
最近读完《DDD Quickly》,是InfoQ推出的免费书。看完之后受益匪浅,总结一下,方便复习。
传统企业的业务是针对的是真实世界,概念复杂,业务繁琐。对于这样的业务,如果没有一个整体的思想来主导软件开发,不免会因为功能的不断堆砌导致系统逐渐臃肿,混乱。随着项目不断推进,开发效率开始下降,甚至维护也变得很困难。
领域驱动设计(Domain Driven Design),是一种企业级软件系统架构的设计模式,就是这样一个“整体思想”,能够很好的管理项目。
业务-项目的起点
软件系统的用来服务业务的,好的软件系统必须能够准确的切合业务需求。因此,开发软件系统的起点一定是对业务知识的了解。在DDD里,“领域”指的就是业务。
通用语言-交流的基础
企业业务代表通常精通业务知识,但未必懂得写代码。开发人员精通算法、软件性能调优,但对业务都是一知半解。因此双方在梳理软件系统需求的时候,会从各自的角度考虑问题。
在了解业务的过程,需要对业务流程构建模型。这个模型不是一蹴而就的,是在和业务代表不断沟通的过程中逐渐完善的。开发人员需要从业务描述中,捕捉关键字,不断完善业务模型。然后将业务模型映射为代码里的模块、类型、属性、方法等待,将业务模型之间的关系映射为代码里的继承、调用等等。这样将业务描述为代码里的概念。
业务模型不是UML类图。对于企业复杂的流程,还没有设计到类这一层的时候,就已经相当复杂。UML多半无法承担这样的责任。
模型驱动设计
随着业务模型逐渐完善时,软件开发工作也要开始起步。这里就存在一个问题:如何将业务模型转化为软件代码。
DDD构建了一种分层架构,层内高内聚、层间低耦合:
分层 | 解释 |
---|---|
UI层 | 负责向用户展现信息以及解释用户命令 |
应用层 | 很薄的一层,用来协调应用的活动。它不包含业务逻辑。它也不保留业务对象的状态,但它保留有应用任务的进度状态 |
领域层 | 本层包含关于领域的信息。这是业务软件的核心所在。在这里保留业务对象的状态。对业务对象和它们状态的持久化被委托给了基础设施层 |
基础设施层 | 本层作为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支持库等作用 |
每一层只依赖于同一层或者更低层,而不会向上依赖。
DDD为领域层和基础设施层构建了若干概念,便于代码的组织。
实体 Entity
实体,代表一类对象,始终拥有唯一的标识符,即使是软件系统不断更迭也不会发生变化。比如银行系统中一次汇款业务,拥有唯一的单号,而且会在数据系统中持续保存很多年。
值对象 Value Object
值对象,也是对象,但是未必有唯一标识符。因为调用的方法并不关心是否唯一,只关心对象里的属性值。
根据业务场景,如果可能,尽量将值对象设置为不可变。特别是,该值对象存在共享的可能的时候。这里主要是基于业务规模大,开发人员没有精力就太过细节的地方进行沟通的情况来考虑,不可变对象的发布更为安全。
DDD规定了若干值对象类型(可能不全,_):
- VO,视图对象,与UI层交互的传值;
- DTO,数据传输对象,也就是方法间的传值;
- PO,持久化对象,对应于数据库存储的对象;
- POJO,简单对象,是指能够在多个场景中持续转换身份的对象。
服务 Service
DDD批判一种数据模型,叫做“贫血模型”。意思是,这种模型只能表达属性,缺乏内置的必要方法,严重依赖第三方方法的调用。典型如Java Bean,只有构造器、setter、getter、toString这类方法。
这种数据模型,实际上并不符合“对象”这样的概念。因为对象本身应该是可以有自身行为的。依赖第三方方法操纵的模式,更像是面向过程的命令式调用。有自身行为的对象,就是DDD鼓励的、与之对应的“充血模型”。
但是充血模型,只有自身行为。有一类流程是将各种对象组织在一起。这种流程无法用数据模型的对象方法来实现。因此,需要另一种现实形式——服务。
服务有三个特征:
- 服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。
- 被执行的操作涉及到领域中的其他的对象。
- 操作是无状态的。
模块 Module
系统增大以后,需要对内部进行一些划分,方便开发管理工作的推进。模块之间低耦合,模块内部高内聚。内聚类型有通信性内聚、功能性内聚。前者是指模块组件操作相同的数据,后者指的是模块组件协同工作完成同样的任务。后者通常更合适。
聚合 Aggregate
聚合是对实体、值对象的边界的划分。
将一组关联的对象组织在一起,他们都有同样的根,聚合根(Aggregate Root)。外部方法只能持有聚合根对象,并间接引用实体或值对象。
对属性值的变更应该交给聚合根来实现。
工厂 Factory
工程用来创建聚合、实体。因为这样的值往往很大,构造很麻烦,而调用方法又未必对构造的细节感兴趣。使用工厂创建,有助于代码解耦。
实体工厂和值对象工厂未必一样。因为实体有唯一标识符,值对象又大多是不可变的。
资源库 Repository
资源库封装了缓存、数据库等基础设施的实现细节,对外提供一组获取或者操纵实体、值对象的接口。
资源库负责封装筛选条件。
资源库,像工厂一样提供对象。但是前者提供的是已经存在的持久化对象,后者创造的是新的对象。
持续重构
最简单的建模应该是阅读业务规范,然后将名词转化为类,动词转化为方法。随着,对业务的理解深入,模型随之需要添加新的概念。就会产生重构的需要。重构应该是小幅度、高频率的,避免破环已有功能或者引入bug。
新引入的概念,应该是之前没有发掘出来的“隐含的概念”。发掘“隐含的概念”需要多和业务代表沟通,要观察他们之间表述的差异,这里往往会有新的发现。也可以阅读业务领域的文献,将其内容和已有的模型对比,找出差异。
凸显这些隐含的概念,往往会用到下列方式:
- 约束,表达不变量的方式。例如:容器的容量上限。
- 过程,表示计算流程,可以是服务或者策略对象。
- 规约,用来测试对象是否满足条件。
保持模型的一致性
大型项目往往是多个团队协同开发。这就造成每个团队只了解自己的模块,对其他团队的模块知之甚少。如果是相互独立的模块还好,但遇到模块之间存在交集的时候,如何控制模块保持与模型相一致就成为了问题。
DDD在这里选择从“有界上下文”入手。
有界上下文 Bounded Context
上下文和模型一一对应。大模型拆分为多个小模型的时候,就需要对上下文进行定界,定出模型范围。
上下文的定界依据包括:团队的组织结构;应用的特定部分中的惯例;物理表现,例如:代码库、数据库Schema等等。
需要注意的是,上下文包含了当前的模块和演化轨迹。
具体到业务领域,DDD推荐按照业务划分上下文。
持续集成 Continuous Integration
上下文的定界不会是一次性就正确的。
随着对业务深入了解,上下文会重新定界,模型需要修正,代码也会发生变更。为了避免出现重大bug,推荐对代码执行每天合入,构建,并执行自动化测试,以尽早发现问题,解决问题。
上下文映射 Context Map
上下文映射描述了不同有界上下文之间的对应关系。
接下来,会讲述几种映射关系:
- 共享内核(Shared Kernel)和客户-供应商(Customer-Supplier)是处理上下文之间的高级交互的模式。
- 隔离通道(Separate Way)是在我们想让上下文高度独立和独立进化时要用到的模式。
- 还有两个模式用来处理系统与一个遗留系统或一个外部系统之间的交互,它们是开放主机服务(Open Host Service)和防崩溃层(Anticorruption Layer)
共享内核 Shared Kernel
这是多个模块存在交集。对于交集的演化,双方需要密切的合作,及早测试。
各方团队都需要关注交集演化对自己模块带来的影响。
客户-供应商 Customer-Supplier
同样是多个模块存在交集,但是不像双方对交集都有演进需要,有些时候只有一方有演进的需要,但可以修改代码的团队却是另一方。
这种情况下,就需要有演进需求的一方,做为客户向另一方提出演进需求,并同步修改自身的代码。客户方,需要提供接口规范,并准备好测试验收计划。
顺从者 Conformist
在客户-供应商场景下,一方依赖于另一方。如果被依赖着没有动力去完成对方的需求时,需求方就得反过来顺从已有的模型。(黑人问号脸???)
防崩溃层 Anti-Corruption Layer
当现在的模型依赖于陈旧的已有系统时,如果对旧系统了解不深,那么往往会出现各种各样的问题。为了防止异常的蔓延,需要在现在模型和已有系统之间再加一层,叫“防崩溃层”。
隔离通道 Separate Way
当子模块不需要交互的时候,可以采用完全独立的上下文,分别独自开发。
需要注意的是,隔离开发的模块随着不断演进,很难再集成在一起。
开放主机服务 Open Host Service
就是子系统之间做集成的时候,需要提供一组转换接口,方便不同的子系统对集成进入的系统的调用。
提炼-专注于核心领域
有时候,业务太过庞大的时候,其领域模型经过多次重构也会很大。这时候,就需要对领域模型进行划分,梳理出核心域(Core Domain)和普通子域(Generic Subdomain)。
把对核心域的开发迭代工作放在更高的优先级。甚至可以把普通子域的工作外包出去,或者使用已有的解决方案、模型来加速完成过。自己完成普通子域的工作可以有最好的集成效果,但会带来成本的上升。