随着微服务架构的普及,领域驱动设计(DDD)又重回软件设计战场。虽然团队内不少项目已经开始尝试,使用DDD指导项目的设计与开发,但还是有不少同学对DDD缺乏基础了解。因此,本文结合书本的定义及个人理解,对DDD中关键概念进行梳理,避免沟通时的歧义。毕竟DDD提倡使用通用语言,业务层面应该使用通用语言,技术层面也应该统一术语。
主要概念
- 通用语言(Ubiquitous Language)
围绕领域模型建立的一种语言,团队所有成员都使用这种语言把领域的所有活动与软件联系起来。
通用语言是事件风暴中产生的,达成共识的、能够准确描述业务涵义和规则的语言。《领域驱动设计》一书对通用语言的重要性进行了较大篇幅的强调,在整个软件生命周期中,无论是口头交流还是书面表达都应该使用通用语言。
个人认为,对团队新成员进行通用语言培训是有必要的。
- 实体(Entity)
一种对象,它不是由属性来定义的,而是通过一连串的连续事件和标识定义的。
实体和值对象都是领域知识中的名词,建模时,常常容易混淆。其关键判断依据是,实体是有标识的,要么是全局唯一标识,要么是聚合内部的本地标识。例如,订单ID是全局唯一标识,而订单项ID只需要在订单ID下唯一即可。
- 值对象(Value Object)
一种描述了某种特性或属性,但没有概念标识的对象。
值对象是由其关键属性决定的,只要关键属性相同,就表示对象相同。值对象应保持其不变性,变更应整体替换。
在实现时,可能表现为在上下文A中为实体,在上下文B中为值对象。例如,“地址”,在订单上下文,它是值对象。但在地址维护子系统,它是实体。修改订单地址,实际上是通过重新选择地址对象,以json字符串等的方式存储于订单实体。地址系统的信息变更,并不会影响到已有订单。
- 服务(Service)
一种作为接口提供的操作,它在模型中是独立的,没有封装的状态。
服务是无状态的,客户使用它时,不需要关心它的历史。服务在系统运行中通常以单一实例存在,它包含实现了各种业务逻辑的方法。DDD中的服务包括应用层服务和领域层服务。实践中,我们使用Facade命名应用层,它通过封装领域层服务,对外提供接口级别的服务。
- 仓储(Repository)
一种把存储、检索和搜索行为封装起来的机制。
可以把它看做一个特殊的服务,它专门提供存储相关接口,上层访问与底层实际存储无关。但作为开发人员,仍需了解Repository各接口的实现细节,避免误用导致的性能等问题。
Repository通常可以支持用户定制化的接口(用户指定操作字段等,需要警惕SQL注入问题)和一些常用接口。例如,exec接口,允许用户指定数据表、字段,甚至操作。
- 固定规则
一种为某些设计元素做出的断言,除了一些特殊的临时情况(例如,方法执行中间,或者尚未提交的数据库事务的中间)意外,它必须一直保持为真。
包括数据一致性规则、必填字段等。例如,订单总价=所有订单项价格 - 各种折扣。
- 聚合(Aggregate)
聚合是一组相关对象的集合,我们把聚合作为数据修改的单元。
聚合根:聚合中一个代表聚合核心概念的实体。每个聚合有且仅有一个聚合根。
外部只能通过引用聚合根,查询、修改聚合内部数据。聚合内应保持固定规则,即符合数据一致性。
聚合是领域模型的第一层边界,通过组合、拆分聚合,构成微服务单元。
聚合间的调用逻辑应通过应用层服务RPC实现,以确保聚合间的低耦合,便于微服务的重组和拆分。
* 聚合的识别是实际操作中的难点,可以采用自下而上的方法,先将每个实体作为一个聚合,不断组合出合适聚合。
- 限界上下文(Bounded Context)
限界上下文是规定了领域边界的语义环境,领域内使用通用语言。
它是领域模型的第二层边界,一个限界上下文应包含应用层、领域层、数据层。在软件设计初期,不同限界上下文可能会共享数据库,以降低成本,但仍需要注意分库,或者分表,并避免联合查询,及表间外键的级联更新、删除。
限界上下文可以作为微服务的边界。
- 工厂(Factory)
一种封装机制,把复杂的创建逻辑封装起来,并为客户抽象出所创建的对象的类型。
包括创建工厂和重建工厂(来自存储或网络的数据重建)。
一般对象(包括实体和值对象)的创建有两种方式,简单的对象创建可以由构造函数(Go中没有静态方法,可以用函数)实现;复杂的对象(通常是聚合根)的创建,可以由工厂方法实现。
工厂创建出来的对象必须满足固定规则。固定规则的逻辑根据是否在全生命周期使用,可放置在实体,若仅在创建时校验,可放置在工厂。
实体工厂创建出来的对象仅包含必填字段即可。值对象工厂创建出来的值对象需包含全部字段,因为值对象是不可变的。
- 领域事件(Domain Event)
领域事件通常是领域知识中的,“当xxx完成,则执行xxx”,当领域事件发生,将进一步触发业务操作。
领域事件是微服务解耦的关键。对非实时一致性场景,事件通过发布、订阅方式实现,达到最终一致性。
领域事件避免了传统RPC的分布式事务难题,减轻了系统实时访问的压力。
* 为了确保可靠性,应根据需要对事件进行持久化。
(个人理解和以往的“事件”没有太大区别,重点是识别出领域事件)
- 贫血模型
领域对象只有属性及其getter/setter方法的纯数据类,业务逻辑通过服务实现。传统的三层架构中的数据类就是这种模式。
- 充血模型
单个、自身的业务逻辑属于领域对象的行为。
涉及多个领域对象交互的部分属于领域层的服务,其他领域无关的、跨聚合的逻辑属于应用层服务。
参考文献
《领域驱动设计——软件核心复杂性应对之道》
《实现领域驱动设计》