Redian新闻
>
DDD的精髓

DDD的精髓

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:技术琐话

你可以,不代表你应该。

(Just because you can, doesn’t mean you should.)

——施莉琳•凯尼恩

在第6章中,我们简要介绍了什么是模型、模型在软件开发中的重要性,以及一些常用的建模方式在软件工程中的应用。本章将重点讲解领域驱动设计(Domain Driven Design,DDD),包括DDD的重要概念,以及如何进行领域建模。

7.1 什么是DDD

DDD是Eric Evans在2003年出版的《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一书中提出的具有划时代意义的重要概念,是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

DDD的革命性在于领域驱动设计是面向对象分析的方法论,它可以利用面向对象的特性(封装、多态)有效地化解复杂性,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据。这些数据对象除了简单的setter/getter方法外,不包含任何业务逻辑,业务逻辑都是以过程式的代码写在Service中。这种方式极易上手,但随着业务的发展,系统也很容易变得混乱复杂。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

7.2 初步体验DDD

在介绍DDD之前,我喜欢用这个银行转账的案例来做一个DDD和事务脚本(Transaction Script)的简单对比。我们要实现一个银行转账的功能,如果用传统的事务脚本方式实现,业务逻辑通常会被写在MoneyTransferService中,而Account仅仅是getters和setters的数据结构,也就是所谓的“贫血模式”。其代码如下所示:

public class MoneyTransferServiceTransactionScriptImpl
      implements MoneyTransferService 
{
  private AccountDao accountDao;
  private BankingTransactionRepository bankingTransactionRepository;
  . . .
  @Override
  public BankingTransaction transfer(
      String fromAccountId, String toAccountId, double amount)
 
{
    Account fromAccount = accountDao.findById(fromAccountId);
    Account toAccount = accountDao.findById(toAccountId);
    . . .
    double newBalance = fromAccount.getBalance() - amount;
    switch (fromAccount.getOverdraftPolicy()) {
    case NEVER:
      if (newBalance < 0) {
        throw new DebitException("Insufficient funds");
      }
      break;
    case ALLOWED:
      if (newBalance < -limit) {
        throw new DebitException(
            "Overdraft limit (of " + limit +") exceeded: " + newBalance);
      }
      break;
    }
    fromAccount.setBalance(newBalance);
    toAccount.setBalance(toAccount.getBalance() + amount);
    BankingTransaction moneyTransferTransaction =
        new MoneyTranferTransaction(fromAccountId,toAccountId,amount);
    bankingTransactionRepository.addTransaction(moneyTransferTransaction);
    return moneyTransferTransaction;
  }}

上述代码有些读者可能会比较眼熟,因为大部分系统都是这么写的。评审完需求,工程师画几张UML图完成设计,就开始像上面这样写业务代码了,这样写基本不用太动脑筋,完全是过程式的代码风格。

同样的业务逻辑,接下来看使用领域建模是怎么做的。在使用DDD之后,Account实体除账号属性之外,还包含了行为和业务逻辑,比如debit()和credit()方法。

public class Account {
  private String id;
  private double balance;
  private OverdraftPolicy overdraftPolicy;
  . . .
  public double balance() return balance; }
  public void debit(double amount) {
    this.overdraftPolicy.preDebit(this, amount);
    this.balance = this.balance - amount;
    this.overdraftPolicy.postDebit(this, amount);
  }
  public void credit(double amount) {
    this.balance = this.balance + amount;
  }}

透支策略OverdraftPolicy也不仅仅是一个Enum了,而是被抽象成包含业务规则并采用策略模式的对象。

public interface OverdraftPolicy {
  void preDebit(Account account, double amount);
  void postDebit(Account account, double amount);}public class NoOverdraftAllowed implements OverdraftPolicy {
  public void preDebit(Account account, double amount) {
    double newBalance = account.balance() - amount;
    if (newBalance < 0) {
      throw new DebitException("Insufficient funds");
    }
  }
  public void postDebit(Account account, double amount) {
  }}public class LimitedOverdraft implements OverdraftPolicy {
  private double limit;
  . . .
  public void preDebit(Account account, double amount) {
    double newBalance = account.balance() - amount;
    if (newBalance < -limit) {
      throw new DebitException(
          "Overdraft limit (of " + limit + ") exceeded: "+newBalance);
    }
  }
  public void postDebit(Account account, double amount) {
  }}

而Domain Service只需要调用Domain Entity对象完成业务逻辑。

public class MoneyTransferServiceDomainModelImpl
      implements MoneyTransferService 
{
  private AccountRepository accountRepository;
  private BankingTransactionRepository bankingTransactionRepository;
  . . .
  @Override
  public BankingTransaction transfer(
      String fromAccountId, String toAccountId, double amount)
 
{
    Account fromAccount = accountRepository.findById(fromAccountId);
    Account toAccount = accountRepository.findById(toAccountId);
    . . .
    fromAccount.debit(amount);
    toAccount.credit(amount);
    BankingTransaction moneyTransferTransaction =
        new MoneyTranferTransaction(fromAccountId,toAccountId,amount);
    bankingTransactionRepository.addTransaction(moneyTransferTransaction);
    return moneyTransferTransaction;
  }}

通过DDD重构后,虽然类的数量比以前多了一些,但是每个类的职责更加单一,代码的可读性和可扩展性也随之提高。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

7.3 数据驱动和领域驱动

7.3.1 数据驱动

目前主流的开发模式是由数据驱动的。数据驱动的开发很容易上手,

有了业务需求,创建数据库表,然后编写业务逻辑,开发过程如图7-1所示。数据驱动以数据库为中心,其中最重要的设计是数据模型,但随着业务的增长和项目的推进,软件开发和维护的难度会急剧增加。

图7-1 数据驱动研发过程

以客户关系管理(Customer Relationship Management,CRM)为例,其中很重要的概念有销售、机会、客户、私海、公海,实体的定义分别如下。

  • 销售(Sales):公司的销售人员,一个销售可以拥有多个销售机会。
  • 机会(Opportunity):销售机会,每个机会包含至少一个客户信息,且归属于一个销售人员。
  • 客户(Customer):客户,也就是销售的对象。
  • 私海(Private sea):专属于某个销售人员的领地(Territory),私海里面的客户,其他销售人员不能触碰。
  • 公海(Public sea):公共的领地,所有销售人员都可以从公海里捡入客户到其私海。

按照我们曾经学习的数据库建模理论,对于上面的场景,不难画出图7-2所示的实体联系(Entity Relationship,ER)图。

图7-2 CRM的ER图

可以看到,图7-2所示的ER图中不存在公海和私海,因为所谓的机会在私海,就是这个机会是不是归属某个销售,这样我们只需要看机会上是否有salesId。如果有,说明机会被某个销售占有,也就是在私海中;反之,这个机会就在公海中。

在这种开发模式下,最后的产出是几张数据库表,以及针对表中数据进行操作的事务脚本,如图7-3所示。

图7-3 事务脚本实现

7.3.2 领域驱动

领域驱动设计关心的是业务中的领域划分(战略设计)和领域建模(战术设计),其开发过程不再以数据模型为起点,而是以领域模型为出发点,研发过程如图7-4所示。领域模型对应的是业务实体,在程序中主要表现为类、聚合根和值对象,它更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系。 这是“领域驱动设计”和“数据驱动设计”之间显著的区别。

图7-4 领域驱动研发过程

仍以上面的CRM为例。假如我们先不考虑数据模型,而是采用面向对象分析(Object Oriented Analysis,OOA)对这个场景进行领域建模,那么可以得到图7-5所示的领域模型。

图7-5 CRM的领域模型

可以看到,在图7-5中,领域模型的描述更加贴近业务,一些重要的业务术语和概念没有丢失,更完整地表达了业务语义。即使是产品经理或者业务人员,也不难看懂这样的领域模型,甚至他们可以和技术人员一起参与到梳理领域模型和创建活动中来。

通过DDD的战略设计和战术设计,我们可以为问题域划分出合适的子域,并对域中的业务进行建模。图7-6所示是我们在实际工作中为CRM进行的领域战略设计。

图7-6 CRM的领域划分

7.3.3 ORM

很明显,领域模型和数据模型并不是一一对应的关系,但也不排除,有些情况领域模型和数据模型是趋同的,但是大部分情况都需要做一层映射(Mapping)。为了弥补二者之间的差异,行业先驱们做了很多关于映射工作的尝试,这种技术有一个名称叫作对象关系映射(Object Relationship Mapping,ORM),如图7-7所示。

图7-7 对象关系映射

ORM曾经非常火,记得当年Hibernate才出现时,我用尽了其中的高级技巧,比如继承关系映射、多对多关系映射……结果弄出来的东西却变成了“四不像”,既不像Entity,也不像数据对象(Data Object,DO)。

ORM的问题在于它太理想化,期望通过工具把数据建模和领域建模合一,这样的尝试注定是很难成功的。仍以上述的CRM案例为例,在数据模型中根本就没有私海和公海这两个实体,工具是无法映射的。因此,Hibernate和JPA的衰落是可以预见的。现在使用最多的是MyBatis,它很简单,完全不理会复杂的关系和对象之间的复杂关系映射,只做数据库表和DO之间的简单映射。

复杂的数据库关系和对象关系之间的差异,其本质是数据模型和领域模型之间的差异,而这种差异的多样性和灵活性是很难通过规则预先定义的,这也是为什么工具的作用会很有限。现在的互联网大厂大多使用MyBatis,原因也在于此。因此,如果你打算实践DDD,请一定不要让工具帮你去建模,工具不会抽象,也不会思考,还是要老老实实自己动手去建。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
时尚圈都在讨论的“静奢风”,精髓到底是什么?230524 体检的精髓(二)领域驱动设计DDD|从入门到代码实践《我的父亲是流亡学生》: 25. 河南梆子在越南2023年回国纪实之二——回家途中老板:给我按 DDD 设计这个新项目~出事的陈师兄北上广CBD的精英,人前有多装,人后就有多疲惫“现在的街头标语有多硬核!”没有废话全是精髓...【收藏】【最新DDL】24暑期实习申请全开!四大暑期实习DDL最全盘点!高德信息业务DDD实战 - 聊聊用领域重构胶水代码每个来我家的朋友都有同款!收纳的精髓也不过如此嘛!如果狗狗变成人类会怎么跑步?小哥抓到各个角色的精髓了!火星乐园第三部《灰界》第十八章 信心价值学会海外拼dd(Temu)才发现:靠海外PDD生活,普通人也可以吉利再练小号:华为智选车的精髓,被这个新品牌掌握了大妈是一种威武的存在多巴胺穿搭到底怎么火的啊?我悟到了精髓…干掉复杂代码 — DDD 与 CQRS 才是黄金组合“现在的街头标语有多硬核?”网友:没有废话全是精髓哈哈哈申请准备了这么久,来当一回招生官如何?和前杜克招生官一起审理申请,探索美本招生精髓!一手烂牌都能打成王炸,鞠学精髓除了换头还有啥?华邦首度公布Chiplet介面计划,看好DDR3 DDR4后市趋势只会 CRUD?滴滴这 DDD 架构建设方案太强了!支持 iOS/Android/网页多种终端贵州美食的精髓,全藏在这个又仙又野的县城里!这才是美哭我的中国式侘寂风,东方美学精髓拿捏稳了!【居住榜样】一文彻底吃透 DDD 最全建模落地方法论!(附实例)| 极客时间Desktop(i5-7400/strix b250h/16g ddr4/256g ssd/2t hdd)不同MBTI的人怎么做毕业演讲?小哥模仿到精髓了……从DDR4到DDR5发生了什么?「查缺补漏」,DDD 核心概念梳理DDD 对决:事务脚本 vs 领域模型,哪个才是业务优化的终极方案?2万字带你入门DDD玛利亚·蒙台梭利博士的14条教育精髓!迄今为止最完整的DDD实践
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。