简介: 在我看来并不是MVC的基础上增加领域层,使用充血模型,解耦基础服务,我的代码就符合DDD了。

作者 | 李宇飞(菜尊)
来源 | 阿里开发者公众号

在我看来并不是MVC的基础上增加领域层,使用充血模型,解耦基础服务,我的代码就符合DDD了。

为什么要使用DDD?

DDD分为战略部分跟战术部分,相信大家都认同DDD的核心在战略而非战术。而战略方面的核心我认为在业务建模,领域划分、统一语言等都在为业务建模服务。

为什么业务建模重要?

以前的开发流程有什么问题?

先说结论,开发人员交付的程序对业务方,产品人员,测试人员来说就是一个黑盒子。除了开发人员自己,没人知道盒子里有什么。当新的需求加入来,需求方,产品人员,甚至测试人员都认为可行,开发人员却给出相反结论。

回顾一下以前的开发流程 大致可以归结为以下步骤:(开发跟测试人员最好能参与需求分析)

  1. 业务方描述抽象需求
  2. 产品将需求转化为可落地的产品(需求具像化,PRD)
  3. 开发人员根据产品的PRD开发
  4. 测试人员根据产品的PRD测试
  5. 产品人员验收
  6. 业务方验收

依照经验来说,在整个流程中开发人员是耗时最长的。与此同时测试人员可能在编写测试用例,而业务方跟产品人员在这段时间内是阻塞的。最终的程序质量靠测试人员来保障。

开发人员完成开发后:

测试人员关注测试用例是否通过。

产品人员确认展现出来的功能是否符合当初的PRD。

业务方确认程序是否符合预期。

举一个我开发项目的例子

一个审批系统

产品的PRD描述了一个三层模型:

流程实例,流程节点,审批任务。流程实例启动创建审批节点,审批节点触发审批任务,任务完成创建下一个节点...。

我是这样做的:

流程实例,审批任务。流程实例启动创建(一批)审批任务,任务被完成后创建后续任务或者流程结束。至于流程节点不存在,不是问题,从任务中提取信息虚拟一个出来。

第一版交付完成。

产品在第一版后追加需求

流程节点可以被非审批人评论。

我....

当时业务方,产品,测试都认为这是一个合理的需求。只有我一脸懵,因为我的程序中没有流程节点这个东西,需求又不能拒绝,无奈给出一个远超他们预期的开发计划。

gap就出在 他们认为流程节点是一个确实存在的东西,而只有我知道这节点是虚拟的,没有标识,不能跟其他信息做关联。

业务建模怎么解决这个黑盒子问题?

DDD引入了业务专家这个角色(在我看来就是业务方,产品)。

  1. 假设业务专家听不懂 什么叫类,什么是方法,设计模式,他只知道他的业务,两方人马完全不在同一频道,这个时候就需要“明确上下文”,“统一语言”了。(不仅仅开发人员与业务专家达成了共识,也包含整个开发团队达成共识)
  2. 业务建模,用例分析法、事件风暴、四色建模等看看开始整上。最终达到划分领域,识别聚合的目的。
  3. 业务建模落地。开发人员开发过程中,应遵守已经建立的业务模型来编写代码。

至此终于实现了,业务专家可通过业务模型窥探到开发人员的代码实现。统一语言、业务模型在业务专家跟开发人员中间充当了沟通的桥梁。(有点像适配器)

当追加新的需求时,业务专家能合理评估需求的可行性。

让非开发人员参与到开发中

统一语言,业务建模,模型充血(OOP)。这一系列手段都是为了实现让非开发人员参与到开发中这一最终目的。与其说DDD是一种架构,不如认为他是指导开发的方法论。

好比盖房子,以前只要把房子交付了能住人就行。现在业务专家是设计师,业务模型是设计图,代码则是建材,程序员就是工地搬砖的,盖起来的房子得跟设计师给出的设计图一样。

业务模型落地的问题?

这是一个让我纠结的问题。我感觉还没有找到符合我期望的答案。

业务模型具现到代码中,就是一个个聚合。这些聚合符合OOP的思想,通过聚合根,实体,值对象的组合来表达业务模型。

以网络上常见的demo为例:

  1. //订单明细实体
  2. public class OrderItemEntity {
  3. //id
  4. private Long id;
  5. //商品(值对象)
  6. private Product product;
  7. //数量(值对象)
  8. private Count count;
  9. // item总价(值对象)
  10. private ItemAmount itemAmt;
  11. public void modifyCount(Count count) {
  12. this.count = count;
  13. this.itemAmt = new ItemAmt(this.product.getPrice()*count.get());
  14. }
  15. }
  16. //订单聚合根
  17. public class OrderAggregate {
  18. //聚合唯一标识
  19. private Long id;
  20. //订单号(值对象)
  21. private OrderNo orderNo;
  22. //总价 (值对象)
  23. private Amount amt;
  24. //订单明细
  25. private List<OrderItemEntity> items;
  26. public void modifyItemCount(Product product,Count count) {
  27. //找到商品
  28. this.items.stream().filter(product::equals).findAny().get();
  29. //修改数量 返回Item总价
  30. item.modifyCount(count);
  31. ItemAmount itemAmt = item.getItemAmt();
  32. //修改订单总价
  33. this.amt = new Amount(this.amt.get()+itemAmt.get());
  34. }
  35. }
  36. //要订单明细中 名称叫电脑的商品数量修改100
  37. Product product = new Product("电脑");
  38. Count count = new Count(100);
  39. Amount amt = orderAggregate.modifyItemCount(product, count);

理论与现实的矛盾

从代码上可以看出这一段1:n关系的代码完全基于内存,非常的OOP,也就是说,我们在获得orderAggregate时,已经加载整个聚合(包括List),这里就隐含了一个条件内存无限大。

假设OrderItemEntity的量级是十万级,百万级,明显这段代码是不能上线的,理论与现实出现了矛盾。

咨询了很多大佬+个人理解(以下方法为我自己命名)

模型提升法(无限套娃)

大佬建议:

“如果真有这种场景,就需要调整聚合,比如:将OrderItem提升为Order, Order提升为BatchOrder”

思路:

创建BatchOrderAggregate,BatchOrderAggregate持有OrderEntity。
创建OrderAggregate持有部分OrderItemEntity,通过分治的方式化整为零。

思考:

整体上符合业务模型,而且没有上限,即如果BatchOrderAggregate不能解决问题,那就祭出BatchBatchOrderAggregate。
BatchBatchOrderAggregate持有BatchOrderEntity。
BatchOrderAggregate持有OrderEntity。
OrderAggregate持有部分OrderItemEntity。

持有仓储法

(隐藏了数据库查询,但是直觉上有点反模式)

大佬建议:

“聚合中构建索引,需要时再加载置换”

思路:

聚合根持有存储引用,需要时加载到内存中。

可以加入一层接口隔离。

  1. public class OrderAggregate {
  2. //聚合唯一标识
  3. private Long id;
  4. //订单号(值对象)
  5. private OrderNo orderNo;
  6. //总价 (值对象)
  7. private Amount amt;
  8. //关联对象接口(接口实现在基础服务层,在实现中操作数据库)
  9. private OrderItemRel orderItemRel;
  10. public void modifyItemCount(Product product,Count count) {
  11. List<OrderItemEntity> items = orderItemRel.find(product);
  12. //找到商品
  13. OrderItemEntity item = items.stream().filter(product::equals).findAny().get();
  14. //修改数量 返回Item总价 这里有分支,item修改是否应该在modifyCount中持久化
  15. //1.modifyCount中持久化item那么数据库事务将被加载AppcationService层,容易产生大事务问题。
  16. //2.modifyCount中不持久化item
  17. //2.1写入消息总线,当OrderAggregate通过Reponsitory持久化时刷出消息持久化
  18. //2.2 OrderAggregate中增加List<OrderItemEntity> items,modifyCount将item加入items。
  19. item.modifyCount(count);
  20. ItemAmount itemAmt = item.getItemAmt();
  21. //修改订单总价
  22. this.amt = new Amount(this.amt.get()+itemAmt.get());
  23. return this.amt;
  24. }
  25. }
自我催眠法

思考:

“持有仓储法”思路,实现也一致,觉得反模式的原因是:聚合中含有数据库操作,有耦合基础服务的嫌疑。

但是换个方向去想:

内存也是存储介质,数据库也是存储介质,二者本来没有质的区别。二者相比只是对于内存操作,编程语言直接提供了API,而数据库访问需要依赖第三方库进行额外编码,假使能将数据库操作封装至跟内存操作一样自然,那么不是不可以让人接受。

Id关联法

(感觉上不太符合我的预期,有代码实现跟业务建模脱钩,耦合基础服务的嫌疑。容易退化为MVC,此句属于本人主观臆断)

大佬建议:

“对ddd理解,不能固化。聚合,本意是解决业务操作的一致性。大量文章,都表述为一次性加载,实操中是不现实的。解决 “业务操作一致性”,走 “id关联+内聚到一个函数+事务控制”,就很好。”

“没有必要强行ddd,拆分开也没有什么问题,通过id关联就可以。”

“业务建模时按照在同一聚合去建,落地时考虑现实,拆分聚合,通过id关联。”

思考:

跟“持有仓储法”很像,区别可能是在于代码写在哪里,但是这种方法总感觉不是OOP。

聚合拆分法

(我觉得applicationService中应该是跨领域的流程编排,Order,OrderItem有相同的生命周期不能算跨领域,只能算夸聚合。至于domainService,做为领域的一部分,理论上不应该涉及基础服务,只是存放业务相关但是不适合放在聚合中,也非跨域的代码)

大佬建议:

“如果OrderItem超过10万,20万, 这种情况一般大概率不需要一次性加载出来所有OrderItem,而是分页加载OrderItem, 这和聚合的特点冲突,建议设计成两个领域对象单独管理”

思路:

建立 OrderAggregate跟 OrderItemAggregate两个聚合,通过领域事件 实现最终一致。

灵活场景法(聚合拆分法Plus)

(感觉还是反模型,代码好理解,业务专家不一定认可,不像自然语言那样自然,为了性能做出妥协)

如demo中我们要修改OrderItem的数量,这样一个场景,场景主体是OrderItem而不是Order,Order被修改可以认为是副作用。

明确场景的情况下(明确上下文) 可以建模为OrderItemAggregate和OrderEntity 将1:n的关系转化为1:1。

  1. //订单明细聚合
  2. public class OrderItemAggregate {
  3. //id
  4. private Long id;
  5. //商品(值对象)
  6. private Product product;
  7. //数量(值对象)
  8. private Count count;
  9. // item总价(值对象)
  10. private ItemAmount itemAmt;
  11. //Order实体
  12. private OrderEntity orderEntity;
  13. public void modifyCount(Count count) {
  14. this.count = count;
  15. ItemAmt itemAmt = new ItemAmt(this.product.getPrice()*this.count.get());
  16. //order总价
  17. Amt amt = this.orderEntity.getAmt();
  18. //总价-item总价+新item总价
  19. amt = new Amt(amt.get() - this.ItemAmt.get() + itemAmt.get());
  20. //变更订单总价
  21. this.orderEntity.modifyAmt(amt);
  22. this.itemAmt = itemAmt;
  23. }
  24. }
  25. //订单实体
  26. public class OrderEntity {
  27. //聚合唯一标识
  28. private Long id;
  29. //订单号(值对象)
  30. private OrderNo orderNo;
  31. //总价 (值对象)
  32. private Amount amt;
  33. public Amount modifyAmt(Amt amt) {
  34. //修改订单总价
  35. this.amt = amt;
  36. }
  37. }
  38. //要订单明细中 名称叫电脑的商品数量修改100
  39. orderItemAggregate.modifyCount(count);

删除/添加订单明细同理。

而,订单支付(订单被支付)的场景,业务主体是Order(这个场景下OrderItem甚至不会出现修改,当然也就没有必要加载OrderItem)。

总结

感谢各位大佬提供自己的思路为我解惑。

对与聚合落地,因为最后一种灵活场景变化聚合的思路,完全无关于基础服务,保持了聚合内的一致性,符合DDD领域只关注业务的思想,而且勉强符合OOP,且落地成本低,从心里上我更倾向于最后一种方式。唯一的难点在于说服自己他是一个正常的业务模型。

推荐阅读

1.如何写出一篇好的技术方案?

2.阿里10年沉淀|那些技术实战中的架构设计方法

3.如何做好“防御性编码”?


阿里云产品测评—开源PolarDB-PG

体验阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 的能力和高可靠、高可用、弹性扩展等企业级数据库特性。发布评测,写下你的感受与评价即可获得多重福利。

点击这里,查看详情。

原文链接:https://click.aliyun.com/m/1000359990/

本文为阿里云原创内容,未经允许不得转载。

浅谈DDD中的聚合的更多相关文章

  1. 浅谈HTTP中GET、POST用法以及它们的区别

    浅谈HTTP中GET.POST用法以及它们的区别 HTTP定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE.URL全称是资源描述符.我们可以这样认为: 一 ...

  2. 浅谈Java中的equals和==(转)

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: 1 String str1 = new String("hello"); 2 String str ...

  3. 浅谈Linux中的信号处理机制(二)

    首先谢谢 @小尧弟 这位朋友对我昨天夜里写的一篇<浅谈Linux中的信号处理机制(一)>的指正,之前的题目我用的“浅析”一词,给人一种要剖析内核的感觉.本人自知功力不够,尚且不能对着Lin ...

  4. 浅谈Java中的对象和引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  5. 浅谈Java中的equals和==

    浅谈Java中的equals和== 在初学Java时,可能会经常碰到下面的代码: String str1 = new String("hello"); String str2 = ...

  6. 转【】浅谈sql中的in与not in,exists与not exists的区别_

    浅谈sql中的in与not in,exists与not exists的区别   1.in和exists in是把外表和内表作hash连接,而exists是对外表作loop循环,每次loop循环再对内表 ...

  7. 浅谈iOS中的userAgent

    浅谈iOS中的userAgent   User-Agent(用户代理)字符串是Web浏览器用于声明自身型号版本并随HTTP请求发送给Web服务器的字符串,在Web服务器上可以获取到该字符串. 在公司产 ...

  8. 浅谈JavaScript中的闭包

    浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...

  9. 浅谈sql中的in与not in,exists与not exists的区别

    转 浅谈sql中的in与not in,exists与not exists的区别   12月12日北京OSC源创会 —— 开源技术的年终盛典 »   sql exists in 1.in和exists ...

  10. 浅谈Java中的深拷贝和浅拷贝(转载)

    浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...

随机推荐

  1. IDEA无限试用插件

    原文地址 之前一直在找激活方法,忽然想到IDEA不是可以试用吗?一直试用不就可以变相地达到了激活的效果? 本篇作废,本篇作废,本篇作废,由于IDEA插件的问题,导致并不能成功的进行重置试用 新整了个J ...

  2. uniapp踩坑记录

    sessionStorage.setItem('token', data.msg)uni.setStorage('token', res.data); 搞了半天登录后直接通过获取getstorage获 ...

  3. 记一次kafka无法生产发送消息排查经历

    参考,欢迎点击原文:https://stackoverflow.com/questions/37902167/kafka-error-while-fetching-metadata-with-corr ...

  4. 告别繁琐!1分钟带你构建RabbitMQ消息应用

    支持.Net/.Net Core/.Net Framework,可以部署在Docker, Windows, Linux, Mac. RabbitMQ作为一款主流的消息队列工具早已广受欢迎.相比于其它的 ...

  5. 工作中最常见的6种OOM问题

    前言 最近我写的几篇线上问题相关的文章:<糟糕,CPU100%了><如何防止被恶意刷接口><我调用第三方接口遇到的13大坑>发表之后,在全网广受好评. 今天接着线上 ...

  6. HTML(html结构、标签导读 、路径))

    HTML第一天 我们接下来是进行的网页开发网页的相关概念: 什么是网页? 什么是HTML? 网页的形成? 一 什么是网页: 1.网站是指在因特网上根据一定的规则,使用 HTML 等制作的用于展示特定内 ...

  7. MySQL系列:索引失效场景总结

    相关文章 数据库系列:MySQL慢查询分析和性能优化 数据库系列:MySQL索引优化总结(综合版) 数据库系列:高并发下的数据字段变更 数据库系列:覆盖索引和规避回表 数据库系列:数据库高可用及无损扩 ...

  8. Python实现SQL注入脚本

    实验环境 攻击主机IP:172.18.53.145 目标主机IP:172.18.53.11 此处的靶场是Vulnhub中的WEB MACHINE: (N7) 靶场测试 访问靶场的登录页面,使用sqlm ...

  9. WPF状态保存

    由于WPF做客户端的时候,它不像BS那样有Session,Cookie给你使用,所以保存状态你首先想到的就是数据库了. 但是你不可能什么都放在数据库,为此还专门为它建立一张表. 而WPF中能用到的除了 ...

  10. 【福利】JetBrains 全家桶永久免费使用

    Jetbrains系列的IDE公认是最好的集成开发工具,但是收费且挺贵.我们以PhpStorm为例,新用户第一年需要199$,注意是$,还不是人民币,这个价格一上来肯定筛选掉一大批用户.确实好用,所以 ...