DDD系列:一、 Domain Primitive
为什么需要Domain Driven Design
软件开发中,降低系统复杂度 是一个永恒的挑战。如使用一些设计模式或范例,从偏技术的角度来降低软件复杂度,而 DDD 的目标是对业务设计提出一种架构思想,进而降低复杂度。
Domain Primitive(DP,最原始的领域):
在一个特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object。
- DP是一个传统意义上的Value Object,可能拥有 Immutable 的特性*(内部的属性被构建后,不允许被修改)*
- DP是一个完整的概念整体,拥有精准定义*(对象中属性设值是经过了校验的,是合法的)*,拥有行为
- DP可以是业务域的最小组成部分、也可以构建复杂组合*(将业务抽象为不可继续分割的对象)*
使用 DP 需要遵循的三个原则
原则一:Make Implicit Concepts Expecit(将隐性的概念显性化)
接口的清晰度
不好的设计:
public User register(String name, String phone, String address)
较好的设计:
public User register(Name, PhoneNumber, Address)
当接口中同一类型参数较多时,使用时容易出错,且不易被发现。
此时可以考虑将同一类型的参数转换为对应的DP模型,使接口变得更清晰、DP内的数据规范化
原则二:Make Implicit Context Expecit(让隐性的上下文显性化)
public class Money {
private BigDecimal amount;
private Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
}
交易过程中,Money中的currency,便是一个隐性的交易属性,将其放在Money对象中,使其凸显出来。
原则三:Encapsulate Multi-Object Behavior(封装 多对象 行为)
public void pay(Money money, Currency targetCurrency, Long recipientId) {
ExchangeRate rate = ExchangeService.getRate(money.getCurrency(), targetCurrency);
Money targetMoney = rate.exchange(money);
BankService.transfer(targetMoney, recipientId);
}
public class ExchangeRate {
private BigDecimal rate;
private Currency from;
private Currency to;
public ExchangeRate(BigDecimal rate, Currency from, Currency to) {
this.rate = rate;
this.from = from;
this.to = to;
}
public Money exchange(Money fromMoney) {
notNull(fromMoney);
isTrue(this.from.equals(fromMoney.getCurrency()));
BigDecimal targetAmount = fromMoney.getAmount().multiply(rate);
return new Money(targetAmount, to);
}
}
在跨境支付中(pay),会涉及到汇率转换,为了避免在业务逻辑中去处理汇率计算,我们用DP(ExchangeRate)包装掉“币种转换计算的”动作。计算时需要交易的双端币种、计算费率等计算属性,我们将其都放在一个DP中,封装多个对象完成计算。
常见的DP的使用场景:
- 有格式限制的
String
:比如Name
,PhoneNumber
,OrderNumber
,ZipCode
,Address
等,将隐性的概念显性化 - 有限制的
Integer
:比如OrderId
(>0),Percentage
(0-100%),Quantity
(>=0)等,将隐性的概念显性化 - 可枚举的
int
:比如Status
,将隐性的概念显性化 Double
或BigDecimal
:一般用到的Double
或BigDecimal
都是有业务含义的,比如Temperature
、Money
、Amount
、ExchangeRate
、Rating
等,将隐性的概念显性化- 复杂的数据结构:比如
Map<String, List<Integer>>
等,尽量能把Map
的所有操作包装掉,仅暴露必要行为,将多对象的行为进行封装
DP与其他POJO的区别
与Value Object的对比:
Domain Primitive 是 Value Object 的进阶版,在原始VO的基础上要求每个DP 拥有概念的整体,而不仅仅是值对象。在VO的Immutable基础上增加了Validity和行为。当然同样的要求无副作用(side-effect free)。
与Data Transfer Object 的区别
DTO | DP | |
---|---|---|
功能 | 数据传输 属于技术细节 | 代表业务域中的概念 |
数据的关联 | 只是一堆数据放在一起,不一定有关联度 | 数据之间的高相关性 |
行为 | 无行为 | 丰富的行为和业务逻辑 |
按DP的思维重构老应用的流程
基于重构的原则(影响的代码范围 和 耗时),我们可以使用如下流程:
第一步:
按业务梳理出DP对象与行为,然后从老代码中抽离方法。
所有抽离出来的方法要做到无状态,因为DP属性中,不能带标识状态的数据,所以一切需要改变状态的代码都不属于DP的范畴。
第二步:
替换数据校验和跟DP相关的无状态业务逻辑
为了减小该步骤的工作量,可以不修该接口的签名,只修改接口内部的实现。
第三步:
定义一个新的接口,参数使用定义的DP模型。重新编排接口实现类中的逻辑
第四步:
修改外部调用的方式(入参修改为对应的DP模型)。