王富贵

Stay hungry,Stay foolish

若你自认理性且正确,那更应该心平气和,言之有据地与人交流,即使不能立刻说服对方,但也至少埋下了一颗种子,冷嘲热讽或居高临下的态度,总会加大人与人之间的隔阂,最终只是在自己的小圈子中制造回声。愿我们最终都能成为乐观且包容的人,愿意耐心听取他人的苦衷和心声。
  menu
69 文章
0 浏览
0 当前访客
ღゝ◡╹)ノ❤️

领域驱动设计

1、领域驱动设计

驱动:我们可以理解为促进,促使的意思,那么领域驱动设计也就是,领域来促使软件系统架构设计

领域:我们可以将其理解为业务问题的范畴,领域可大可小,对应着大小问题的边界

简单的说,领域驱动设计就是将业务上要做的一件大事,通过推演和抽象,拆分成多个内聚的领域。而之所以称之为领域,是因为我们在设计时将其通用性非常好,能够适配不同的复杂的情况

举个例子:我做了个支付系统,这个支付系统不仅仅我们公司能用,其他公司搬过去也能用,或者说改造的成本很低。这个系统,可以说是在支付领域上的优秀系统。又比如若依,pig等热门的常见的权限管理系统,市面上很多公司项目都是基于他们项目进行开发的,因此他们也可以称之为权限管理领域的优秀系统

那么,如果某个项目在该领域上达到极点,甚至说所有的涉及该领域的需求都可以用他,那么这个项目通常也可以称之为框架

产品驱动设计?

与领域驱动设计概念相对的就是产品驱动设计,也就是说,针对我产品的需求,做crud来实现需求,就达成了 产品需求来促使软件系统架构设计条件。事实上,领域驱动设计的目的是提高产品的复用性或者是通用性,产品能够更低成本的二次开发、迭代和复用。 这样做的好处就是crud并不需要频繁修改,甚至迁移的时候做到不修改。

为什么要做领域驱动设计?

前面我们多次提到了,领域驱动设计的目标就是将产品做成一个领域内的,其通用性和复用性很高。以此来设计的产品具有二开便捷,复用性强的特点。适合于频繁维护迭代,领域推广复用等需求的产品。

深入理解领域

领域可大可小,而越小的领域驱动编程的产品,越好做。

举个例子:我们做一个支付领域的产品,支付领域下可以分为多个比他小的领域,比如说订单系统领域,分账领域,风控领域。。。。

领域越小的产品越好做,怎么理解呢?

我提出一个问题,创建对象领域的领域驱动产品设计的产品你知道吗?

我相信你其实能够想到,spring。我认为spring就是创建对象领域最优秀的产品,但凡是java中需要创建对象,都可以使用spring。这正如我们上面提到的,设计该领域的需求都可以用它,那么他就可以被称之为框架

而对于spring本身的领域而言,无非就是创建对象,将创建对象的控制权,交由spring来做。而我们自己做,通常来讲只需要使用 new 关键字或者使用反射即可。那么,创建对象这个领域,可以称之为是小之又小了。

领域驱动设计的关键

我们后面会提出很多领域驱动设计的模板,思想,套路,但请你记住,不一定要按部就班的实现,或者强行套用。这只是一种设计规范。但一定要遵循高内聚,低耦合的思想。ddd仅仅只是对你的一种指导。

1.1、Domain Primitive

第一章的内容放在demo01中

假设你现在需要做一个简单的数据统计系统,当地推销员输入客户的姓名和手机号。更具客户的手机号归属地和所属运营商,将客户群体分组。分配给相应的销售组,由销售组更近后续服务。

现在来做业务流程

输入用户名、手机号 -> 校验参数(不通过异常) -> 获取手机号的归属地和运营商编号 -> 获取分组编号 -> 构建客户对象,存入数据表wom

审查点:接口语义与参数校验

public User register(String name, String phone) throws ValidationException {
    // 参数校验
    if (name == null || name.length() == 0) {
        throw new ValidationException("name");
    }
    if (phone == null || !isValidPhoneNumber(phone)) {
        throw new ValidationException("phone");
    }

方法内部一开始对其内部进行了校验,这段代码被编译后,只会保留参数类型而不会保留参数名,如果代码被其他包集成,其他程序员可能不了解内部逻辑,则会颠倒内部顺序。此外,假设在未来系统开始支持通过用户名和身份证号进行注册,甚至更复杂的同时支持手机号和身份证号注册,此时我们就需要开新的接口来进行实现。

我们的目标就是使接口语义很明确,拓展性强一些,最好是可以进行一定的自检。

此外,对于参数校验而言,我们是封装在接口内部进行校验,如果校验逻辑发生变动,那么我们需要在对应的地方进行一一修改。此外,如果还有别的接口也需要使用校验,那么校验代码实际上就会重复。这显然不符合开闭原则和复用逻辑。

综合以上考虑,我们的修改目标如下:

  1. 接口语义明确,可拓展性强,最好带有自检性
  2. 参数校验逻辑复用,内聚
  3. 参数校验异常和业务逻辑异常解耦

因此,我们的改造是接口接收参数尽量使用自定义类型(实体类)接收,并且校验逻辑写在实体类内部封装。

1.1.1、修改效果

那么,效果如下

dp(pojo)

在PhoneNumber的内部,我们封装了对其校验的方法,以及获取其对应归属地等方法。将参数校验解耦。

public class PhoneNumber {
    private final String number;
    // 校验电话号码合法的正则表达式
    private final String pattern = "^0?[1-9]{2,3}-?\\d{8}$";

    // 仅存在含参构造器
    public PhoneNumber(String number) {
        if (number == null) {
            throw new ValidationException("number不能为空");
        } else if (isValid(number)) {
            throw new ValidationException("number格式错误");
        }
        this.number = number;
    }

    private boolean isValid(String number) {
        return number.matches(pattern);
    }

    // 获取区号
    public String getAreaCode() {
        //...
    }

    // 获取操作员id
    public String getOperatorCode(PhoneNumber phone) {
        //...
    }
}

service

在service层,我们需要做的仅仅是接收参数,并持久化保存在数据库中。其他校验均交由另外所属域进行聚合。

public class RegistrationServiceImpl implements RegistrationService {

    private SalesRepRepository salesRepRepo;
    private UserRepository userRepo;

    public User register(String name, PhoneNumber phone) {
  
        // 获取用户信息
        SalesRep rep = salesRepRepo.findRep(phone.getAreaCode(), phone.getOperatorCode());

        // 存储用户信息
        User user = new User();
        user.name = name;
        user.phone = phone;

        if (rep != null) {
            user.repId = rep.repId;
        }

        return userRepo.save(user);
    }
}

1.1.2、修订总结

上述的 PhoneNumber 类型就属于Domain Primitive。DP(Domain Primitive):实体类模型,且这些属性是无状态的,不同于普通的实体类,DP抽象并封装自检和一些隐性属性的计算逻辑,因此他拥有比普通模型更多的功能,属于充血模型。在DDD里,DP可以说是一切模型、方法、架构的基础。他是特定领域、拥有精确定义、可以自我验证、拥有行为的对象。可以认为是领域的最小组成部分。

DP实际上存在三条原则:

  1. 让隐性的概念显性化
  2. 让隐性的上下文显性化
  3. 封装多对象行为

第一条我们将号码所属的区号以及所属操作员使用方法的形式进行提供,将 PhoneNumber 隐性的概念显性化。这里我们提出一个问题,假如说区号以及操作员等需要远程调用或者接口实现,还在写DP合理吗?

第二条,我们的案例总并没有体现。但我们可以假设一个例子。全世界的手机号的区号分配是根据国际电信联盟的E.123和E.164标准分配的。那么所属的区号协议就是手机号的隐性上下文。

第三条,一个DP可以封装多个DP的行为。

1.2、Entity & Domain Service & Repository

如果我们需要对代码进行拓展

1. 上一节留下的问题,需要对手机号进行实名校验,实名信息通过调用外部服务获得。(假设目前由中国电信提供该服务)
2. 根据外部服务返回的实名信息,按照一定逻辑计算出用户标签,记录在用户账号中。
3. 根据用户标签为该用户开通相应等级的新客福利。

1

普通写法

public class RegistrationServiceImpl implements RegistrationService {

    private SalesRepMapper salesRepDAO;
    private UserMapper userDAO;
    private RewardMapper rewardDAO;
    private TelecomRealnameService telecomService;
    private RiskControlService riskControlService;

    public UserDO register(String name, PhoneNumber phone) {
        // 参数合法性校验已在PhoneNumber中处理
        // 参数一致性校验
        TelecomInfoDTO rnInfoDTO = telecomService.getRealnameInfo(phone.getNumber());
        if (!name.equals(rnInfoDTO.getName())) {
            throw new InvalidRealnameException();
        }
     
        // 计算用户标签
        String label = getLabel(rnInfoDTO);
        // 计算销售组
        String salesRepId = getSalesRepId(phone);
      
        // 构造User对象和Reward对象
        String idCard = rnInfoDTO.getIdCard();
        UserDO userDO = new UserDO(idCard, name, phone.getNumber(), label, salesRepId);
        RewardDO rewardDO = RewardDO(idCard, label);
      
        // 检查风控
        if(!riskControlService.check(idCard, label)) {
            userDO.setNew(true);
            rewardDO.setAvailable(false);
        }else {
            userDO.setNew(false);
            rewardDO.setAvailable(true);
        }
      
        // 存储信息
        rewardDAO.insert(rewardDO);
        return userDAO.insert(userDO);
    }
  
    private String getLabel(TelecomInfoDTO dto) {
        // 本地逻辑处理
    }
  
    private String getSalesRepId(PhoneNumber phone) {
        SalesRepDO repDO = salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());
        if (repDO != null) {
            return repDO.getRepId();
        }
        return null;
    }
}

1.2.1、Domain Service改造

我们需要抽象RPC调用检查实名认证,那么这个RPC实现的RealnameService就是Domain Service

public interface RealnameService {
    RealnameInfo get(PhoneNumber phone);
}

public class TelecomRealnameService implements RealnameService {

    @Override
    public RealnameInfo get(PhoneNumber phone){
        // RPC调用,并将返回结果封装为RealnameInfo
        // RealnameInfo是DP
    }
}
public class RegistrationServiceImpl implements RegistrationService {

    private SalesRepMapper salesRepDAO;
    private UserMapper userDAO;
    private RewardMapper rewardDAO;
    private RealnameService realnameService;
    private RiskControlService riskControlService;

    public UserDO register(String name, PhoneNumber phone) {
        // 一致性校验
        RealnameInfo realnameInfo = realnameService.get(phone);
        realnameInfo.check(name);
     
        // 计算标签信息
        String label = getLabel(realnameInfo);

        // 计算销售组
        String salesRepId = getSalesRepId(phone);

        // 构造对象
        String idCard = realnameInfo.getIdCard();
        UserDO userDO = new UserDO(idCard, name, phone.getNumber(), label, salesRepId);
        RewardDO rewardDO = RewardDO(idCard, label);
      
        // 检查风控
        if(!riskControlService.check(idCard, label)) {
            userDO.setFresh(true);
            rewardDO.setAvailable(false);
        }else {
            userDO.setFresh(false);
            rewardDO.setAvailable(true);
        }
      
        // 存储信息
        rewardDAO.insert(rewardDO);
        return userDAO.insert(userDO);
    }
  
    private String getLabel(RealnameInfo info) {
        // 本地逻辑处理
    }
  
    private String getSalesRepId(PhoneNumber phone) {
        SalesRepDO repDO = salesRepDAO.select(phone.getAreaCode(), phone.getOperatorCode());
        if (repDO != null) {
            return repDO.getRepId();
        }
        return null;
    }
}

1.2.2、Entity和Repository改造

再来抽象数据调用

// User Entity
public class User {
    // 用户id,DP
    private UserId userId;
    // 用户手机号,DP
    private PhoneNumber phone;
    // 用户标签,DP
    private Label label;
    // 绑定销售组ID,DP
    private SalesRepId salesRepId;
  
    private Boolean fresh = false;
  
    private SalesRepRepository salesRepRepository;
  
    // 构造方法
    public User(RealnameInfo info, name, PhoneNumber phone) {
        // 参数一致性校验,若校验失败,则check内抛出异常(DP的优点)
        info.check(name);
        initId(info);
        labelledAs(info);
        // 查询
        SalesRep salesRep = salesRepRepository.find(phone);
        this.salesRepId = salesRep.getRepId();
    }
  
    // 对this.userId赋值
    private void initId(RealnameInfo info) {
  
    }
  
    // 对this.label赋值
    private void labelledAs(RealnameInfo info) {
        // 本地处理逻辑
    }
  
    public void fresh() {
        this.fresh = true;
    }
}

使用Repository改变Entity的状态

public interface UserRepository {
    User find(UserId id);
    User find(PhoneNumber phone);
    User save(User user);
}

public class UserRepositoryImpl implements UserRepository {

    private UserMapper userDAO;

    private UserBuilder userBuilder;
  
    @Override
    public User find(UserId id) {
        UserDO userDO = userDAO.selectById(id.value());
        return userBuilder.parseUser(userDO);
    }

    @Override
    public User find(PhoneNumber phone) {
        UserDO userDO = userDAO.selectByPhone(phone.getNumber());
        return userBuilder.parseUser(userDO);
    }

    @Override
    public User save(User user) {
        UserDO userDO = userBuilder.fromUser(user);
        if (userDO.getId() == null) {
            userDAO.insert(userDO);
        } else {
            userDAO.update(userDO);
        }
        return userBuilder.parseUser(userDO);
    }
}

最终呈现的结果

public class RegistrationServiceImpl implements RegistrationService {
    private UserRepository userRepository;
    private RewardRepository rewardRepository;
    private RealnameService realnameService;
    private RiskControlService riskControlService;

    public UserDO register(String name, PhoneNumber phone) {
        // 查询实名信息
        RealnameInfo realnameInfo = realnameService.get(phone);

        // 构造对象
        User user = new User(realnameInfo, phone);
        Reward reward = Reward(user); //发奖逻辑
      
        // 检查风控
        if(!riskControlService.check(user)) {
            user.fresh();
            reward.inavailable();
        }
      
        // 存储信息
        rewardRepository.save(reward);
        return UserRepository.save(user);
    }
}

1.2.3、抽调风控DS

我们认为发奖也属于风控上下文的内容

抽象风控的Domain Service

public interface CheckUserService(User user) {
    void check(user);
}

public class CheckAndUpdateUserServiceImpl(User user) {
    private RiskControlService riskControlService;
    private RewardRepository rewardRepository;
  
    @Override
    public void check(User user) {
        rewardCheck(user);
        // ...
        // 可能存在的其他逻辑
    }
  
    private void rewardCheck(User user) {
        Reward reward = Reward(user);
        // 检查风控
        if(!riskControlService.check(user)) {
            user.fresh();
            reward.inavailable();
        }
        rewardRepository.save(reward);
    }
}

最终的成果

public class RegistrationServiceImpl implements RegistrationService {

    private UserRepository userRepository;
    private RealnameService realnameService;
    private CheckUserService checkUserService;

    public UserDO register(String name, PhoneNumber phone) {
        // 查询信息
        RealnameInfo realnameInfo = realnameService.get(phone);

        // 构造对象
        User user = new User(realnameInfo, phone);
      
        // 检查风控并更新对象
        checkUserService.check(user);
      
        // 存储信息
        return userRepository.save(user);
    }
}

1.2.4、总结

DP(Domain Primitive):抽象并封装自检和一些隐性属性的计算逻辑,且这些属性是无状态的。一般用于接收参数。DP是充血模型。

Entity:抽象并封装单对象有状态的逻辑。pojo这种实体类的聚合,Entity也可以是充血模型。

Domain Service:抽象并封装多对象的有状态逻辑。主要是需要使用RPC调用或者其他内存逻辑操作。

Repository:抽象并封装外部访问逻辑。也类似于Service,主要是对Entity和数据库查询的聚合。

领域驱动设计业务的指导过程:

  1. 首先对处理的业务问题进行总览
  2. 然后领域对象(Entity)进行划分,明确每个领域对象包含的信息和职责边界。并进行夸对象,多对象的逻辑组织(Domain Service)
  3. 接着在上层应用中更具业务描述去编排Entity和Domain Service
  4. 最后再做一些下水道工作,对下层数据访问,RPC调用一些实现

就DP和Entity而言,他们的区别如下

  1. DP是无状态的属性,充血模型
  2. Entity是有状态的属性,充血模型

简单举个例子:

Entity:观众通过特定程序购买了某个座位的票,这种情况下座位就是有状态的,那么他是Entity,我们就需要关注这个座位而状态。比如通过唯一ID来追踪该对象。关注其价格、预订和位置等属性。

DP:另外一种就是观众买票后进场可以随便坐,这个时候座位就是无状态的模型,我们也不需要关注其状态,此时他就是DP模型。

那么实际上我们的最终的出口需要做两件事情

  • 解耦外部依赖

    • 数据访问
    • RPC调用
  • 解耦内部逻辑

1.3、统一语言和模型价值

统一语言:

统一语言,试想一下,如果我在上文没有对两种理由加以区别,说明肯定会有一部分人会混淆概念,一旦自己的知识产生前后矛盾,轻则需要会去网上查资料,揣摩理解,重则在脑中形成浆糊,直接宕机,类似的现象在实际工作中很常见,在建模过程中,不同岗位,不同角色的人,务必对概念形成一致的认识,只有以此为基础的沟通,才是有效的,这在ddd中被称为统一语言。

其实常被人们吐槽的阿里黑话中的拉通、对齐,就是这个含义,举个例子,假如你在做互联网金融相关领域的业务,金融背景的同事往往认为一些概念是常识,比如复试记账法中的借贷,但是技术背景的你对它的理解却很容易产生偏差,甚至你们将他完全理解成了两种不同的事物,一旦这种信息差产生,没有及时消除,那么在项目后续的迭代中,建模将会越来越困难。甚至举步维艰,甚至完全推翻,所以好的模型现金这句话我觉得不是说说而已。

模型价值:

那什么是模型,模型的价值究竟在哪,举一个比较直观的例子,星盘,是由古希腊的天文学家发明的,在中世纪伊斯兰科学家又对他进了改进,新盘可旋转的铜环代表着各个恒星在天体上的位置,刻有当地地平坐标系的盘面是可替换的,它代表了不同维度的星空景象,在新盘的盘面旋转铜环,还可以计算出全年任何时刻的天体位置,相似的,如果知道某个恒星在天体上的位置,也可以因此计算出时间,在这里星盘使用机械的方式,建立了,恒星、星空和时间的面向对象模型,在这个案例下,你也可以使用软件来实现,这个新模型,但首先需要想了解天文的专家学习,天文里面的知识,然后建模,如果你的模型是良好的,那么就不能因为更改了坐标系而失效,也不能因为新加入了一颗恒星就失效,领域驱动设计就是一套方法论,去帮助我们建设良好的模型,在平时的工作中,想要建设良好的型其实是困难重重的。回头看,领域驱动设计就是我们需要面向某个领域进行建模构建架构,不能因为某些属于领域内的改动而使我们的程序不能正常运转。也就是说,我们的架构需要满足领域内所有的需求,其适配性或者叫通用性比较好。

是否需要建模:

业务简单业务复杂业务非常复杂
可能的情况无需投入,若业务深耕,需要引入良好的模型考研产品经理能否兼任领域专家分工明确,关注领域信息
模型价值

对于复杂业务来说,公司会聘请各个领域的专家,有的领域专家和开发人员比较密切,比如一起参与评审需求、参加会议,也愿意互相沟通建模相关的事务,但大多数情况下还是会依赖产品经理作为中间磨合层。事实上,公司一大,人员非常多,很难做到扁平化管理。此时各个层级的沟通,各个职位的人在沟通的时候,就需要尽量少的丢失建模的关键信息。

1.4、聚合根和限界上下文

假如你是用户,用户绑定了银行卡和电话号码。那么实际上用户引用了银行卡和电话号码等对象。假如银行卡和对象号码存在引用关系,在用户注销账号的时候,银行卡与电话号码的关系是否需要删除呢?我们应该如何描述他们的关系呢?

聚合(Aggregate)是对存在引用关系的一组对象的封装。其中,根对象是唯一能够被外部引用的对象。聚合暴露的接口,只允许操作根对象。根对象是一个Entity,因为每个聚合都需要ID和状态来区分其他聚合,通过根对象ID来代表整个聚合的ID。而聚合的边界,就是你判断哪些对象可以放入当前聚合的条件。

2

对于聚合而言,就是当对根进行操作的时候,其他对象是否是相关联的。以此来确定聚合的边界。

举个例子

3

我们简单了解一个聚合根的实现

// Entity标识,每个Entity需要一个标记,Identifier接口用于标识这个Entity是可以作为聚合根标识
public interface Identifier extends Serializable {
}

// Entity都需要是标识的父类
public interface Entity<T extends Identifier> {

}

// PhoneNumber是一个标识
public class PhoneNumber implements Identifier {
  
}

// 不是所有的DP都作为标识的
public class Red {
    private String desc;
    private String rgb;
    // ...
}

// 聚合根是一个Entity,所以必须继承Identifier标识符,并且他需要继承Entity
public interface AggregateRoot<T extends Identifier> extends Entity<T>{
  
}

// 微信账户作为一个聚合根,泛型采用PhoneNumber座位微信账户的全局标识
public class WechatAccount implements AggregateRoot<PhoneNumber> {
  
}

1.4.1、转账聚合根(DS)

假设我们需要做转账业务,我们需要定义余额、钱包、微信账号,他们就转账业务是一个聚合

// 余额
public class Balance implements Entity<BalanceNumber> {
    private BalanceNumber id;
    //...
    private BalanceNumber() {}
}

// 钱包
public class Wallet implements Entity<WalletNumber> {
    private WalletNumber id;
    private Balance balance; // 余额
    private Statement statement; // 账单明细
    //...
    private Wallet() {}
}

// 微信账号,聚合根
public class WechatAccount implements AggregateRoot<PhoneNumber> {
    private PhoneNumber id;
    private Nickname nickname; 
    private Wallet wallet;
    //...
    private WechatAccount() {}
}

1.4.1.1、Domain Service

// domain service
public class TransferServiceImpl {
    public AccountRepository repository;
  
    @Override
    public void transfer(WechatAccount payer, WechatAccount payee, Asset asset) {
        payer.withdraw(asset);
        payee.deposit(asset);
        repository.save(payer);
        repository.save(payee);
   }
}

我们之前了解到,WechatAccount是聚合的聚合根,因此,我们只能操作WechatAccount来打到操作聚合的目标。

public class WechatAccount implements AggregateRoot<PhoneNumber> {
    private PhoneNumber id;
    private Nickname nickname; 
    private Wallet wallet;
    //...
    private WechatAccount() {}
  
    // 支出
    public void withdraw(Asset asset) {
        wallet.pay(asset);
    }
  
    // 存储,存款
    public void deposit(Asset asset) {
        wallet. receive(asset);
    }
}

// 钱包,需要耦合到聚合根中
public class Wallet implements Entity<WalletNumber> {
    private WalletNumber id;
    private Balance balance; // 余额
    private Statement statement; // 账单明细
    //...
    private Wallet() {}
  
    public void pay throws BalanceException(Asset asset) {
        balance.decrease(asset.getBalance);
        statement.add(asset);
    }
  
    public void receive throws BalanceException(Asset asset) {
        balance.increase(asset.getBalance);
        statement.add(asset);
    }
  
} 

1.4.2、数据层访问

我们前面提到了数据层访问需要使用Repository,因此Repository需要做以下改造

Responsitory的泛型有两,一个是聚合根,聚合根下引用了聚合根标识,这个标识应该是标识接口的父类

public interface Repository<T extends AggregateRoot<ID>, ID extends Identifier> {
    save(T t);
    WechatAccount find(ID id);
}

接下来我们来定义微信的Repository,其聚合根是WechatAccount(微信账户),聚合根标识是PhoneNumber(手机号)

public interface AccountRepository extends Repository<WechatAccount, PhoneNumber> {
    // save就是通过聚合根持久化所有聚合的对象
    save(WechatAccount wechatAccount);
    // find是通过手机号查找整个聚合
    WechatAccount find(PhoneNumber phoneNumber)
}

我们再次回顾概念,Entity是对所有有状态属性的聚合,所以Entity可能对PO而言是一对多的关系

接下来我们对AccountRepository进行实现

public class WeAccountRepositoryImpl implements AccountRepository {
    private WechatAccountDAO wechatAccountDAO;
    private WalletDAO  walletDAO;
    private BalanceDAO balanceDAO;
    private StatementDAO statementDAO;
  
    // save就是通过聚合根持久化所有聚合的对象
    @Override
    public save(WechatAccount wechatAccount) {
        WechatAccountPO wechatAccountPO = WechatAccountConverter.convert(wechatAccount);
        wechatAccountDAO.save(wechatAccountPO);
      
        Wallet wallet = WalletFactory.obtain(wechatAccount);
        WalletPO walletPO = walletConverter.convert(wallet);
        walletDAO.save(walletPO);
      
        Balance balance = BalanceFactory.obtain(wallet);
        BalancePO balancePO = balanceConverter.convert(balance);
        balanceDAO.save(balancePO);
      
        Statement statement = StatementFactory.obtain(wallet);
        StatementPO statementPO = statementConverter.convert(statement);
        statementDAO.save(statementPO);
      
        //... 其他WechatAccount的属性
    }
  
    // find是通过手机号查找整个聚合
    @Override
    WechatAccount find(PhoneNumber phoneNumber) {
        //...
    }
}

实际上,这样子写我们会将Repository中所有对象的属性全部保存一遍,而那些没有被修改的属性保存非常浪费性能。所以上述代码非常具有优化价值,这也称之为Change-tracking问题。

1.4.3、聚合根总结

对于聚合根而言,聚合根就是一个根Entity,包含内部所有的子Entity。我们需要确定其根Entity到底是谁。根Entity内部会有一个唯一标识叫做聚合根标识,用于标注他是哪一个聚合根。除此之外,我们还需要确定聚合的边界,也就是那些Entity是属于该聚合的。

聚合属性需要确认

  • 聚合根
  • 边界

在使用上,运行时我们需要使用Domain Service来对聚合根进行对应的操作。而对聚合根的操作需要持久化,那么Repository应运而生。我们知道Repository是下水道操作,你可以随意操作。但是对于聚合根的Repository,聚合根会拥有很多Entity,因此我们会封装对所有Entity的持久化。但是假如部分Entity并未发生改变,也去持久化就非常浪费性能。这也称之为Change-tracking问题。对于这个问题的解决我们可以引入版本号,采用AOP的方式编写一个框架。

使用

  • 运行时:domain service
  • 持久化:Repository(change-tracking问题)

1.4.4、限界上下文

Bounded Context限界上下文,本质是为了解决复杂系统的领域分治问题。这不只是ddd架构方式所需要解决的,其他任何的架构方式都需要解决类似的问题。领域分治最简单的解决方案就是通过拆分微服务。而拆分微服务本身也存在边界问题,而微服务本身也存在通信问题。

因此在分布式系统中如何解决分治是非常重要的问题。

1.5、实战

我们简单回顾一下领域驱动设计的过程(大致过程,不定):

  1. 首先对处理的业务问题进行总览
  2. 然后领域对象(Entity)进行划分,明确每个领域对象包含的信息和职责边界。并进行跨对象,多对象的逻辑组织(Domain Service)
  3. 接着在上层应用中更具业务描述去编排 EntityDomain Service
  4. 最后再做一些下水道工作,对下层数据访问,RPC调用一些实现

下水道工作是指需要系统外部依赖的组件,比如数据库,RPC接口,当我们需要替换数据库或者请求别的接口的时候,如果我们在上层应用层实现他们,那么在这些依赖发生改变,如RPC接口调用其他公司的,数据库换成Oricon,就需要修改应用层代码。并且我们需要抽象成接口依赖,注入接口自动装配实现来达到防腐的木点。来避免下游变动导致的应用层变动。

接口属于防腐层,因为他不经常改动;实现属于腐烂层,因为数据库,RPC等等可能经常变动,甚至技术栈出现变动。


标题:领域驱动设计
作者:1938857445
地址:https://www.lmlx66.top/articles/2022/11/26/1669436035810.html