随着进入战术设计,组织的工作重心会转向领域模型的设计与实现。战术设计提供了一组构建块,用于把某个限界上下文(bounded context)的领域模型实现为代码。这类设计有助于建模者处理复杂业务逻辑场景。它尤其适合用于实现核心域的限界上下文。本章将讨论战术设计的这些构建块。
领域驱动模型的构建块
在战术设计的语境中,对领域模型的工作重点落在它的源代码模型之上。常见做法是实现一种领域的对象模型,同时将行为与数据结合在其中。不过,同样也可以接受采用函数式编程方法,将数据与基于函数的行为分离开来。本书采用的是面向对象的领域模型。
战术设计提供了一组构建块,用于实现某个限界上下文的领域模型。一个领域模型可以通过以下元素来表达:
- 实体(Entity) :实体表示一个具有自身生命周期的对象,并且能够通过改变其值,随着时间推移而改变其状态。实体通过其**唯一标识符(id)**来识别。
- 值对象(Value object) :值对象由不可变值构成,并且不声明自身唯一身份,尽管一个值对象也可以被设计为包含另一种类型(例如实体)的唯一身份。作为一个整体的不可变值,定义了这个值对象。改变这些值会创建一个新的值对象实例,而这个新实例通常会被用来替换旧实例。
- 聚合(Aggregate) :聚合表示一个由紧密相关对象组成的图结构,它由一个或多个实体,以及可选的一个或多个值对象构成。一个聚合实例反映的是领域模型某一部分的状态。它划定了一个清晰的一致性边界,并且可以充当一个事务边界。那个组合了聚合中其他所有部分的父实体,被称为聚合根(aggregate root) 。聚合根定义了聚合的公共接口。
- 领域服务(Domain service) :领域服务执行的是特定于领域的业务逻辑,而这些逻辑并不适合由某个单独实体或值对象来负责,而是可能在其操作过程中使用一个或多个领域对象。
- 领域事件(Domain event) :领域事件表示领域模型中某个重要的事情已经发生。通常,聚合会是领域事件的来源。一个或多个限界上下文——包括产生该事件的那个限界上下文本身——都可能关心某个给定事件是否已经发生。在这种情况下,感兴趣的一方必须安排好对其关心的每个事件进行监听与消费。
一个限界上下文不仅仅由其领域模型构成。它还包括用于处理模型完整生命周期的机制,其中包括持久化,以及跨边界集成时所需的机制,包括转换。
Ports and Adapters 架构与战术设计
Ports and Adapters 架构(见图 4.1)——也被称为六边形架构(hexagonal architecture)——由 Alistair Cockburn 提出 [4.1]。它通过在系统边界处引入 ports 和 adapters,将关注点分离开来,并创建松耦合的软件组件,从而帮助把应用的业务逻辑与其外部软件环境隔离开来。Ports and Adapters 架构与战术设计非常契合,尽管它并不是唯一适合的架构模式。
图 4.1 Ports and Adapters 架构
Port 是定义应用边界的接口;adapter 则将参与者(actor)与 port 连接起来。所谓参与者,是指任何与应用发起对话的一方,或者由应用向其发起对话的一方——例如 Web UI、数据库、消息代理等等。
Ports and Adapters 架构把软件组件划分为外部部分与内部部分,其中 ports 和 adapters 负责把外部的参与者连接到内部部分(驱动型或主适配器,driving or primary),以及把内部部分连接到外部(被驱动型或次适配器,driven or secondary)。这种模式将业务逻辑实现与其外部环境组件解耦。当外部组件发生变化时,内部的业务逻辑本身保持不变;只需要调整外层的 adapters 即可。此外,Ports and Adapters 中这种松耦合的软件组件,也便于自动化测试,因为这些组件可以很容易地被单独隔离测试。Ports and Adapters 的代价在于:为了确保松耦合,它在实现结构上会增加复杂性,表现为需要大量接口与适配器,以及各种转换机制。它非常适合那些复杂、持续演进、且高不确定性并要求灵活性的领域;但对于简单应用或短生命周期原型而言,这种额外复杂性未必合理。
Ports and Adapters 可以应用到单个限界上下文上。正如前面提到的,它与战术设计模式结合得很好,但并不限于这些模式。在这种架构的中心,是带有业务逻辑或业务驱动用例的应用本身。Ports and Adapters 架构并不规定内部应用必须如何实现。然而,对于复杂领域(例如核心域)而言,这个应用可以被实现为由战术设计构建块构成的领域模型。
领域模型聚焦于用前述构建块模式所表达的业务逻辑,并且不包含技术性关注点,例如持久化。为了体现领域模型的完整生命周期,这些业务逻辑需要被持久化到某种存储机制中。尽管持久化本身不是领域模型的一部分,但领域驱动设计(DDD)为此提供了一个构建块模式:Repository(仓储) 。Repository 帮助把实体或聚合持久化到其底层存储机制(例如数据库)中,并从中取回。Repository 接口是一个很好的被驱动 port 候选项,而它的实现则是一个很好的被驱动 adapter 候选项;此时数据库就可以被视为一个外部的被驱动参与者,应用通过它与外部环境发生连接。
像事务管理、安全处理以及用例与任务编排这样的横切性与技术性方面,可以放在应用服务(application service)中。应用服务并不是领域模型的正式组成部分,也不属于 DDD 的那些构建块模式。相反,应用服务通常扮演的是领域模型的一个直接客户端:它调用领域模型的方法来执行特定于业务的行为。这个服务会使用 repository,从底层存储机制中获取领域对象,或者将领域对象保存回去。应用服务的接口是一个很好的驱动型 port 候选项,它可以被一个驱动型 adapter 使用——例如,一个暴露给外部客户端的 REST endpoint,就可以作为驱动型参与者与应用进行连接。
应用服务不应包含业务逻辑。如果某项业务逻辑操作超出了单个领域模型内部那种更细粒度、自包含的行为范围,并且需要同时涉及多个领域对象,那么这时就轮到领域服务发挥作用了。在《Implementing Domain-Driven Design》[4.2] 一书中,Vaughn Vernon 对战术设计的应用提供了更深入的细节与示例。
下面的讨论会通过一个简化示例,展示如何借助战术设计构建块与 Ports and Adapters 架构来实现 CfP Management 限界上下文,如图 4.2 所示。
图 4.2 Ports and Adapters 架构与战术设计构建块的结合
CfP Management 限界上下文通过 HTTP 提供一个公开的 RESTful API。CfpRestAPI 会向外部客户端暴露这些 REST 端点,如代码清单 4.1 所示。下面的这些简化代码示例使用 Java 编写,并采用 Spring Boot 框架。
代码清单 4.1 通过 CfpRestAPI(驱动型 adapter)向外部客户端暴露 REST 端点
Application Service
CfpRestAPI(驱动型 adapter)会把传入请求委派给 CfpManagement 接口(驱动型 port),而这个接口由 CfpApplicationService 来实现。如代码清单 4.2 所示,CfpApplicationService 与 Cfp 领域模型以及 CfpRepository 发生交互,并处理安全与事务。
代码清单 4.2 CfpApplicationService 作为驱动型 port 的实现,与领域模型交互,并协调持久化、安全与事务
Aggregate
Cfp 领域模型被实现为一个聚合,它只反映与 CfP 相关的业务逻辑以及该领域模型的状态。这个聚合——如代码清单 4.3 所示——由一个 Cfp 实体以及若干不可变值对象组成,如图 4.2 所示。Cfp 实体代表聚合根,并提供聚合的公共接口。
代码清单 4.3 Cfp 聚合封装了围绕 Call for Paper 生命周期的业务逻辑,其中 Cfp 是聚合根,并由值对象组成
Cfp 领域模型中每一个会改变状态的操作(define、open、reschedule、close),都会在对应语境下应用相关业务规则。这确保了:只要某个客户端(例如 CfpApplicationService)拿到了该领域模型的一个实例,Cfp 领域模型就始终处于一个有效状态。
Cfp 聚合的状态会被持久化到数据库中。为了把最新状态保存到数据库并从数据库中取回,CfpApplicationService 会调用 CfpRepository,并使用相应的 save 和 findById 方法。在这个例子中,数据库选择的是 MongoDB。为了连接 MongoDB 数据库,MongoDBCfpRepository 作为一个被驱动 adapter,实现了 CfpRepository 这个 port。该 adapter 使用一个 MongoDB 专用客户端与 MongoDB 数据库通信。把领域模型转换成与 MongoDB 相关的数据传输对象(DTO),以及反向从 DTO 转回领域模型,这种转换都发生在 Ports and Adapters 架构的外层部分;这种转换既可以由 adapter 本身提供,也可以由一个独立的类来提供。把领域模型与 DTO 之间的转换放到应用之外,能够让内部部分摆脱基础设施相关因素的影响,并使二者能够独立演进。代码清单 4.4 展示了 MongoDBRepository 的一个示例代码。
代码清单 4.4 MongoDBCfpRepository 作为一个被驱动 adapter,将领域模型状态持久化到 MongoDB 数据库并从中取回
第 3 章“使用战略性领域驱动设计设计解决方案空间”中的“上下文映射”一节,已经描述了在与其他限界上下文集成时,涉及转换机制的上下文映射模式,例如防腐层或带有发布语言的开放主机服务。在 Ports and Adapters 中,当转换机制是某个限界上下文自身的一部分时,它们可以被实现于外层部分,从而使相关领域模型能够独立于其集成协议而演进。
将 DDD 与 Ports and Adapters 架构结合起来,有助于为复杂领域构建自适应、可演进的限界上下文——在这种方式下,系统的外部部分与外层部分都可以被替换,而不会影响内部领域模型中的业务逻辑。与此同时,领域模型又能够独立于其环境中的集成协议而演进。
小结
本章讨论了战术性领域驱动设计。战术设计的重点,是提供构建块以将领域模型实现为代码——如图 4.3 所示。在这种设计中,Ports and Adapters 架构通过 ports 和 adapters 来创建限界上下文中的松耦合软件组件,并使其能够方便地与其环境集成。
图 4.3 战术设计总结
Ports and Adapters 架构与战术设计的结合,会导向一个自适应、可演进的系统:它把领域逻辑保留在系统的最内核处,并使其与这个核心之外发生的变化解耦。内部部分中的领域模型与外层部分中的环境,可以彼此独立地演进。本章讨论的是底层软件设计方面的内容,并通过会议活动策划示例中的 CfP Management 限界上下文,说明了如何使用战术设计的构建块来实现领域模型。
下一章将从软件设计切换到组织设计。