定义
在领域驱动设计(Domain-Driven Design,DDD)中,“Domain Primitive”(领域原语)是指领域模型中的基本数据类型或值对象,它们代表了业务领域中的最基本的、不可分割的数据元素。Domain Primitive通常是不可变的,它们的行为受到业务规则的约束,并且通常不包含任何业务逻辑。
Domain Primitive可以是简单的数据类型,如字符串、整数、布尔值等,也可以是一些复杂的值对象,如金额、日期、时间范围等。它们用于描述业务领域中的基本概念和属性,例如订单号、产品价格、用户姓名等。通过使用Domain Primitive,可以有效地表达业务需求,并且保持领域模型的清晰和简洁。
在DDD中,Domain Primitive与其他领域模型元素(如实体、聚合、服务等)一起组成了完整的领域模型。通过对领域原语的合理设计和使用,可以构建出具有高内聚性和低耦合性的领域模型,从而更好地满足业务需求,并且易于维护和演化。
上面是比较官方的解释,下面是我总结的DP的特性:
DP:隐式对象、隐式上下文、多对象封装 -- 无状态的业务值对象。
- DP是一个传统意义上的Value Object,拥有Immutable-不可变的特性。
- DP是一个完整的概念整体,拥有精准定义。
- DP使用业务域中的原生语言。
- DP可以是业务域的最小组成部分、也可以构建复杂组合。
结合具体例子讲解下DP在实际业务中的应用。
应用
1.单个原子业务概念的封装。
一个新应用在全国通过 地推业务员 做推广,需要做一个用户的注册系统,在用户注册后能够通过用户电话号的区号对业务员发奖金。
在这里,我们可以看到,原来电话号仅仅是用户的一个参数,属于隐形概念,但实际上电话号的区号才是真正的业务逻辑,而我们需要将电话号的概念显性化,通过写一个Value Object来表示。
public class PhoneNumber {private final String number;public String getNumber() {return number;}public PhoneNumber(String number) {if (number == null) {throw new ValidationException("number不能为空");} else if (isValid(number)) {throw new ValidationException("number格式错误");}this.number = number;}public String getAreaCode() {for (int i = 0; i < number.length(); i++) {String prefix = number.substring(0, i);if (isAreaCode(prefix)) {return prefix;}}return null;}private static boolean isAreaCode(String prefix) {String[] areas = new String[]{"0571", "021", "010"};return Arrays.asList(areas).contains(prefix);}public static boolean isValid(String number) {String pattern = "^0?[1-9]{2,3}-?\\d{8}$";return number.matches(pattern); }}
DP对象将业务数据和业务校验的逻辑封装内聚到PhoneNumber VO对象中,它是业务模型的最小对象不可再分割,同时精准表达业务的原生语义。
下面来看一个组合对象的例子。
2. 当我们做这个支付功能时,实际上需要的一个入参是支付金额 + 支付货币。我们可以把这两个概念组合成为一个独立的完整概念:Money。
@Value //不可变类
public class Money {private BigDecimal amount;private Currency currency;public Money(BigDecimal amount, Currency currency) {this.amount = amount;this.currency = currency;}
}@Value
public class Currency {private String code;private String name;
}
其中Currency表示货币代码和名称。
3. 对隐藏业务行为的封装,可以将转换汇率的功能,封装到一个叫做 ExchangeRate 的 DP 里。
@Value
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);}
}
下面在领域服务中使用DP对象:
public class TransferServiceImplNew implements TransferService {private AccountRepository accountRepository;private AuditMessageProducer auditMessageProducer;private ExchangeRateService exchangeRateService;private AccountTransferService accountTransferService;@Overridepublic Result<Boolean> transfer(Long sourceUserId, String targetAccountNumber, BigDecimal targetAmount, String targetCurrency) {1.当前业务处理// 参数校验Money targetMoney = new Money(targetAmount, new Currency(targetCurrency));// 读数据Account sourceAccount = accountRepository.find(new UserId(sourceUserId));Account targetAccount = accountRepository.find(new AccountNumber(targetAccountNumber));ExchangeRate exchangeRate = exchangeRateService.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());// 业务逻辑accountTransferService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);// 保存数据accountRepository.save(sourceAccount);accountRepository.save(targetAccount); 2.关联业务处理 -- 发送领域事件通知 // 发送审计消息AuditMessage message = new AuditMessage(sourceAccount, targetAccount, targetMoney);auditMessageProducer.send(message);return Result.success(true);}
}
DDD的注意点
-
实体对象只能保存自己的状态,不能关联其他实体对象,只可以通过参数的方式依赖。
-
实体的不变性、数据的一致性 : 创建即一致\尽量避免public setter\通过聚合根保证主子实体的一致性。一个最容易导致不一致性的原因是实体暴露了public的setter方法,特别是set单一参数会导致状态不一致的情况。【建议】在有些简单场景里,有时候确实可以比较随意的设置一个值而不会导致不一致性,也建议将方法名重新写为比较“行为化”的命名,会增强其语意。比如setPosition(x, y)可以叫做moveTo(x, y),setAddress可以叫做assignAddress等。
-
不可以强依赖其他聚合根实体或领域服务:只保存外部实体的ID、针对于“无副作用”的外部依赖。
-
任何实体的行为只能直接影响到本实体(和其子实体)。
-
在自己的业务中强制验证,能百分百保证使用这个业务时的正确性,防止不熟悉的人或者时间久远导致遗漏。
Player.equip(Weapon, EquipmentService) {
EquipmentService.canEquip(this, Weapon);
} ✅
boolean canEquip = EquipmentService.canEquip(Player, Weapon);
if (canEquip) {
Player.equip(Weapon); // ❌,这种方法不可行,因为这个方法有不一致的可能性,迪米特法则--内部封装、隐藏细节
} -
在封装领域规则时,可以通过策略管理对象 + 具体的策略对象完成策略的逻辑。