【DDD架构】

一、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

在这里插入图片描述
案例分析

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

传统代码:

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

在这里插入图片描述

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、业务层、基础设施层)

在这里插入图片描述
缺点:高耦合、上层对下层强依赖

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测试,而不需要真正物理性的关闭外部依赖。
    在这里插入图片描述

3.抽象中间件

在这里插入图片描述

4.封装业务逻辑

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

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

  • 用Domain Service封装多对象逻辑
    在这里插入图片描述
    经过重构后的代码有以下几个特征:

  • 业务逻辑清晰,数据存储和业务逻辑完全分隔。

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

  • 原有的TransferService不再包括任何计算逻辑,仅仅作为组件编排,所有逻辑均delegate到其他组件。这种仅包含Orchestration(编排)的服务叫做Application Service(应用服务)。
    在这里插入图片描述

整理后:
在这里插入图片描述

  • 最底层不再是数据库,而是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架构图

在这里插入图片描述

6.依赖关系图

在这里插入图片描述

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

在这里插入图片描述

三、Repository

1.change-tracking变更追踪

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

四、领域层设计规范

传统OOP代码问题:

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

声明:本文是个人学习笔记
参考自:
https://zhuanlan.zhihu.com/p/340911587
https://zhuanlan.zhihu.com/p/343388831
https://zhuanlan.zhihu.com/p/348706530
https://zhuanlan.zhihu.com/p/356518017

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nydia~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值