目录

领域驱动实战 | Domain Primitive 的简单使用

本文介绍一个实际项目中可以使用的概念: Domain Primitive.

一、前言

领域驱动这个概念也听到很多次了,书上看过的已经忘记,道听途说的不敢苟同(自己也分不清真假,听闻也没一个真正”成功“的案例),这里我就说一句我的理解:深入挖掘业务内核,找到问题的根本,把复杂问题分为多个领域(领域内聚)去解决业务实际的问题,而不是通过技术角度随着岁月留下一堆跳跃的代码.

二、本文主角 Domain Primitive

2.1 Domain Primitive 的定义

概念
  1. 不从任何其他事物发展而来
  2. 初级的形成或生长的早期阶段

让我们重新来定义一下 Domain Primitive :Domain Primitive 是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object .

• DP是一个传统意义上的Value Object,拥有Immutable的特性 • DP是一个完整的概念整体,拥有精准定义 • DP使用业务域中的原生语言 • DP可以是业务域的最小组成部分、也可以构建复杂组合

注意:Domain Primitive的概念和命名来自于Dan Bergh Johnsson & Daniel Deogun的书 Secure by Design.

2.2 使用 Domain Primitive 的三原则

• 让隐性的概念显性化 • 让隐性的上下文显性化 • 封装多对象行为

2.3 Domain Primitive 和 DDD 里 Value Object 的区别

在 DDD 中, Value Object 这个概念其实已经存在:

• 在 Evans 的 DDD 蓝皮书中,Value Object 更多的是一个非 Entity 的值对象 • 在Vernon的IDDD红皮书中,作者更多的关注了Value Object的Immutability、Equals方法、Factory方法等

Domain Primitive 是 Value Object 的进阶版,在原始 VO 的基础上要求每个 DP 拥有概念的整体,而不仅仅是值对象.在 VO 的 Immutable 基础上增加了 Validity 和行为.当然同样的要求无副作用(side-effect free).

▍Domain Primitive 和 Data Transfer Object (DTO) 的区别

在日常开发中经常会碰到的另一个数据结构是 DTO ,比如方法的入参和出参.DP 和 DTO 的区别如下:

DTO DP
功能 数据传输属于技术细节 业务领域中的概念
数据的关联 只是一堆数据放在一起不一定有关联度 数据之间的高相关新
行为 无行为 丰富的行为和业务逻辑

2.4 什么情况下应该用 Domain Primitive

常见的 DP 的使用场景包括:

• 有格式限制的 String:比如Name,PhoneNumber,OrderNumber,ZipCode,Address等 • 有限制的Integer:比如OrderId(>0),Percentage(0-100%),Quantity(>=0)等 • 可枚举的 int :比如 Status(一般不用Enum因为反序列化问题) • Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有业务含义的,比如 Temperature、Money、Amount、ExchangeRate、Rating 等 • 复杂的数据结构:比如 Map> 等,尽量能把 Map 的所有操作包装掉,仅暴露必要行为

三、项目的的尝试

举例子:

DP对象

 1@Getter
 2@JSONType(deserializer = NameDeserializer.class, serializer = NameSerializer.class)
 3public class Name implements Serializable {
 4    private static final long serialVersionUID = 7482387369308807214L;
 5
 6    private final String name;
 7
 8    public Name(final String name) {
 9
10        if (StringUtils.isBlank(name)) {
11            throw new ValidationException("名称不能为空!!!");
12        }
13
14        if (!isValid(name)) {
15            throw new ValidationException("仅支持1-64位大小写字母,数字,中划线和下划线组成,必须字母开头!!!");
16        }
17        this.name = name;
18    }
19
20    private static boolean isValid(String code) {
21        String pattern = "^[a-zA-Z][a-zA-Z0-9_-]{1,64}$";
22        return code.matches(pattern);
23    }
24}

领域方法

1    // 根据应用和Git信息查询流水线发布记录
2    public PipelineDO queryByAppAndGit(final Name appName, final Git git) {
3        // 进行逻辑处理
4    }

说明

  • final 修饰字段,不可变性的特点
  • 构造方法就直接校验合法性,不需要factory和validutil,更内聚自己的功能
  • 因为存在JSON序列化和反序列的情况,这里需要自定义序列化和反序列的方法

效果

  • 前端联调企图传随便的内容进来,直接通不过参数校验,就进不到核心方法了
  • 参数相对更能直观其意,并且进来的参数值取出来肯定是合法的
  • 代码可以写的更少

容易出错的地方

  • 如果用Map做一个缓存,Key放String,而实际对应的是Code对象,那么查询的时候要用code.genCodeString(),一开始容易写code忘记转换,导致查询不到结果
  • JSON转换在上面已经提到了需要自己定义扩展,会增加一定的重复代码量(我们暂时用的是每个DP类型两个转换类,可以合并到一个方法,根据类型判断,但是语义不够清晰

还没做好的地方

  • 如果通过jar包提供给第三方接口调用也使用这个DP类,那么接口调用方传参不合法的时候就会报错

四、参考

阿里云领域驱动DP文章