DDD(domain driven design)领域驱动设计模型

  • 一、DP(domain primitive)
  • 1、什么是DP
  • 2.为什么要用DP
  • 2.1 API接口清晰度
  • 2.2 数据验证和错误处理
  • 2.3 业务代码的清晰度
  • 3.DP原则
  • 3.1 将隐性的概念显性化
  • 3.2 将隐性的上下文显性化
  • 3.3 封装多对象行为
  • 4. DP与DTO
  • 5.DP使用场景
  • 二、DDD架构推演
  • 传统三层架构(UI、业务层、基础设施层)
  • 1.抽象数据存储层
  • 1.1 将data access层做抽象,降低系统对数据库的直接依赖
  • 方法
  • 概念区别
  • 2.抽象第三方服务
  • 2.1 防腐层ACL
  • 3.抽象中间件
  • 4.封装业务逻辑
  • 5.DDD架构图
  • 6.依赖关系图
  • 7.模型所在模块和转化器
  • 三、Repository
  • 1.change-tracking变更追踪
  • 四、领域层设计规范


一、DP(domain primitive)

1、什么是DP

dp是一种基本类型,包括type(数据类型)和class(类)。

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

}

如上例子,将于电话号相关的逻辑完整的收集到一个文件(class)中,形成了phoneNumber这个type

2.为什么要用DP

ddd java 落地 ddd java代码_ddd java 落地


案例分析

一个新应用在全国通过 地推业务员做推广,需要做一个用户注册系统,同时希望在用户注册后能够通过用户电话(先假设仅限座机)的地域(区号)对业务员发奖金。

传统代码:

public class User {
    Long userId;
    String name;
    String phone;
    String address;
    Long repId;
}

public class RegistrationServiceImpl implements RegistrationService {

    private SalesRepRepository salesRepRepo;
    private UserRepository userRepo;

    public User register(String name, String phone, String address) 
      throws ValidationException {
        // 校验逻辑
        if (name == null || name.length() == 0) {
            throw new ValidationException("name");
        }
        if (phone == null || !isValidPhoneNumber(phone)) {
            throw new ValidationException("phone");
        }
        // 此处省略address的校验逻辑

        // 取电话号里的区号,然后通过区号找到区域内的SalesRep
        String areaCode = null;
        String[] areas = new String[]{"0571", "021", "010"};
        for (int i = 0; i < phone.length(); i++) {
            String prefix = phone.substring(0, i);
            if (Arrays.asList(areas).contains(prefix)) {
                areaCode = prefix;
                break;
            }
        }
        SalesRep rep = salesRepRepo.findRep(areaCode);

        // 最后创建用户,落盘,然后返回
        User user = new User();
        user.name = name;
        user.phone = phone;
        user.address = address;
        if (rep != null) {
            user.repId = rep.repId;
        }

        return userRepo.save(user);
    }

    private boolean isValidPhoneNumber(String phone) {
        String pattern = "^0[1-9]{2,3}-?\\d{8}$";
        return phone.matches(pattern);
    }
}

DDD架构代码:

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

}


public class User {
    UserId userId;
    Name name;
    PhoneNumber phone;
    Address address;
    RepId repId;
}

public User register(
  @NotNull Name name,
  @NotNull PhoneNumber phone,
  @NotNull Address address
) {
    // 找到区域内的SalesRep
    SalesRep rep = salesRepRepo.findRep(phone.getAreaCode());

    // 最后创建用户,落盘,然后返回,这部分代码实际上也能用Builder解决
    User user = new User();
    user.name = name;
    user.phone = phone;
    user.address = address;
    if (rep != null) {
        user.repId = rep.repId;
    }

    return userRepo.saveUser(user);
}

2.1 API接口清晰度

调用时:

public User register(String, String, String)

传统代码可能会出现如下错误:

service.register("张三", "北京", "0731-88888888")

这个错误是编译无法发现的

DDD架构代码:

public User register(Name, PhoneNumber, Address);
service.register(new Name("张三"), new Address("北京"), new PhoneNumber("0731-88888888"))

2.2 数据验证和错误处理

public User register(
  @NotNull Name name,
  @NotNull PhoneNumber phone,
  @NotNull Address address
)

DP参数一定是正确的或null, 将数据验证前置到了调用方

2.3 业务代码的清晰度

DDD只剩下核心业务逻辑

3.DP原则

3.1 将隐性的概念显性化

3.2 将隐性的上下文显性化

3.3 封装多对象行为

4. DP与DTO

ddd java 落地 ddd java代码_ddd java 落地_02

5.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<String, List> 等,尽量能把 Map 的所有操作包装掉,仅暴露必要行为

二、DDD架构推演

传统三层架构(UI、业务层、基础设施层)

ddd java 落地 ddd java代码_User_03


缺点:高耦合、上层对下层强依赖

1.抽象数据存储层

1.1 将data access层做抽象,降低系统对数据库的直接依赖

方法
  • 新建实体类entity
    实体是拥有id的域对象,包括数据和行为。实体与数据存储格式无关,在设计中以该领域的通用严谨语言为依据。避免了其他业务逻辑和数据库的直接耦合,避免可当数据库字段变化时大量业务逻辑也跟着变的问题。
  • 新建对象储存接口类repository
    repository只负责entity的存储和读取,而repository的实现类实现数据库存储的细节。通过repository接口,底层数据库链接可以通过不同的实现类而替换。让业务逻辑不在面向数据库编程,而是面向领域模型编程。
概念区别
  1. DO和entity
    DO是单纯的和数据库表的映射关系,每个字段对应数据库表的一个column,这种对象叫Data Object。DO只有数据,没有行为。AccountDO的作用是对数据库做快速映射,避免直接在代码里写SQL。无论你用的是MyBatis还是Hibernate这种ORM,从数据库来的都应该先直接映射到DO上,但是代码里应该完全避免直接操作 DO。
    entity字段和数据库储存不需要有必然的联系。Entity包含数据,同时也应该包含行为。字段尽可能用 Domain Primitive 代替,可以避免大量的校验代码。
  2. DAO和repository
    DAO对应的是一个特定的数据库类型的操作,相当于SQL的封装。所有操作的对象都是DO类,所有接口都可以根据数据库实现的不同而改变。比如,insert 和 update 属于数据库专属的操作。
    Repository对应的是Entity对象读取储存的抽象,在接口层面做统一,不关注底层实现。比如,通过 save 保存一个Entity对象,但至于具体是 insert 还是 update 并不关心。Repository的具体实现类通过调用DAO来实现各种操作,通过Builder/Factory对象实现AccountDO 到 Account之间的转化

    repositoryImpl实现类,由于其职责被单一拎出来,因此只需要关注entity对象到DO之间的映射关系以及repository方法到DAO方法之间的映射。

2.抽象第三方服务

解决第三方不可控、入参出参强耦合的问题,最常用的设计模式是防腐层

2.1 防腐层ACL

防腐层原则是:外部一切不可信
作用:

  1. 适配器:外部数据、接口和协议可能不符合内部规范,将数据转化逻辑封装到ACL内部。
  2. 缓存:对于频繁调用且数据变更不频繁的外部依赖,通过在ACL里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入ACL,能够降低业务代码的复杂度。
  3. 兜底:如果外部依赖的稳定性较差,一个能够有效提升我们系统稳定性的策略是通过ACL起到兜底的作用,比如当外部依赖出问题后,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑一般都比较复杂,如果散落在核心业务代码中会很难维护,通过集中在ACL中,更加容易被测试和修改。
  4. 易于测试:类似于之前的Repository,ACL的接口类能够很容易的实现Mock或Stub,以便于单元测试。
  5. 功能开关:有些时候我们希望能在某些场景下开放或关闭某个接口的功能,或者让某个接口返回一个特定的值,我们可以在ACL配置功能开关来实现,而不会对真实业务代码造成影响。同时,使用功能开关也能让我们容易的实现Monkey测试,而不需要真正物理性的关闭外部依赖。
  6. ddd java 落地 ddd java代码_DDD架构_04


3.抽象中间件

ddd java 落地 ddd java代码_DDD架构_05

4.封装业务逻辑

  • 用Domain Primitive封装跟实体无关的无状态计算逻辑

  • 用Entity封装单对象的有状态的行为,包括业务校验

  • 用Domain Service封装多对象逻辑
  • ddd java 落地 ddd java代码_ddd java 落地_06

  • 经过重构后的代码有以下几个特征:
  • 业务逻辑清晰,数据存储和业务逻辑完全分隔。

  • Entity、Domain Primitive、Domain Service都是独立的对象,没有任何外部依赖,但是却包含了所有核心业务逻辑,可以单独完整测试。

  • 原有的TransferService不再包括任何计算逻辑,仅仅作为组件编排,所有逻辑均delegate到其他组件。这种仅包含Orchestration(编排)的服务叫做Application Service(应用服务)。
  • ddd java 落地 ddd java代码_User_07

整理后:

ddd java 落地 ddd java代码_DDD架构_08

  • 最底层不再是数据库,而是Entity、Domain Primitive和Domain Service。这些对象不依赖任何外部服务和框架,而是纯内存中的数据和操作。这些对象我们打包为Domain Layer(领域层)。领域层没有任何外部依赖关系。
  • 再其次的是负责组件编排的Application Service,但是这些服务仅仅依赖了一些抽象出来的ACL类和Repository类,而其具体实现类是通过依赖注入注进来的。Application Service、Repository、ACL等我们统称为Application Layer(应用层)。应用层 依赖 领域层,但不依赖具体实现。
  • 最后是ACL,Repository等的具体实现,这些实现通常依赖外部具体的技术实现和框架,所以统称为Infrastructure Layer(基础设施层)。Web框架里的对象如Controller之类的通常也属于基础设施层。

5.DDD架构图

ddd java 落地 ddd java代码_User_09

6.依赖关系图

ddd java 落地 ddd java代码_java_10

7.模型所在模块和转化器

ddd java 落地 ddd java代码_java_11

三、Repository

1.change-tracking变更追踪

repository是对聚合根进行操作,一个聚合根可能包括多个entity,当其中1个entity改变其他不改变时,需要找到这个改变的entity,即变更追踪。
有一下2个主流方法:
1.基于snapshot
当数据从DB里取出来后,在内存中保存一份snapshot,然后在数据写入时和snapshot比较。
2.基于proxy

四、领域层设计规范

传统OOP代码问题:

  1. 业务规则的归属到底是对象的“行为”还是独立的”规则对象“?
  2. 业务规则之间的关系如何处理?
  3. 通用“行为”应该如何复用和维护?