文章目录

  • 一、到底什么是DDD
  • 1、传统的MVC三层架构
  • 2、DDD到底解决了什么问题
  • 3、DDD四层架构
  • 4、为什么需要舍弃MVC而用DDD
  • 二、DDD改造实战
  • 1、充血模型
  • 2、避免大实体
  • 3、Dao改造
  • 4、构建防腐层
  • 5、抽象中间件
  • 6、使用领域服务,封装跨实体业务
  • 7、使用设计模式
  • 8、改造结果
  • 9、使用四层架构
  • 三、总结
  • 1、DDD与中台
  • 2、DDD的不足
  • 3、cola架构
  • 4、总结
  • 参考资料


一、到底什么是DDD

1、传统的MVC三层架构

以下是一个简单的转账案例(忽略代码的正确性):

@PostMapping("/pay")
public ResponseData pay(HttpServletRequest request)
{
    String requestPayStr = request.getParameter("pay");
    logger.info("获取支付请求参数 => " + requestPayStr);

    // 1.解密报文
    String decrypt = DecryptUtils.decrypt(requestPayStr);
    logger.info("解密后 => {}", decrypt);
    Pay pay = JSON.parseObject(decrypt, Pay.class);

    return payService.pay(pay);

}


public ResponseData pay(Pay pay)
{
    // 2.校验流水号,保证幂等
    Pay payPo = payMapper.selectById(pay.getId());
    if (payPo != null)
    {
        return ResponseData.fail("流水号重复");
    }

    // 3.校验余额
    Account fromAccount = accountMapper.selectById(pay.getFrom().getId());
    if (fromAccount.getAvaliable() < pay.getAmount())
    {
        return ResponseData.fail("余额不足");
    }

    // 4.风控校验
    if (riskCheckService.check(pay))
    {
        return ResponseData.fail("风控不通过");
    }

    // 5.记录流水
    payMapper.insert(pay);

    // 6.扣款
    fromAccount.setAvaliable(fromAccount.getAvaliable() - pay.getAmount());
    accountMapper.updateById(fromAccount);
    accountMapper.updateById(pay.getTo());


    // 7.通知
    noticeService.notice(pay);

    return ResponseData.success(null);

}

这种代码是不是再熟悉不过了?没错,这就是我们使用Springboot进行日常开发的代码。
乍一看是不是没什么问题?确实是没问题,每一步都是按照产品定义的标准,一步一步进行实现的。

2、DDD到底解决了什么问题

1、DDD首先解决的是:业务、产品、开发等等一系列人员之间沟通的困难性。
因为各个人员所具备的技术知识不同、对业务的理解也不一样,在描述同一个需求的时候,往往会因为一些不一致的观点进行长时间的讨论。
所以,需要在他们之间达成一个共识。这种通用语言就是领域

2、其次,DDD解决了“越来越复杂的老系统频繁迭代导致代码复杂度增加”的难题。
DDD领域驱动设计,旨在解决软件复杂性之道。一个庞大古老的系统,通常迭代起来非常困难,经历了非常多的程序员之手,东拼西凑写的五花八门,这些老代码谁都不知道是干嘛的,要想把它们梳理清楚需要大量的时间。如果要动主流程,可能一不小心就得重构。重构又是得将原来所有的老流程梳理明白。而DDD就是为了解决这种没完没了的重构。

DDD的本质就是:
解决架构变化过程中,很多业务无法梳理的问题。
并且让技术主动理解业务,避免过度设计并且将必要的部分提供扩展。

3、DDD四层架构

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_DDD

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_MVC_02


DDD统一基础概念:实体、领域服务、防腐层、仓库、工厂、值对象、聚合……

DDD四层架构规范:
1.领域层:Domain Layer:放之四海而皆准的理想。系统的核心,纯粹表达业务能力,不需要任何外部依赖。
2.应用层:Application Layer:理想与现实。协调领域对象,组织形成业务场景。只依赖于领域层。
3.用户层:User Interface:护城河。负责与用户进行交互。解释用户请求,返回用户响应。只依赖于应用层。
4.基础层:Infrastructure Layer:业务与数据分离。为领域层提供持久化机制,为其他层提供通用技术能力。

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_持久层_03

4、为什么需要舍弃MVC而用DDD

传统MVC可维护性差:大量的第三方模块影响核心代码稳定性。
传统MVC可拓展性差:业务逻辑与数据存储相互依赖,无法复用。
传统MVC可测试性差:庞大事务脚本与基础设施强耦合,无法单元测试。

最后业务多发生几次迭代后,这段代码就将成为一个可怕的黑洞。

所以,高质量应用就要求高内聚、低耦合。

MVC的思维就是按照业务写逻辑,而DDD的思维,更容易运用设计模式与设计原则。

二、DDD改造实战

1、充血模型

我们平常用的实体对象,通常定义核心的参数,设置get、set方法,在service中调用其方法进行属性赋值。。

而充血模型的实体对象,对其状态改变的操作都封装在实体对象中,描述了核心的业务能力。
实体中,应该包含引起它的状态发生变化的方法。并不是所有相关的方法全放在里面。(比如对金额转入转出操作适合放在实体中,比如说将帐号余额放到数据库就不适合放在实体中)

public class Account {

    private String id;

    // 余额
    private Integer avaliable;

    // 转出操作
    public void debit(Integer amount) {
        if (this.avaliable < amount) {
            throw new RuntimeException("余额不足");
        }
        this.avaliable -= amount;
    }
    // 转入操作
    public void credit(Integer amount) {
        this.avaliable += amount;
    }
}

系统能做什么事一目了然。

2、避免大实体

每一个实体的应用范围不要太大。

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_后端_04

3、Dao改造

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_封装_05

4、构建防腐层

构建防腐层,隔离外部服务。

对于外部服务,统一使用接口进行处理,并在实现类中进行额外的封装。

public class RiskCheckService implements IRiskCheckService{

    public Result check(Pay pay)
    {
        // 参数封装
        RiskCode riskCode = pay2RiskCode(pay);
        if (riskCode == RiskCode.RISK_CODE_NORMAL)
        {
            return Result.success();
        }
        // xxxxxxx
    }
}

5、抽象中间件

也算是防腐层,将MQ等中间件,也通过接口进行封装,而不是全都耦合在Service中。

public class RocketMQMessageService implements IMessageService{

    public Result send(Message message)
    {
        // 参数封装
        RocketMQMessage rocketMQMessage = message2RocketMQMessage(message);
        
        // ……
        
        // 发送消息
        rocketmqTemplate.send(rocketMQMessage);
        
        // ……
    }
}

6、使用领域服务,封装跨实体业务

多个实体的操作,使用领域服务进行额外封装。

public class AccountTransferService implements IAccountTransferService{
    
    public Result transfer(Account from, Account to, Integer amount)
    {
        if (from.getAvaliable() < amount)
        {
            return Result.fail("余额不足");
        }
        from.credit(amount);
        to.debit(amount);
        return Result.success("转账成功");
   }
}

7、使用设计模式

运用设计模式,对适当场景提供扩展。

8、改造结果

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_后端_06


改造之后,需求更容易梳理,业务逻辑纯净清晰,没有了业务逻辑与实现细节之间的复杂转换。

更容易单元测试,业务与基础设施隔离,没有基础设施,依然很容易设计单元测试案例。各个功能组件的依赖都是独立的,可以编写单元测试案例,单独测试。

更容易开发,领域内服务自治,不用担心其他模块的影响,下单模块的Account与账务模块的Account属性与方法都可以安全不同,没有任何直接关联。

技术容易更新,业务与数据隔离很清晰,改ORM技术只需要改仓库层实现,对业务无影响。

9、使用四层架构

└── demo
    ├── application    
    |   ├── assembler    # dto与do转换
    |   ├── dto    # 数据传输对象
    |   ├── event
    |   |   ├── publish  # 事件发布
    |   |   └── subscribe  # 事件订阅
    |   |
    |   └── service  
    |       ├── service     # 应用服务接口
    |       └── serviceImpl     # 应用服务实现
    |
    ├── domain    
    |   ├── aggregate1     
    |   |   ├── event    # 存放事件实体,以及事件的具体业务逻辑实现
    |   |   |   ├── publish    # 发布的具体逻辑(接口 + 实现)
    |   |   |   └── subscribe  
    |   |   |
    |   |   ├── model    
    |   |   |   ├── entity    # 实体
    |   |   |   ├── member    # 成员对象,相当于do
    |   |   |   └── do
    |   |   |
    |   |   ├── repository  
    |   |   |   ├── factory    # 工厂(po和do转换)
    |   |   |   └── facade    # 仓储接口
    |   |   |
    |   |   └── service    # 领域服务     
    |   |        ├── domainService    # 领域服务接口
    |   |        └── domainServiceImpl    # 领域服务实现
    |   |
    |   ├── aggregate2     # 聚合2
    |   └── aggregate3
    |
    ├── infrastructure    
    |   ├── config    # 全局配置
    |   ├── constant    # 常量
    |   ├── common    # 存放消息、数据库、缓存、文件、总线、网关、公用的常量、方法、枚举等
    |   ├── exception   # 自定义异常
    |   ├── jpa     # 持久层
    |   |   ├── assembler   
    |   |   ├── mapper   # 持久层
    |   |   ├── po   # 持久对象
    |   |   └── repository  # 仓储实现
    |   |       ├── mysql       # 仓储实现
    |   |       ├── redis       # 仓储实现
    |   |       └── post        # 仓储实现
    |   |
    |   ├── tool    # 全局id生成工具等
    |   └── util    # 工具
    |
    └── interfaces    
        ├── validator   # dto校验规则
        ├── handler   # 全局异常处理
        ├── assembler       # dto转为do
        ├── vo     # 视图模型(根据需求引入VO, PO, DTO, DO)
        └── facade    # 接口
└── demo2
    ├── application    
    |   ├── assembler    
    |   ├── dto    
    |   ├── event
    |   |   ├── publish  
    |   |   └── subscribe 
    |   |
    |   └── service     # 应用服务
    |       ├── context1     # 按限界上下文划分
    |       |   ├── service
    |       |   └── serviceImpl 
    |       |     
    |       ├── context2     
    |       └── context3     
    |       
    ├── domain    
    |   ├── aggregate1     
    |   |   ├── event    
    |   |   |   ├── publish    
    |   |   |   └── subscribe  
    |   |   |
    |   |   ├── model    
    |   |   |   ├── entity    
    |   |   |   ├── member    
    |   |   |   └── do
    |   |   |
    |   |   ├── repository  
    |   |   |   ├── factory    
    |   |   |   └── facade    
    |   |   |
    |   |   └── service     
    |   |        ├── domainService    
    |   |        └── domainServiceImpl    
    |   |
    |   ├── aggregate2     
    |   └── aggregate3
    |
    ├── infrastructure    
    |   ├── config    
    |   ├── constant    
    |   ├── common    
    |   ├── exception   
    |   ├── jpa     
    |   |   ├── assembler   
    |   |   ├── mapper   # 持久层
    |   |   ├── po   
    |   |   └── repository  # 仓储实现
    |   |       ├── mysql       
    |   |       ├── redis      
    |   |       └── post       
    |   |
    |   ├── tool    
    |   └── util   
    |
    └── interfaces   
       ├── context1     # 按限界上下文划分
       |   ├── validator   
       |   ├── handler   
       |   ├── controller     # 接口
       |   ├── vo     # dto、vo
       |   └── wrapper     # dto转do
       |
       ├── context2     
       └── context3

通常情况下执行逻辑(业务不复杂):controller -> 应用服务 -> 仓储 -> 持久层
业务复杂时:展现层 -> 应用服务 -> 领域服务 -> 仓储 -> 持久层
每层对应不同的对象(展现层vo、应用层dto、领域层do、持久层po),中间隔了dto是为了vo与do解耦
持久化操作应该放在基础层
member相当于do
按照DDD分层架构,仓储实现的部分应该属于基础层代码
仓储为了解耦领域逻辑和数据处理逻辑,在中间加了薄薄的一层仓储,相当于只是过渡的作用

三、总结

1、DDD与中台

DDD其实与中台的思想非常相似:

动手实践DDD领域驱动设计,DDD到底好不好用?真有那么神吗_持久层_07

2、DDD的不足

DDD并不是万能的银弹,映射到具体的业务场景时,DDD的理论体系也需要由模糊到清晰。

DDD缺乏一个规范的过程指导。
DDD没有万能的需求管理体系。
DDD并没有给出明确的领域建模方法。
对团队整体的技术能力要求较高。
DDD的学习成本很高。
直接指导DDD落地的框架非常少。
DDD是动态发展的,在不同的技术环境下,会有不同的表现形式。

3、cola架构

cola架构与DDD非常像,但是不是DDD架构,它主张清晰架构。

https://blog.51cto.com/u_16213578/7217553

4、总结

DDD的思想确实可以提高个人的视野,目前企业中基本也是小的、新的项目先试点,没有一套完整的体系结构。

日常开发中,也可以尝试去构建防腐层、中间件,虽然不需要严格的按照DDD的四层结构来开发,但是也可以进行适当的封装、提供扩展,也可以实现项目较好的扩展性。

参考资料

领域驱动设计(DDD)在爱奇艺打赏业务的实践