9.2 领域模型

用同时包含数据与行为的对象模型表达领域,让规则、状态变化和不变量围绕业务概念协作。

学习目标

  • 能把订单折扣与授信规则分配给拥有相应状态和行为的领域对象
  • 能用不变量、聚合边界和事务边界解释领域模型与事务脚本的取舍
  • 能识别贫血对象、跨对象规则泄漏和伪多态,并提出可验证的重构步骤

为什么 9.2 领域模型 值得单独学习

用一组相互协作的对象表达业务状态、行为和约束。它不是把数据库表逐一翻译成类,也不是为了“面向对象”而堆叠 getter 和 setter;它要求业务决定靠近真正拥有知识和状态的对象,让规则变化有稳定的落点。

订单折扣与授信很适合观察这个差异。金额计算可以暂时放进一个用例过程,但当会员等级、促销互斥、授信额度和退款规则被多个用例共同使用时,规则已经不属于某一个入口。此时,单纯复制条件分支会制造漂移,模型需要表达“订单能否提交”“额度是否足够”“折扣是否可叠加”等业务语言。

本章的案例、对象图、实验和判断题均为课程原创;Martin Fowler 的公开图书页与模式目录只用于核对 9.2 的模式名称、范围和相关模式族。学习重点是从可观察的状态变化推导结构选择,而不是复述原书正文或代码。

先建立直觉:让对象对自己的决定负责

先猜一猜:如果订单服务在控制器里读取订单、读取会员、判断促销、扣减额度,接下来退款流程也要复用这些规则,继续复制条件会发生什么?最先出现的不是代码变长,而是同一个业务决定开始拥有多个版本。领域模型的第一步,是把拥有状态的对象和作出决定的行为放回同一处。

一个可工作的至少应该让读者看到三件事:它持有哪些业务状态,它保护哪些变化条件,以及其他对象通过什么行为请求变化。Order 不应只是公开 statustotal 的数据袋;它可以通过 submit() 检查订单状态和金额,通过 applyDiscount() 让折扣策略参与决定,通过 reserveCredit() 与授信对象协作。

对象有行为并不等于所有逻辑都必须塞进一个类。查询、持久化和外部通知仍可由应用服务协调,但业务决定应由能解释该决定的对象完成。这样,入口从 HTTP 改成批处理时,规则仍然沿用同一组对象行为。

目录单元到教学证据

9.2 领域模型

9.2 领域模型 的单元边界内,学习者要用订单折扣与授信说明:复杂规则为什么需要对象协作,以及对象模型如何避免表示层和数据库结构吞掉业务行为。单元键为 poeaa24-pattern-02-domain-model;核心证据是同一条规则被下单、退款和改价三个用例复用时,决定仍然只有一个来源。

评审记录要回答五个问题:谁拥有状态,谁验证约束,哪个对象发起状态变化,哪些对象必须一起保持一致,以及持久化失败后如何恢复。若这些答案都只能落在控制器或 ORM 回调里,说明系统还没有形成可解释的领域模型。

专属设计案例:订单折扣与授信

把一次提交订单拆成 OrderCustomerDiscountCreditAccount 四个协作角色。Order 管理订单行和提交状态,Customer 提供会员资格,Discount 表达可替换的折扣决定,CreditAccount 负责额度占用和释放。应用服务只负责加载对象、开启事务、调用行为并保存结果。

设计记录采用五个字段:模式键为 poeaa24-pattern-02-domain-model;当前裁决是“跨用例复用、跨对象不变量和规则频繁变化时采用领域模型”;观测项包括规则复杂度、对象协作、聚合边界、事务边界和演化成本;保留条件是“对象行为能用业务语言命名并被单独测试”;拒绝条件是“模型只是表字段的镜像,真正决定仍集中在服务层”。

从数据袋到行为对象

先给每个对象写一个最小责任,而不是先设计完整类层次。Order.submit() 可以检查订单是否为空、折扣是否已计算以及授信是否已预留;CreditAccount.reserve() 只处理额度是否足够和占用记录;Discount.apply() 只回答当前订单是否符合该策略。每个行为都应该能指出自己读取的状态和保护的约束。

用不变量划出边界

领域模型要保护的是“任何有效状态都必须成立”的业务条件,例如订单提交后金额不能被无条件改写,额度占用不能超过可用额度,互斥促销不能同时生效。对象边界不是按表名切,而是按哪些状态必须一起决定和保存来切。

订单与订单行通常需要放在同一个内,因为订单总额和行项目必须保持一致。会员资料或授信账户可以是另一个聚合;订单只持有它们的标识或通过明确的领域服务协作,避免一次操作隐式加载整个系统。

模式结构图

领域模型:对象网络 + 行为封装Order- items: OrderItem[]- customer: Customer+ calculateTotal()Customer- creditLimit: Money- orders: Order[]+ checkCredit()Product- price: Money- discount: Discount+ getDiscountedPrice()1..*多态:Discount 策略PercentDiscountprice * (1 - rate)AbsoluteDiscountprice - amountBulkDiscountqty > N → 打折适用:规则复杂、频繁变化、需要多态和对象协作的系统
领域模型用对象网络封装业务规则,每个对象有自己的行为和状态。 多态让不同策略有不同实现,规则变化时只需修改对应类。

用订单折扣走一遍建模过程

分步1 / 3

先找出拥有状态的对象

先预测:订单金额、订单状态和订单行应该由控制器、数据库表,还是 Order 对象负责?如果一个对象能同时看到相关状态并表达提交行为,业务决定就有了可复用的落点;入口只需传入请求并调用它。

领域模型:对象网络 + 行为封装Order- items: OrderItem[]- customer: Customer+ calculateTotal()Customer- creditLimit: Money- orders: Order[]+ checkCredit()Product- price: Money- discount: Discount+ getDiscountedPrice()1..*多态:Discount 策略PercentDiscountprice * (1 - rate)AbsoluteDiscountprice - amountBulkDiscountqty > N → 打折适用:规则复杂、频繁变化、需要多态和对象协作的系统
领域模型用对象网络封装业务规则,每个对象有自己的行为和状态。 多态让不同策略有不同实现,规则变化时只需修改对应类。

常见误区

多态如何承接变化

当折扣只有一种稳定算法时,一个行为方法就够了;当百分比折扣、满件折扣和会员折扣各自变化时,可以用Discount 约定统一行为,由不同策略实现自己的计算。调用方只请求“计算折扣”,不必知道当前策略的分支细节。

多态不是把所有 if 改成类名。每个实现必须拥有清晰的变化理由,且共同约定应该由测试保护。如果策略之间共享了大量条件,说明抽象边界还不稳定;保留一个简单过程可能比提前建立层次更诚实。

对象行为最终仍要在一个内协同。应用服务可以加载订单、会员和授信账户,调用领域行为,再一次性保存必要变化;领域对象决定“能否发生”,事务协调决定“哪些变化一起提交”。不要把数据库回滚误当成跨外部通知的恢复方案。

选择与拒绝矩阵

评审问题选择领域模型的证据应拒绝或暂缓的信号
责任状态与业务行为由能解释它的对象共同拥有对象只有字段,决定集中在控制器或服务层
变化多个用例复用同一规则,变化可局部落在策略或对象每个入口复制一套条件,修改必须人工同步
一致性聚合边界明确,关键不变量由行为保护任意调用者都能直接改写关键状态
协作对象通过少量业务行为协作,调用路径可追踪为了“面向对象”增加大量转发包装,却没有新责任
替代已与事务脚本和表模块比较复杂度、复用和演化成本仅因团队偏好或框架生成类就选择领域模型

本章小结

掌握 9.2 领域模型的标志,不是画出更多类,而是能让订单折扣与授信的业务决定回到拥有状态和知识的对象。用不变量确定聚合边界,用多态承接稳定的变化轴,让应用服务负责事务协调;当对象仍是数据袋或规则继续在入口复制时,模型还没有完成。每次重构都应以成功、拒绝、回滚和复用测试作为证据。

本章练习

练习

问题 1: 订单包含订单行,订单总额必须与订单行一致。应该让控制器直接修改订单行列表,还是让 Order 提供行为?请说明边界依据。

问题 2: 百分比折扣、满件折扣和会员折扣各自变化,但下单、退款和改价都需要计算折扣。如何使用领域模型降低复制?

问题 3: 应用服务先调用 CreditAccount.reserve(),再发送外部确认消息;消息发送失败时数据库事务已提交。领域模型能自动解决什么,仍需补什么?

前后导航

出处声明

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

领域模型
用对象及其协作表达业务状态、行为和约束的组织方式,适合规则复杂、频繁变化或跨用例复用的场景。
领域对象
同时拥有业务状态与相关行为、能够保护自身规则的模型对象。
不变量
任何有效业务状态都必须成立的约束,例如额度不能超用或提交后订单不能任意改写。
聚合边界
必须作为一个一致性单元被决定和保存的一组对象及其根对象范围。
多态
通过统一行为约定,让不同对象或策略提供各自实现,以隔离变化分支的机制。
事务边界
必须整体提交或整体失败的一组持久化变化,不等同于领域对象的业务规则。

讨论

评论区加载中…