首页
Domain-Driven Design 领域驱动设计
总结自《阿里技术专家详解DDD系列》
DDD架构DEMO
一、Domain Primitive(DP)
一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object
就好像 Integer、String 是所有编程语言的 Primitive 一样,在 DDD 里, DP 可以说是一切模型、方法、架构的基础,而就像 Integer、String 一样, DP 又是无所不在的。
使用:
1.将隐性的概念显性化
- 生成一个 Type(数据类型)去显性的表示一个概念。
- 将这个概念相关的逻辑完整的收集到一个 Class(类)里。
2.将隐性的上下文显性化
- 拓展一个简单概念的上下文,将多个概念组合成一个独立的完整概念。
3.封装多对象行为
- 一个概念涉及多个对象之间的复杂业务逻辑,将其封装为 DP,简化原始代码。
定义:
- DP 是一个传统意义上的 Value Object,拥有 Immutable 的特性 。
- DP 是一个完整的概念整体,拥有精准定义。
- DP 使用业务域中的原生语言。
- DP 可以是业务域的最小组成部分、也可以构建复杂组合。
常见使用场景:
- 有格式限制的字符串:比如 Name,PhoneNumber,OrderNumber,ZipCode, Address 等。
- 有限制的整数:比如 OrderId(>0),Percentage(0-100%),Quantity(>=0) 等。
- 浮点数:一般用到的 Double 或 BigDecimal 都是有业务含义的,比如 Temperature、Money、Amount、ExchangeRate、Rating 等。
- 复杂的数据结构:比如 Map 等,尽量能把 Map 的所有操作包装掉,仅暴露必要行为。
二、应用架构
传统的三层分层结构:UI 层、业务层、和基础设施层。上层对于下层有直接的依 赖关系,导致耦合度过高。
传统架构违背的原则:单一性原则(对象或类应该只有一个变更的原因)、依赖反转原则(代码中依赖抽象,而不是具体的实现)、开放封闭原则(开放扩展,封闭修改)
几个概念:
- Entity类:是拥有 ID 基于领域逻辑的实体类,除了拥有数据之外,同时拥有行为,其参数应该尽可能的由 DP 代替。
- Repository接口:是 Entity 对象读取储存的抽象,在接口层面做统一,不关注底层实现。
- Domain Service类:是一个包含多个 Entity 对象之间完整行为的类。
- Application Service类:传统架构中的Service类,不再包括任何计算逻辑,仅仅作为组件编排。
领域模型
- 失血模型:仅仅包含领域中域的定义和getter/setter方法,业务逻辑全部放到领域服务层中,是对传统三层架构中数据模型的简单封装;
- 贫血模型:包含了少部分业务逻辑,主要是使用有意义的行为方法替代setter方法,但不包含依赖持久层的业务逻辑,这部分逻辑放在领域服务层中;
- 充血模型:包含了所有的业务逻辑,领域服务层的功能被最大限度的弱化,只是通过依赖反转的方式为领域提供持久层的操作实现;
- 胀血模型:完全摒弃掉领域服务层,在模型中合并了持久层。
改造方法:
- 用 Domain Primitive 封装跟实体无关的无状态计算逻辑。
- 用 Entity 封装单对象的有状态的行为,包括业务校验等。
- 用 Domain Service 封装多对象逻辑。
改造后的DDD架构:
- 最底层不再是数据库,而是由 Entity、Domain Primitive 和 Domain Service 组合成的 Domain Layer(领域层)。这些对象不依赖任何外部服务和框架,是纯内存中的数据和操作。领域层没有任何外部依赖关系。Domain 层是核心业务逻辑,属于经常被修改的地方。
- 再其次的是由 Application Service、Repository、ACL 等组合成的 Application Layer(应用层),核心是负责组件编排的 Application Service。 应用层依赖领域层,但不依赖具体实现。Application 层属于Use Case (业务用例),是描述比较大方向的需求, 接口相对稳定,一般不会频繁变更。
- 最后是 ACL,Repository 等的具体实现,这些实现通常依赖外部具体的技术实现和框架,所以统称为 Infrastructure Layer(基础设施层)。Web 框架里的对象如 Controller 之类的通常也属于基础设施层。Infrastructure 层属于最低频变更的,一般这个层的模块只有在外部依赖变更了之后才会跟着升级。
Entity、Data Object (DO) 和 Data Transfer Object (DTO):
Data Object (数据对象,同 Persistent Object 、PO):
实际上是我们在日常工作中最常见的数据模型。在 DDD 的规范里,DO 应该仅仅作为数据库物理表格的映射,不能参与到业务逻辑中。为了简单明了,DO 的字段类型和名称应该和数据库物理表格的字段类型和名一一对应。
Entity(实体对象):
是我们正常业务应该使用的业务模型,它的字段和方法应该和业务语言保持一致,和持久化方式无关。也就是说,Entity 的生命周期应该仅存在于内存中,不需要可序列化和可持久化。
DTO(传输对象):
主要作为 Application 层的入参和出参,DTO 的价值在于适配不同的业务场景的入参和出参,避免让业务对象变成一个万能大对象。
DTO Assembler:
在 Application 层,Entity 到 DTO 之间的转化器。
Data Converter:
在 Infrastructure 层,Entity 到 DO 之间的转化器。
三、架构设计要点
Aggregate(聚合根):
复杂一点的领域里,通常主实体会包含子实体,如主子订单模型、商品/SKU 模型、跨子订单优惠、跨店优惠模型等,这时候主实体就需要起到聚合根的作用。
- 子实体不能单独存在,只能通过聚合根的方法获取到。任何外部的对象都不能直接保留子实体的引用。
- 子实体没有独立的 Repository,不可以单独保存和取出,必须要通过聚合根的 Repository 实例化。
- 子实体可以单独修改自身状态,但是多个子实体之间的状态一致性需要聚合根来保障。
实体:
- 使用含参数的构造函数、工厂模式、build模式等创建对象,尽可能的不要对外暴露 setter 方法,通过行为方法来修改内部状态。
- 不可以强依赖其他实体或领域服务,只保存外部实体的 ID,领域服务通过方法参数传入。
- 实体的任何行为只能直接影响到本实体(和其子实体)。
- 实体类同一个行为根据不同的类型有不同的实现,应该抽离到对应的 Strategy 中去实现,在领域服务里通过 StrategyManager 去获取对应的 Strategy 实现。
领域服务:
- 领域对象自身的行为通过接口实现组件化,内部实现可通过统一的System类去处理。
- 领域对象的行为只会变更自身的状态,但涉及到多个领域对象或外部依赖的一些规则,需要借助领域服务实现,领域服务应该作为方法参数引入。
- 多个领域对象状态变更的行为需要直接使用领域服务的方法实现。
领域事件:
- 领域中的事件发生后,利用事件机制去通知到领域内的其他对象,通过一个显性的事件,将事件触发和事件处理解耦,最终起到代码更清晰、扩展性更好的目的。
- 用于实现多个领域的更新,因为一致性应该是由领域层去保证,那么涉及到多个领域的变更时,不应该在应用层连续调用多个领域的save方法,而是应该发送领域事件通知到其他领域的变更(在实现上一般是同步通知,这样可以使用同一个数据库事务)。