首页
DDD 之 Domain Primitive
不是讲架构,而是一种比较好用的开发思想。
Java Bean
- 所有属性为private
 - 提供默认构造方法
 - 提供getter和setter
 - 实现serializable接口
 
问题
以前是被告知好处是使用getter和setter可以自行添加业务逻辑,但是慢慢共识已经变成不允许添加任何逻辑了,如果添加了可能造成不必要的bug,唯一的作用可能只是说控制某些属性的可见性和可修改性。
Data Object (DO)
- 首先是一个Java Bean
 - 数据库模型的映射
 - 日常开发使用的数据模型,一般放置在 entity 或 domian 包下
    
// 一个典型的DO类 @Data @TableName("tb_user") public class User implements Serializable { @TableId private Long id; private String name; private String phone; private String address; } 
DO应该如何设计和使用
- 是一个java bean
 - 字段与数据库字段一一对应
 - 将其活动范围限制在基础设施层
 
DTO Assembler & Data Converter
- DTO Assembler:DTO 和 entity互相转化
 - Data Converter:DO 和 entity互相转化
 
简单的web三层架构
UI层(controller)、业务层(service、facade等)、基础设施层(mapper、repository等)
- 问题:
    
- 上层对下层直接依赖,耦合度过高(本次不涉及)
 - 业务层没有自己的数据模型,将DO当成了Entity(领域)使用。
 
 - 正确的数据模型对应关系:
    
- UI层:VO、DTO、Query、Command
 - 业务层:Entity (domain)
 - 基础设施层:DO、DTO(rpc)
 
 
将DO当作entity使用带来的问题
一段用户注册的代码示例:
// 业务层
public class RegistrationServiceImpl implements RegistrationService {
    private UserRepository userRepo;
    public User register(String name, String phone, String address) {
        // 校验逻辑
        if (name == null || name.length() == 0) {
            throw new ValidationException("name");
        }
        if (phone == null || !isValidPhoneNumber(phone)) {
            throw new ValidationException("phone");
        }
        // 省略其他字段校验逻辑
        // 业务逻辑
        if(userRepo.findByName(name) != null){
            throw new ValidationException("姓名不能重复");
        }
        if(userRepo.findByPhone(phone) != null){
            throw new ValidationException("手机号不能重复");
        }
        // 省略其他业务逻辑
        // 创建用户DO
        User user = new User();
        user.setName(name);
        user.setPhone(phone);
        user.setAddress(address);
        // 调用基础设施层方法持久化 返回结果
        return userRepo.save(user);
    }
}
- 问题1:清晰度不明确,属性使用的是没有特殊意义的原始类型,并不是一个具体的概念,只能通过变量名,无法通过变量类型去区分。
    
User register(String name, String phone, String address) // 上述注册方法在运行时 参数全是String类型 User register(String, String, String); // 在controller层如此调用不会出现问题,因为是完全可以运行的,甚至如果遗漏了参数校验,甚至可能直接落库,导致出现脏数据 service.register("张三", "浙江省杭州市", "13211110000"); // 因为name和phone类型都是String 所以只能在方法名加上ByXXX区分 User findByName(String name); User findByPhone(String phone); // 多个参数也只能通过变量名区分 User findByNameOrPhone(String name, String phone); // 参数弄错可以直接运行,运行后出现问题不好排查 findByNameOrPhone(user.getPhone(), user.getName()); - 问题2:数据验证和错误处理代码四散,每次使用Name,Phone这种有业务意义的参数时,都需要添加相应的校验代码,会有大量的重复代码,维护成本非常高,而且也是不可控的,因为无法保证是否会被调用以及调用的方式是否正确。
 - 问题3:业务代码的清晰性,如果需要解析参数,如从address参数解析出省市区信息,我们还得额外添加一段代码。
 - 问题4:可测试性,如果我想测试校验address参数是否合法的代码?我们如何编写测试用例?
    
// 弊端:不能保证调用方使用相同的调用方式 @Test void testAddress() { String address = " "; // if(address==null){ throw new ValidationException(); } if(!address.contains("省")){ throw new ValidationException(); } // 其他省略 } // 抽离出静态工具类 弊端业务逻辑分散 @Test void testAddress() { String address = " "; // if(AddressUtils.validate(address)){ throw new ValidationException(); } } 
现有的解决方案
- 抽离成公共代码
    
- 不在同一个类里也需要粘贴复制,以后修改得多处修改。
 
 - 抽离成静态工具类
    
- 工具类的基本原则就是封闭修改的,而静态代码是一段业务逻辑,不可避免的会被修改;
 - 大量的静态工具类也会造成业务逻辑代码的分散,增加维护难度;
 - 无法控制其他开发人员如何使用静态工具类,或者还需要去了解使用的方法。
 
 
根本问题就是:name、address、phone都是具有业务意义的概念,并不能简单的用原始类型去描述。
Java Primitive 原始类型
八大原始类型,以及对应的包装类、String 、BigDecimal、BigInteger、以及枚举等都可以视作 Java 语言和 Java Bean 的基础。
- 不从任何事物发展而来
 - 初级的形成或生长的早期阶段
 
Domain Primitive 领域的基础
- DP 是一个传统意义上的 Value Object (表示一个有含义的值的对象),拥有 Immutable 的特性。
 - DP 是一个完整的概念整体,拥有精准定义。
 - DP 使用业务域中的原生语言。
 - DP 可以是业务域的最小组成部分、也可以构建复杂组合。
 
如何创建一个DP?
- 隐形概念显性化
    
- 创建一个 Type(数据类型)去显性的表示一个概念。
 - 将这个概念相关的逻辑完整的收集到一个 Class(类)里。 ```java // 参考Integer String 等原始类型去设计DP public class Name { // 用final修饰属性 创建完即不可变 @Getter private final String name;
 
// 构造方法不对外暴露 private Name(String name) { this.name = name; }
// 使用静态方法创建对象 public static Name valueOf(String name) throws ValidationException { // 参数校验 if (name == null || name.isBlank()) { throw new ValidationException(“不能为空”); } if (!Pattern.compile(“[\u4e00-\u9fa5]+”).matcher(name).matches()) { throw new ValidationException(“姓名只能为中文!”); } // 其他省略
// 创建对象并返回 return new Name(name); } } 
// DP类应该定义行为方法 public class Address {
private final String address;
// 其他方法省略
// 一个获取省份的行为方法
public String getProvince(){
    return this.address.split("省")[0];
} } ``` * 隐性上下文显性化
* 拓展一个简单概念的上下文,将多个概念组合成一个独立的完整概念。 ```java // 钱这个概念其实隐性包含有币种这个概念,只是我们会将币种默认为人民币,但不代表其不存在 // 金额和币种才能完整的构成money这个概念,可以先将其定义,方便以后拓展。 public class Money {
private BigDecimal amount;
// 币种可以是一个DP或者一个枚举 private Currency currency; }
* 封装多对象行为(不再拓展)
    * 一个概念涉及多个对象之间的复杂业务逻辑,将其封装为 DP,简化原始代码。
## DP的使用
```java
// DP的使用
// 接口方法
User find(Name name);
User find(Phone phone);
User find(Name name, Phone phone);
// 类型不匹配,编译出错,及时发现问题
find(user.getPhone(), user.getName());
//测试用例
@Test
void testAddress() {
    String address = " ";
    // 创建一个Address对象即可。
    Address.valueOf(address);
}
** 一旦创建必然合法,定义的行为方法能保证安全使用,即完全可控,所有业务逻辑全部封装在类中。**
DP适合的场景
- 有格式限制的字符串:比如 Name,PhoneNumber,OrderNumber,ZipCode, Address 等。
 - 有限制的整数:比如 OrderId(>0),Percentage(0-100%),Quantity(>=0) 等。
 - 浮点数:一般用到的 Double 或 BigDecimal 都是有业务含义的,比如 Temperature、Money、Amount、ExchangeRate、Rating 等。
 - 复杂的数据结构:比如 Map 等,尽量能把 Map 的所有操作包装掉,仅暴露必要行为。
    
完整示例:
 
/**
 * 有范围长整型DP类基类 <br>
 *
 * @author kaikoo
 */
@EqualsAndHashCode(callSuper = true)
public abstract class RangedLong extends Number implements Type {
  private final long value;
  /**
   * @param value 数值
   * @param fieldName 字段名称
   * @param min 最小值
   * @param minInclusive 是否包含最小值
   * @param max 最大值
   * @param maxInclusive 是否包含最大值
   */
  protected RangedLong(
      long value,
      String fieldName,
      Long min,
      Boolean minInclusive,
      Long max,
      Boolean maxInclusive) {
    if (min != null) {
      var cmp = Long.compare(value, min);
      if ((minInclusive && cmp < 0) || (!minInclusive && cmp <= 0)) {
        throw IllegalArgumentExceptions.forMinValue(fieldName, min, minInclusive);
      }
    }
    if (max != null) {
      var cmp = Long.compare(value, max);
      if ((maxInclusive && cmp > 0) || (!maxInclusive && cmp >= 0)) {
        throw IllegalArgumentExceptions.forMaxValue(fieldName, max, maxInclusive);
      }
    }
    this.value = value;
  }
  @JsonValue
  public long getValue() {
    return this.value;
  }
  protected static long parseLong(Object o, String fieldName) {
    if (o == null) {
      throw IllegalArgumentExceptions.forIsNull(fieldName);
    } else if (o instanceof Long l) {
      return l;
    } else if (o instanceof String s) {
      try {
        return Long.parseLong(s);
      } catch (NumberFormatException e) {
        throw IllegalArgumentExceptions.forWrongPattern(fieldName);
      }
    }
    throw IllegalArgumentExceptions.forWrongClass(fieldName);
  }
  @Override
  public int intValue() {
    return (int) this.value;
  }
  @Override
  public long longValue() {
    return this.value;
  }
  @Override
  public float floatValue() {
    return (float) this.value;
  }
  @Override
  public double doubleValue() {
    return (double) this.value;
  }
}
/**
 * long类型Id
 *
 * @author kaikoo
 */
@EqualsAndHashCode(callSuper = true) // 重写EqualsAndHashCode
public class LongId extends RangedLong implements Identifier {
  protected LongId(long value, String fieldName) {
    super(value, fieldName, 0L, false, null, null);
  }
  @JsonCreator
  public static LongId of(long l) {
    return new LongId(l, "LongId");
  }
  public static LongId valueOf(Object o, String fieldName) {
    return new LongId(parseLong(o, fieldName), fieldName);
  }
  @Override
  public String identifier() {
    return String.valueOf(getValue());
  }
}
项目改造
- 确定概念,创建DP类,收集概念的所有相关业务逻辑和行为。
 - 替换所有创建和使用
 - 创建新接口
 - 修改外部调用
    
// controller层 @PostMapping("/user") public Boolean register(@RequestBody @Valid UserCreateCommand command) { // 做一些参数的非空校验 // 参数封装为DP对象 return registerService.register(Name.valueOf(command.getName()), Phone.valueOf(command.getPhone), Address.valueOf(command.getAddress())); } // service层 public User register(Name name, Phone phone, Address address) { // 不再需要参数校验逻辑,DP对象创建出来后就必然是合法的 // 业务逻辑 (其实可以移到 entity 的行为方法中) if(userRepo.find(name) != null){ throw new ValidationException("姓名不能重复"); } if(userRepo.find(phone) != null){ throw new ValidationException("手机号不能重复"); } // 省略其他业务逻辑 // 创建entity User user = User.builder().name(name).phone(phone).address(address).build(); // 调用持久层方法持久化 返回结果 return userRepo.save(user); } 
Entity 实体 业务模型
- 属于领域对象,业务层使用
 - 由DP组成
 - 生命周期存在内存中,不需要序列化
 - 拥有业务行为
 - 不对外开放属性的修改
 - 字段和方法与业务语言一致
 
DO作为数据模型的可靠性无法保障,创建了一个对象,这个对象无法被可靠的创建,且在整个周期内无法可靠的运行。因为暴露了构造方法和set方法,可以随意创建一个不符合业务规范的对象,且生存周期内可以随意通过set方法修改属性。
下面是一个entity的案例:
/**
 * 用户账户
 *
 * @author KaiKoo
 */
@JsonDeserialize(builder = Account.AccountBuilder.class) // 设置反序列化使用Builder
@EqualsAndHashCode(callSuper = true)
@Getter
@Builder
public class Account extends Entity<AccountId> {
  @Setter(AccessLevel.PROTECTED)
  private AccountId id;
  @DiffIgnore private Instant createTime;
  @DiffIgnore private Instant updateTime;
  private AccountState state;
  // 行为方式使用依赖反转
  public void save(AccountService accountService) {
    // handle
    if (this.id == null) {
      // 新增逻辑
      // 设置初始状态
      this.state = AccountState.of(AccountStateEnum.INIT);
    } else {
      // 更新逻辑
      if (!accountService.allowModify(this)) {
        throw new BusinessException("不允许修改");
      }
    }
    // save
    accountService.save(this);
  }
  // 行为方法,只能在entity内部修改属性
  public void invalidate() {
    // 领域都是合法的,可以忽略空指针问题。
    if (AccountStateEnum.ACTIVE.equals(this.state.getValue())) {
      this.state = AccountState.of(AccountStateEnum.TERMINATED);
    } else {
      throw new BusinessException("当前状态无法失效。");
    }
  }
}
总结
DP就是为一个有业务意义的字段创建一个类,并把相关的所有代码全写在这个类中,其实平常大家应该都有过这样的设计想法,只是Java Bean的影响太深了。
entity由DP组成,字段不需要和数据库一一对应,不需要持久化。