Skip to the content.

首页

DDD 之 Domain Primitive

不是讲架构,而是一种比较好用的开发思想。


Java Bean

问题

以前是被告知好处是使用getter和setter可以自行添加业务逻辑,但是慢慢共识已经变成不允许添加任何逻辑了,如果添加了可能造成不必要的bug,唯一的作用可能只是说控制某些属性的可见性和可修改性。


Data Object (DO)

DO应该如何设计和使用

DTO Assembler & Data Converter


简单的web三层架构

UI层(controller)、业务层(service、facade等)、基础设施层(mapper、repository等)

将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);
    }
}

现有的解决方案

根本问题就是:name、address、phone都是具有业务意义的概念,并不能简单的用原始类型去描述。


Java Primitive 原始类型

八大原始类型,以及对应的包装类、String 、BigDecimal、BigInteger、以及枚举等都可以视作 Java 语言和 Java Bean 的基础。

Domain Primitive 领域的基础

如何创建一个DP?

// 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适合的场景

/**
 * 有范围长整型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());
  }
}

项目改造

  1. 确定概念,创建DP类,收集概念的所有相关业务逻辑和行为。
  2. 替换所有创建和使用
  3. 创建新接口
  4. 修改外部调用
     // 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 实体 业务模型

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组成,字段不需要和数据库一一对应,不需要持久化。