领域驱动设计 DDD 实践指南

领域驱动设计

领域驱动设计(Domain Driven Design)是一种软件设计方法,已经存在了近20年,在过去十年中,随着微服务和相关技术的激增,受到了巨大的关注。DDD更注重软件开发的逻辑、语义和结构方面(倾向于业务端),而不是应用规范的实现方式,虽然它也提供了几种良好的设计模式。DDD是处理复杂软件的理想方法,但对于小型、独立项目来说可能会很冗长。

什么是领域驱动设计

什么是领域?领域由三部分组成:

  1. 领域里有用户,即涉众域
  2. 用户要实现某种业务价值,解决某些痛点或实现某种诉求,即问题域
  3. 面对业务价值,痛点和诉求,有对应的解决方案,这是解决方案域

什么是领域驱动设计?通俗地讲,针对特定业务,用户在面对业务问题时有对应的解决方案,这些问题与方案构成了领域知识,它包含流程、规则以及处理问题的方法,领域驱动设计就是围绕这些知识来设计系统。

以度假景点系统为例,度假景点系统所服务的用户有这几类:游客,运营,销售。解决 这几个核心问题:如何创建关联景点门票、酒店房型等商品组合,如何进行价格规则计算、什么时候进行售卖,订单如何扣减和支付。解决方案: 通过统一定义 product,里面涉及不同的景点门票和酒店房型的打包组合,同时定义不同的优惠方式,比如会员折扣,积分折扣等,构建时间售卖规则,构建订单流转流程,从订单、支付、扣减库存到发送邮件。

DDD-mapping

贫血模型

在领域模型中:只包含数据结构(字段、属性),没有任何业务逻辑的方法。叫做贫血模型。

充血模型

在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

为什么选择 DDD

基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。相对应的,基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。比如,包含各种利息计算模型、还款模型等复杂业务的金融系统。

领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。我们知道,越复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的时间和精力在前期设计上。而基于充血模型的 DDD 开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。

通过前期定义好领域知识,限定好上下文边界,从而可以形成高内聚,低耦合的业务单元,同时在同一个服务中,定义很好不同聚合的边界,使用领域模型,可以更好的在上层进行编排复用。

DDD 的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。

建立领域通用语言

为了实践不同领域之间的交互关系,我们可以看一下现实生活中KFC小程序购买套餐的场景

kfc-menu-1

kfc-menu-2

战略建模

User case

我将根据不同的角色出发,列出每个角色可能会涉及到的所有use case,然后罗列出每个use case可能会涵盖到的功能域,比如商品,订单,支付,库存等,从而识别出业务中的领域

User 🙎‍♂

选择商品加入购物车

用户可以浏览所有的商品,这里的商品可以是套餐,例如全家桶,小食拼盘等,也可以是单独种类的商品,香辣鸡翅🍗等

  • 修改购物车

当用户选择了一些商品后,可以在购物车里进行修改,选择购买的数量,或者删除某些商品

  • 商品结算

当用户添加好购物车后,选择外送或者堂食,然后进行结算

  • 下单支付
  • 用户选择优惠助手中的商品添加进购物车

用户可能领取了一些优惠套餐,然后就可以在规定时间内进行购买

  • 用户可以在优惠助手中选择满减优惠券,会员卡等进行折扣

运营人员🧑🏻‍💻

  • 配置商品

运营人员可以配置一个可售卖的商品,比如商品的基本信息,商品可供选择的最小粒度产品,售卖数量范围,售卖时间,产品的折扣信息(打折,指定价格,买一赠一,会员折扣),赠品,售卖地区

  • 配置优惠助手

将一些优惠套餐放进优惠助手中,限制使用次数,使用时间,来进行促销

  • 上架新产品
  • 配置优惠券

用户在商品结算时,可以通过使用优惠券(买一赠一,折扣)等来进行打折

  • 配置产品库存

根据每天的供货量,配置每件产品的库存

系统⚙️

  • 更新库存

当用户每次购买商品中的产品后,需要进行库存的更新,防止系统库存超卖等现象,与此同时,当用户在下单某些商品时,因为库存不足,从而不能选择某些产品进行选购

  • 在优惠助手中展示用户可使用的卡券

因为用户可能已经购买过有次数限制的优惠套餐,从而需要过滤掉用户已经使用过的卡券

  • 列举可售卖商品

根据运营人员配置的商品售卖时间,来列举所有可售卖的商品,并且显示最小可购买价格

  • 计算商品结算时的价格

上面所有use case,可以通过event storming的方式,将所有的场景列举出来

领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很适合作为这种通用语言的构造基础

使用模型作为语言的核心骨架。要求团队在进行所有的交流是都使用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。这儿需要确保团队使用的语言在所有的交流形式中看上去都是一致的。因为这个原因,这种语言被称为通用语言(Ubiquitous Language)

**在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。**也就是说,通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。通用语言可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。

union language

  • Domain Event
    • 用户选择套餐,选择优惠券和地址进行下单
    • 某种产品库存已售罄
    • 上架新产品
  • Command
    • 下单
    • 更新库存
    • 添加购物车
    • 上架新产品
  • Policy
    • 选购某些套餐,其有固定购买数量
    • 优惠券使用时间范围,每天最多使用次数
  • Aggregates
    • 产品
    • 订单
    • 优惠券
  • Views
    • 不同类别的产品
    • 已有优惠券
    • 购物车
    • 已购订单

image.png

Core Domain: 核心领域是一个组织赖以立足和区别于其他组织的根本所在。一个组织之所以能够成功,甚至能够存在,本质上依赖于其在核心领域中具备卓越的能力。正因为核心领域如此重要,它理应获得最高的优先级最多的资源投入,以及最优秀的开发人员来实现。对于小型系统来说,可能只有一个核心领域;而在大型系统中,可能存在多个核心领域。

Support Domain: 支撑子域是指那些虽然不是核心领域,但对于组织的成功依然不可或缺的业务领域。它不同于通用子域,因为它通常还需要结合组织的特定需求,进行一定程度的定制或专门化开发。在这种类型的子域中,组织可以在已有的通用解决方案基础上出发,通过调整、扩展或集成来满足自身业务的独特要求。

Generic Domain: 通用子域是指那些在组织中没有独特性、但对整体系统运作仍然是必要组成部分的领域。这类子域的业务需求通常较为标准化,没有组织特有的复杂规则或流程。因此,组织可以通过采用现成的第三方解决方案(off-the-shelf software)来节省大量的开发时间和人力成本。一个典型的例子就是用户身份管理系统(如登录、注册、权限控制),这类需求在不同组织之间大致相同,适合使用成熟的通用方案。

我们从整个小程序使用的生命周期开始先来识别所使用到的功能域:

  • 商品配置

  一个商品在可以被售卖之前需要先在运营人员手里进行配置相关的售卖时间,不同种类的产品,可购买数量,产品的折扣信息,是否可以使用会员卡等;

  • 优惠助手管理

  从小程序的界面上还可以看出,运营人员可以配置优惠助手中的优惠商品,来进行促销;

  • 上架新商品

  运营人员可以通过上架新商品,来提供在不同时段可供售卖的商品

  • 可售卖的商品和最小可购买价格

  在小程序上,最多的就是商品的展示了,那么在界面上需要显示可售卖的商品和最小可购买价格

  • 优惠券管理

  运营人员需要提供一些满减活动的优惠券,独立于商品,可以在订单进行结算时提供折扣,并且对于优惠券的使用次数和时间有一定的限制,并且需要在优惠助手进行展示的时候进行校验

  • 优惠券使用

  当用户将购物车里的商品进行结算时,可以选择自己拥有哪些优惠券,来进行折扣,那么在订单进行价格计算时需要考虑进去

  • 购物车管理

  用户可以随时修改购物车里的商品

  • 订单结算

  当用户最终选择好购物车里的内容后,就可以跳转到商品结算页面,然后选择优惠券,进行最后的订单结算

  • 订单详情

  当用户支付完成时,可以查看其购买的订单详情

  • 支付
      用户可以选择用微信支付,或者银行卡来进行支付

  当罗列了这些存在的功能后,我们其实可以进行领域的识别,从不同利益人的角度出发,可以划分出大部分不同的领域,比如从用户出发,就是最核心的围绕订单相关领域;

  从运营人员出发,就是商品配置,优惠助手管理,优惠券配置,而支付相关其实是依赖的第三方支付方式,可以作为支撑域来辅助订单领域;

  在小程序下,购物车的管理大部分是客户端来完成,目前暂时不考虑在服务中引入购物车相关概念。

  对于用户信息的展示,也需要相应的用户管理;

  对于最小粒度的产品,还需要管理其库存和价格,考虑到不同上下文可能存在的交互复杂度,这里假设我们使用的是第三方的产品管理平台来管控实时库存和价格。

做好限界上下文

在进行上下文划分之后,我们还需要进一步梳理上下文之间的关系。

康威(梅尔·康威)定律

任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。

康威定律告诉我们,系统结构应尽量的与组织结构保持一致。这里,我们认为团队结构(无论是内部组织还是团队间组织)就是组织结构,限界上下文就是系统的业务结构。因此,团队结构应该和限界上下文保持一致。

梳理清楚上下文之间的关系,从团队内部的关系来看,有如下好处:

  1. 任务更好拆分,一个开发人员可以全身心的投入到相关的一个单独的上下文中;
  2. 沟通更加顺畅,一个上下文可以明确自己对其他上下文的依赖关系,从而使得团队内开发直接更好的对接。

从团队间的关系来看,明确的上下文关系能够带来如下帮助:

  1. 每个团队在它的上下文中能够更加明确自己领域内的概念,因为上下文是领域的解系统;
  2. 对于限界上下文之间发生交互,团队与上下文的一致性,能够保证我们明确对接的团队和依赖的上下游。

在实际业务开发中,可以基于如下 3 个原则对限界上下文进行检验,并在适当时进行调整。

  • 原则 1:正交原则。包括业务能力正交、领域知识正交、领域模型正交。
  • 原则 2:单一抽象层次原则(SLAP),即永远确保在同一层次上进行抽象。
  • 原则 3:奥卡姆剃刀原则。如无必要,勿增实体。

Image.png

限界上下文之间的映射关系

  • 合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。
  • 共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
  • 客户方-供应方开发(Customer-Supplier Development):上下文之间有组织的上下游依赖。
  • 遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
  • 防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上下文交互。
  • 开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上下文进行访问。
  • 发布语言(Published Language):通常与OHS一起使用,用于定义开放主机的协议。
  • 大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
  • 另谋他路(SeparateWay):两个完全没有任何联系的上下文。

战术建模

梳理清楚上下文之间的关系后,我们需要从战术层面上剖析上下文内部的组织关系。首先看下DDD中的一些定义。

实体

  当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。

例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。

值对象

  当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。

例:比如颜色信息,我们只需要知道{"name":"黑色","css":"#000000"}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。

聚合根

  在 DDD 中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。

  领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

  聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。

  聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。

  

在回到酒店预定系统,我们先看下核心业务场景:

核心业务场景

酒店预订系统是一个典型的复杂业务领域,涉及多个业务概念和复杂的定价规则:

  1. 酒店产品管理: 房型、房间、入住规则
  2. 价格管理: 基础价格、动态定价、优惠策略
  3. 用户管理: 会员等级、地域差异、渠道偏好
  4. 营销活动: 节假日定价、限时促销、季节性调整
  5. 组合产品: 酒店+景点的混合产品

业务复杂性挑战

  • 动态定价: 价格根据日期、房型、供需关系实时变化
  • 多维度策略: 用户策略、营销策略、渠道策略的组合应用
  • 外部系统集成: 库存系统、支付系统、第三方价格数据
  • 业务规则复杂: 可用性检查、价格计算、用户权限验证
  • 演进需求: 业务需求不断变化,需要灵活的架构支持

下面基于酒店领域系统进行上下文建模

Image.jpeg

  1. 聚合根:
    • HotelOffer: 用来展示商品组合配置的生命周期
    • UserPricingStrategy:用户定价策略聚合根,管理基于用户属性的定价
    • MarketingPricingStrategy:营销定价策略聚合根,管理基于市场活动的定价
  2. 实体
    • HotelProduct: 酒店产品实体,包含房型和房间信息
    • UserLevelDiscount: 用户等级折扣实体
    • RegionPricing: 地域定价实体
    • ChannelPricing: 渠道定价实体
    • HolidayPricing:假日定价实体
    • seasonalPricings: 季节定价实体
    • FlashSaleActivities: 秒杀活动实体
  3. 值对象
    • UserContext: 用户上下文,包含用户等级、地域、渠道信息
    • DateRange: 日期范围,表示有效期或入住期间
    • PricePair: 价格对,包含基础价格和优惠价格
    • UserLevel: 用户等级枚举(青铜、白银、黄金、白金、钻石)
    • Region: 地域枚举(华北、华南、华东、华西、国际)
    • Channel: 渠道枚举(官网、移动端、OTA、线下、企业)
    • Inventory: 外部库存系统映射到商品活动上下文中的库存
  4. 领域服务
    1. ComprehensivePricingDomainService : 综合定价策略
    2. **OfferAvailableService:**是否可售卖
  5. 边界上下文
    1. 酒店预订上下文 (Hotel Booking Context)
      • 核心业务逻辑
      • 聚合根: HotelOffer, UserPricingStrategy, MarketingPricingStrategy
    2. 价格数据上下文 (Price Data Context)
      • 外部价格数据接入
      • 防腐层: PriceDataAdapter

下面我将演示基于酒店领域系统在从大泥球架构到业务不断演进下的跨聚合的领域服务编排实现:

架构迭代流程

📚 第一阶段:从大泥球到基础领域模型

问题背景

传统的大泥球架构通常表现为:

  • 所有业务逻辑混杂在一个巨大的类中
  • 数据结构和业务行为耦合
  • 外部系统依赖直接暴露在业务逻辑中
  • 难以测试、维护和扩展

大泥球代码示例(重构前)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 大泥球式的酒店价格查询服务 - 一切都混在一起
*/
import com.yonhoo.ddd.domain.model.HotelProduct;
import com.yonhoo.ddd.domain.model.PriceRule;
import com.yonhoo.ddd.domain.model.Validity;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;

public class ApplicationService {
private PriceRuleRepository priceRuleRepository;
private ProductRepository productRepository;
private ValidityRepository validityRepository;
private HotelOfferRepository hotelOfferRepository;
private PriceDataRepository priceDataRepository;


/**
* 应用层直接处理所有业务逻辑
*/
public BigDecimal calculateMinPriceV1(String offerNo, LocalDate checkInDay) {
List<PriceRule> priceRule = priceRuleRepository.queryPriceRuleByOfferNo(offerNo);
List<HotelProduct> hotelProducts = productRepository.queryHotelProductByOfferNo(offerNo);
Validity validity = validityRepository.queryValidityByOfferNo(offerNo);

if (calculateCheckInDayIsAvailable(validity, checkInDay)) {
throw new RuntimeException("CheckInDay is not available");
}

return priceRule.stream().map(priceRuleItem -> calculatePrice(priceRuleItem, hotelProducts, checkInDay))
.filter(Objects::nonNull)
.min(BigDecimal::compareTo)
.orElseThrow(() -> new RuntimeException("price is not available"));
}


public Boolean calculateCheckInDayIsAvailable(Validity validity, LocalDate checkInDay) {
// logical processing
return true;
}

public BigDecimal calculatePrice(PriceRule priceRule, List<HotelProduct> hotelProducts, LocalDate checkInDay) {
// logical processing
return BigDecimal.ONE;
}
}

大泥球架构的问题分析

1. 单一职责原则严重违背

  • 一个类承担了查询库存、所有商品组合、价格规则,价格计算等多重职责

2. 业务逻辑和技术实现强耦合

  • 业务规则硬编码在技术实现中
  • 数据库操作和业务逻辑混杂

3. 外部系统依赖直接暴露

  • 没有防腐层保护
  • 外部系统变化直接影响业务逻辑

4. 业务规则难以变更

  • 价格计算规则硬编码
  • 添加新的折扣规则需要修改核心方法

5. 测试困难

  • 方法过于庞大,依赖过多
  • 难以进行单元测试

6. 可维护性差

  • 代码结构混乱,理解困难
  • 修改一个小功能可能影响整个系统

初始重构:HotelOffer V1

识别 hotelOffer 聚合根,将offer本身的能力下沉到领域层,对外直接提供最小价格查询功能,外部不需要感知最小价格内部的业务逻辑,只需要使用这个能力进行编排就行,不同开发人员只需要聚焦在自己领域的那块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**  
* 酒店产品聚合根 - 第一版
* 从大泥球中提取出的核心业务概念
*/
import com.yonhoo.ddd.domain.model.*;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

public class HotelOffer {
String offerNo;
HotelProduct products;
List<PriceRule> priceRuleList;
Validity validity;
List<Channel> channels;

/**
* 验证指定日期是否可以入住(聚合内业务规则)
*/
public boolean isAvailableForCheckIn(LocalDate checkInDay) {
return validity.validateCheckInDayIsAvailable(checkInDay);
}

public BigDecimal calculateMinPrice(LocalDate checkInDay, Map<String, ? extends AbstractPriceData> roomPriceData) {
// 内部业务逻辑完全封装,外部无需知道PriceRule、HotelProduct等细节
return priceRuleList.stream().map(priceRule -> {
DateRange occupationDateRange = products.minOccupationDateRange(checkInDay);
return occupationDateRange.toStream()
.map(calculatedDay -> products.getHotelProducts().stream().map(room ->
priceRule.getPrice(calculatedDay, roomPriceData.get(room.getRoomNo()).getMinPriceByDay(calculatedDay)))
.min(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO))
.reduce(BigDecimal::add)
.orElseThrow(() -> new RuntimeException("price is not available"));
})
.min(BigDecimal::compareTo)
.orElseThrow(() -> new RuntimeException("price is not available"));
}
}

⚠️注:

这里之前在设计的时候,一直存在一个问题,为什么外部的价格系统:外部库存和价格不属于 HotelOffer 聚合,应该通过领域服务协作,不应该注入 HotelOffer 中,这样会破坏聚合。为什么这里还通过参数传进去呢?

聚合根可以在行为中接受来自其他上下文的只读数据作为参数,用于做出业务决策,只要:

  • 它不会将这些数据变成自身持久化状态;
  • 它不会因此破坏自身聚合边界;
  • 它仍然主导业务规则的决策。
  • 外部的信息需要做好防腐层的映射,只提供 HotelOffer 需要的能力,这样 HotelOffer 就不需要感知到外部信息的变化,只聚焦自己本身的业务

应用层只需要查出这个HotelOffer的聚合,然后聚合基于实时价格查出当前最小价格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ApplicationServcie {

private HotelOfferRepository hotelOfferRepository;
private PriceDataRepository priceDataRepository;

public BigDecimal calculateMinPriceV2(String offerNo, LocalDate checkInDay) {
// 1. 获取聚合根
HotelOffer hotelOffer = hotelOfferRepository.queryHotelOfferByOfferNo(offerNo);

// 2. 获取外部价格数据
List<String> roomNoList = hotelOffer.getRoomNoList();
Map<String, PriceData> roomPriceDataMap = priceDataRepository.queryPriceDataByRoomList(roomNoList);

// 3. 使用领域服务协调聚合和外部数据
return hotelOffer.calculateMinPrice(checkInDay, roomPriceDataMap);
}
}

关键改进

  1. 业务概念清晰化: 明确了HotelOffer作为酒店产品的核心概念
  2. 封装业务规则: 将价格计算逻辑封装在聚合根内部
  3. 依赖倒置: 通过AbstractPriceData抽象外部价格数据

🎯 重构收益对比

维度 重构前(大泥球) 重构后(DDD)
代码行数 单个方法 多个职责清晰的小方法
业务概念 混杂不清 聚合根、实体、值对象清晰
测试性 难以单元测试 每个组件可独立测试
扩展性 修改影响全局 职责分离,影响局部
可读性 需要深入理解所有细节 业务概念一目了然
维护性 高风险,牵一发动全身 低风险,职责边界清晰

🔄 第二阶段:防腐层的建立

外部系统集成挑战

随着业务发展,需要集成多个外部系统:

  • 价格数据系统(版本不断演进)
  • 库存管理系统
  • 第三方OTA平台

PriceData 演进过程

PriceData V1: 基础版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PriceData extends AbstractPriceData {
private String roomNo;

private List<PricePair> pricePairs;

@Override
public BigDecimal getMinPriceByDay(LocalDate day) {
return pricePairs.stream().filter(item -> day.isEqual(item.getDay()))
.map(PricePair::getPrice)
.min(BigDecimal::compareTo)
.orElseThrow(() -> new RuntimeException("no available price"));
}

@Data
@AllArgsConstructor
public class PricePair {
private LocalDate day;
private BigDecimal price;
}

}

PriceDataV2: 增强版本 支持不同时段的定价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PriceDataV2 extends AbstractPriceData {
private String roomNo;
private List<TimingPrice> timingPriceList; // 支持时间维度的价格

@Override
public BigDecimal getMinPriceByDay(LocalDate day) {
// 支持多时段价格,选择最优价格
return timingPriceList.stream()
.filter(timingPrice -> timingPrice.getDay().isEqual(day))
.map(TimingPrice::getPrice)
.min(BigDecimal::compareTo)
.orElseThrow();
}

// 内部类:支持时间维度的价格
private class TimingPrice {
private BigDecimal price;
private LocalTime timing;
private LocalDate day;
}
}

防腐层设计:PriceDataAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* 价格数据适配器 - 增强防腐层
* 将外部价格数据转换为聚合根友好的领域概念
*/
public class PriceDataAdapter {

/**
* 将外部价格数据适配为聚合根内部使用的价格查询器
*/
public static RoomPriceQuery adaptToPriceQuery(Map<String, ? extends AbstractPriceData> externalPriceData) {
return new RoomPriceQuery() {
@Override
public BigDecimal queryRoomMinPrice(String roomNo, LocalDate day) {
AbstractPriceData priceData = externalPriceData.get(roomNo);
if (priceData == null) {
throw new RuntimeException("No price data found for room: " + roomNo);
}
return priceData.getMinPriceByDay(day);
}

@Override
public boolean hasDataForRoom(String roomNo) {
return externalPriceData.containsKey(roomNo);
}
};
}

/**
* 聚合根内部使用的价格查询接口 - 领域概念
*/
public interface RoomPriceQuery {
BigDecimal queryRoomMinPrice(String roomNo, LocalDate day);
boolean hasDataForRoom(String roomNo);
}
}


/**
* 使用适配器的计算方法 - 更纯粹的领域概念
* 展示如何进一步增强防腐层
*/
public BigDecimal calculateMinPriceWithAdapter(LocalDate checkInDay,
PriceDataAdapter.RoomPriceQuery priceQuery) {
// 使用领域友好的价格查询接口,而不是直接依赖外部数据结构
return priceRuleList.stream().map(priceRule -> {
DateRange occupationDateRange = products.minOccupationDateRange(checkInDay);
return occupationDateRange.toStream()
.map(calculatedDay -> products.getHotelProducts().stream()
.filter(room -> priceQuery.hasDataForRoom(room.getRoomNo()))
.map(room -> priceRule.getPrice(calculatedDay,
priceQuery.queryRoomMinPrice(room.getRoomNo(), calculatedDay)))
.min(BigDecimal::compareTo)
.orElse(BigDecimal.ZERO))
.reduce(BigDecimal::add)
.orElseThrow(() -> new RuntimeException("price is not available"));
})
.min(BigDecimal::compareTo)
.orElseThrow(() -> new RuntimeException("price is not available"));
}

通过PriceDataAdapter 可以完全屏蔽来自聚合外的知识,在这个聚合计算最小价格时,不需要感知外部业务的价格变化,只需要通过 roomNo 的引用,就可以完成价格的查询,配置聚合内的商品进行价格计算。

防腐层的价值

  1. 隔离变化: 外部系统变化不影响核心业务逻辑
  2. 领域友好: 提供符合业务语言的接口
  3. 版本兼容: 支持多版本外部系统同时存在

🚀 第三阶段:聚合根演进

HotelOfferV2: 支持客户选择策略

业务需求变化:需要支持不同的客户选择策略(固定价格、最低价格等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**  
* 酒店产品聚合根V2
* 新增:客户选择策略支持
*/
public class HotelOfferV2 {
String offerNo;
HotelProduct products;
CustomerChoice customerChoice;
List<PriceRule> priceRuleList;
Validity validity;


/**
* 计算最低价格(核心业务方法)
* 支持客户选择策略,内部逻辑完全封装
*/
public BigDecimal calculateMinPrice(LocalDate checkInDay, PriceDataAdapter.RoomPriceQuery priceQuery) {
return priceRuleList.stream().map(priceRule -> {
DateRange occupationDateRange = products.minOccupationDateRange(checkInDay);
return occupationDateRange.toStream()
.map(calculatedDay -> products.getHotelProducts().stream().map(room ->
priceRule.getPrice(calculatedDay, priceQuery.queryRoomMinPrice(room.getRoomNo(), calculatedDay)))
.reduce(getMinimalPriceCalculateMethod(customerChoice))
.orElse(BigDecimal.ZERO))
.reduce(BigDecimal::add)
.orElseThrow(() -> new RuntimeException("price is not available"));
})
.min(BigDecimal::compareTo)
.orElseThrow(() -> new RuntimeException("price is not available"));
}

/**
* 根据客户选择确定价格计算方法(内部策略)
*/
private BinaryOperator<BigDecimal> getMinimalPriceCalculateMethod(CustomerChoice customerChoice) {
if (customerChoice == CustomerChoice.FIXED) {
return BigDecimal::add;
} else {
return BigDecimal::min;
}
}

}

演进亮点

  1. 策略模式: 通过CustomerChoice支持不同的价格计算策略
  2. 向后兼容: 保持原有接口不变,添加新功能
  3. 业务规则封装: 策略选择逻辑完全封装在聚合根内部, 外部业务层无需感知,也不需要变动

HybridOffer: 组合产品支持

业务进一步演进:需要支持酒店+景点的组合产品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 混合产品聚合根
* 支持酒店+景点的组合产品
*/
@Data
@AllArgsConstructor
public class HybridOffer {
private ProductGroups productGroups; // 产品组合
private String offerNo;
private List<PriceRule> priceRuleList;
private Validity validity;
private CustomerChoice customerChoice;

/**
* 组合产品价格计算
*/
public BigDecimal getMinPriceV3(LocalDate checkInDay,
Map<String, ? extends AbstractPriceData> priceData) {
// 验证可用性
if (!validity.validateCheckInDayIsAvailable(checkInDay)) {
throw new RuntimeException("checkInDay is not available");
}

// 计算酒店部分价格
HotelProduct hotelProduct = productGroups.getHotelProduct();
BigDecimal hotelPrice = calculateHotelPrice(checkInDay, priceData, hotelProduct);

// 计算景点部分价格
AttractionProduct attractionProduct = productGroups.getAttractionProduct();
BigDecimal attractionPrice = calculateAttractionPrice(checkInDay, priceData, attractionProduct);

// 组合价格
return hotelPrice.add(attractionPrice);
}

/**
* 获取不同产品类型的标识符列表
*/
public List<String> getHotelRoomList() {
return productGroups.getHotelProduct().getHotelProducts().stream()
.map(RoomInfo::getRoomNo)
.collect(Collectors.toList());
}

public List<String> getAttractionTicketList() {
return productGroups.getAttractionProduct().getProductItemList().stream()
.map(TicketItem::getProductNumber)
.collect(Collectors.toList());
}
}

关键创新

  1. 组合模式: 通过ProductGroups管理不同类型的产品
  2. 统一接口: 对外提供一致的价格计算接口
  3. 扩展性: 易于添加新的产品类型,商品组合只在聚合内进行变化,对外还是提供价格计算

🏗️ 第四阶段:多聚合协调与领域服务

问题:跨聚合的复杂业务逻辑

随着业务复杂度增加,出现了跨多个聚合的复杂业务场景:

  • 用户定价策略(基于用户属性,会员策略,渠道)
  • 营销定价策略(基于市场活动,促销、假日限购)
  • 综合定价逻辑(协调多个策略)

用户定价策略聚合根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 用户定价策略聚合根
* 职责:管理基于用户属性的定价策略(等级、地域、渠道等)
*/
public class UserPricingStrategy {
private String strategyId;
private String strategyName;
private boolean active;
private PriorityLevel strategyPriority;

// 时效性支持
private LocalDateTime effectiveStartTime;
private LocalDateTime effectiveEndTime;
private DateRange validDateRange;

// 策略组件
private List<UserLevelDiscount> userLevelDiscounts;
private List<RegionPricing> regionPricings;
private List<ChannelPricing> channelPricings;
private PriorityRule priorityRule;

/**
* 核心业务方法:计算用户策略折扣
*/
public BigDecimal calculateUserDiscount(BigDecimal basePrice,
UserContext userContext,
LocalDateTime currentTime) {
//processing....

return finalPrice;
}


}

营销定价策略聚合根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 营销定价策略聚合根
* 职责:管理节假日、限时活动等可配置的营销策略
*/
public class MarketingPricingStrategy {
private String strategyId;
private String strategyName;
private StrategyType strategyType;
private boolean active;
private DateRange effectivePeriod;
private PriorityLevel priorityLevel;

// 不同类型的营销策略
private List<HolidayPricing> holidayPricings;
private List<FlashSaleActivity> flashSaleActivities;
private List<SeasonalPricing> seasonalPricings;

/**
* 计算营销策略价格
*/
public BigDecimal calculateMarketingPrice(BigDecimal basePrice,
LocalDate targetDate,
MarketingContext context) {
//processing....

return finalPrice;
}
}

领域服务:ComprehensivePricingDomainService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**  
* 综合定价领域服务
* 职责:协调 HotelOffer 基础价格、用户策略定价、营销策略定价,计算最终价格
*/
public class ComprehensivePricingDomainService {

/**
* 计算综合最终价格
* 体现了领域服务协调多个聚合根的职责
*/
public static PricingResult calculateFinalPrice(
HotelOffer hotelOffer,
LocalDate checkInDay,
Map<String, ? extends AbstractPriceData> roomPriceData,
UserContext userContext,
MarketingContext marketingContext,
List<UserPricingStrategy> userPricingStrategies,
List<MarketingPricingStrategy> marketingPricingStrategies) {

// 1. 计算基础价格 (HotelOffer聚合根)
BigDecimal basePrice = HotelPricingDomainService.calculateMinPrice(
hotelOffer, checkInDay, roomPriceData);

// 2. 应用用户策略定价 (UserPricingStrategy聚合根)
BigDecimal userDiscountedPrice = applyUserPricingStrategies(
basePrice, userContext, userPricingStrategies);

// 3. 应用营销策略定价 (MarketingPricingStrategy聚合根)
BigDecimal marketingPrice = applyMarketingPricingStrategies(
userDiscountedPrice, checkInDay, marketingContext, marketingPricingStrategies);

// 4. 构建定价结果
return buildPricingResult(basePrice, userDiscountedPrice, marketingPrice,
checkInDay, userContext, marketingContext);
}
}

通过这次的业务迭代和不断地聚合演进,可以看出,随着业务迭代,我们始终聚焦在各个上下文内部,当业务迭代后,只需要在聚合内部进行业务变更,外层不需要感知,应用层还是保持不变,同时当存在多个聚合时,可以通过领域服务来进行跨聚合的处理,对于应用层还是无感知的。

DDD-Aggregation

DDD 分层架构

在进行分层时,我们保证同一层的组件处于同一个抽象层次。这是分层架构的设计原则,通过“单一抽象层次原则(SLAP)”,可以保证每层的上下文边界都限制在该层中,通过上下文的映射来进行信息传递,避免不同层的变化,导致业务进行变动。

DDD 分层

领域建模的不停演进

在业务流程设计过程中,随着业务的迭代,业务提供的能力也会不断增加,在前期设计过程中,可能无法预测应用层到底会具备什么能力,我们可以在领域层提供不同聚合的能力,变成原子能力,如图中所示例,刚开始应用层服务A、B,只需要领域服务 a、领域服务 b,随着业务迭代,需要提供一种新的业务能力,由于底层的领域层我们已经封装了原子的能力,那么提供一个新的领域服务,来编排不同的聚合能力,来达到新业务的需求。通过这种方式,可以灵活的适应业务的迭代,以及编排的复杂度。

业务持续迭代