1 为什么我要研究领域驱动设计

1.1 设计方法各样且代码无法反映设计

我大概从2017年10月份开始研究DDD,当时在一家物流信息化的公司任职架构师,研究DDD的初衷在于为团队寻找一种软件设计的方法论。作为架构师,经常参与设计评审,包括:需求评审、设计评审、代码评审。在评审过程中,有一点感受非常深,就是评审过程非常痛苦且几乎没有效率和成果。让我痛苦的地方有:

  • 每一个系统分析师都是基于自己的方式来进行设计功能,有的用类图、有的基于流程图,有的详细、有的粗放,更麻烦的是,大家对业务背景的理解程度完全不同,认知不同,沟通低效,很难找出设计的不合理性。
  • 评审代码时,我几乎很难将其与设计对应起来,看设计我已经够痛苦了,还要被这些代码再虐待一遍,实在痛苦至极,这样的代码评审也就变成了代码规范性、代码设计优雅度的评审,很难找出代码业务逻辑的问题。让代码正确的反应设计,是当时评审过程中碰到的一个更大的问题。

1.2 代码质量很难有效提升

在承担架构师之前,我的另一个职责是技术管理,做的工作是与软件质量相关的。当时加入一个大概2000万规模的项目,有大约100开发人员参与,开发周期大概1年。加入该团队在开发的过程中,发现了两个问题:

  • 每一个BA(可以理解为PD)设计的产品界面操作习惯都不一样,所有的开发人员做出来的界面的操作也完全不同。但是,这是一个面向物流行业的信息化软件,操作习惯的一致性很重要。
  • 代码非常混乱,没有任何的规范可言,看代码简直想吐。

基于第一个问题,我定义了统一的界面规范,这个界面规范通过和公司的PMO合作将其融入到工程过程中,作为开发人员必须遵循的规范。第二个问题,我则花费了很多的时间来尝试解决(大概有2年时间都与代码质量做斗争),最终与寻找统一的设计方法殊途同归。

如何让我们的代码变得更加干净,我在执行的过程中,按照以下步骤一步一步的执行。

  • 定义了统一的代码规范,基于界面规范的基础上,统一定义了模板工程,这些模板工程都有很好的代码基因。
  • 定义了代码规范的培训教程,包括基本的书写规范、《代码整洁之道》、《重构技巧》。
  • 定义了代码规范、代码评审制度,写入PMO定义的过程工作,作为开发人员遵循的制度。
  • 通过代码评审提升质量太慢,为了大规模快速推广,引入了SonarQube,定义了软件代码质量的度量方法,软件的代码质量分数由:圈复杂度、重复率、代码规模问题、SonarQube扫描的问题数四个维度来衡量。在度量方法之上,定义了代码质量管理制度,每周扫描软件获得详细的代码质量报告,发送给相应的产品负责人,将代码质量管理制度也融入PMO的工程过程里面,全公司进行推广,由产品负责人负责本部分的代码质量提升。

基于以上的代码质量管理方法,我认为已经是做的相当不错,但是非常遗憾的是,当我抽样评审产品的代码时,我依然感到无比沮丧,软件的代码还是太复杂、太难看懂了,与《代码整洁之道》的要求相差太远了,我耗费了1年多的工作几乎毫无成果可言。因此,我在深深思考,在编码层面,定义了规范、做了优雅编码培训、定义了编写优秀代码的相关制度,就为了让开发人员把代码写好,使代码看起来更加清晰,软件更加容易维护,为什么还是无法实现?

2 软件复杂性的根源

贫血模型是软件复杂性的根源。贫血模型本质是面向数据的设计,面向过程的编码。基于贫血模型的分层架构,通常分为UI层、业务逻辑层、数据访问层、贫血模型层,贫血模型与数据模型一致。

业务规则是软件最核心的代码,通常只占整个软件很小的一部分。在基于贫血模型的架构中,业务规则的实现,通常混杂在上层UI展现逻辑、数据库访问、缓存等各种逻辑中,分散在各个层和关联对象。通过阅读业务逻辑层的代码来还原真实的业务规则很困难,很难从代码反映其业务规则设计,并且随着软件需求变更,业务规则更加难以还原,软件复杂度将不可控。

以下是一段业务逻辑层的实现代码。

  1. public OrderDto signOrder(Order order) {
  2. Assert.notNull(order, "OrderDto can not be null.");
  3. OrderDto result = new OrderDto();
  4. result.setIsOperationSuccess(true);
  5. if (null == order.getId()) {
  6. result.setIsOperationSuccess(false);
  7. result.setOperationMassage("id不能为空。");
  8. return result;
  9. }
  10. OrderCondition orderCondition = new OrderCondition();
  11. orderCondition.setId(order.getId());
  12. order = orderMapper.selectOne(orderCondition);
  13. if (null == order) {
  14. result.setIsOperationSuccess(false);
  15. result.setOperationMassage("该订单不存在。");
  16. return result;
  17. }
  18. if (order.getOrderStatus() != Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_WAIT_RECEIVE.getCode())) {
  19. result.setIsOperationSuccess(false);
  20. result.setOperationMassage("订单号:{" + order.getOrderNo() + "}不是待收货状态,不能进行签收。");
  21. return result;
  22. }
  23. // 该订单下的所有商品的实收数(发货数量)必须都大于0
  24. boolean validDeliveryCount = true;
  25. Double orderTotalAmount = 0d;
  26. List<OrderGoodsDto> orderGoodsList = orderGoodsBiz.selectOrderGoodsByOrderId(order.getId());
  27. List<OrderGoods> orderGoodsListForUpdate = new ArrayList<>();
  28. if (EmptyUtil.isNotEmpty(orderGoodsList)) {
  29. for (OrderGoodsDto orderGoods : orderGoodsList) {
  30. if (null == orderGoods.getDeliveredNum() || orderGoods.getDeliveredNum() <= 0) {
  31. validDeliveryCount = false;
  32. } else {
  33. // 根据商品发货数量重新计算订单总金额......
  34. Double price = (null == orderGoods.getDiscountPrice() ? orderGoods.getOriginalPrice() : orderGoods.getDiscountPrice());
  35. Integer goodsNum = (null == orderGoods.getDeliveredNum() ? 0 : orderGoods.getDeliveredNum());
  36. orderTotalAmount += price * goodsNum;
  37. // 更新orderGoods的收货数量
  38. orderGoods.setReceivedNum(goodsNum);
  39. OrderGoods orderGoodsForUpdate = new OrderGoods();
  40. BeanUtils.copyProperties(orderGoods, orderGoodsForUpdate);
  41. orderGoodsListForUpdate.add(orderGoodsForUpdate);
  42. }
  43. }
  44. }
  45. if (!validDeliveryCount) {
  46. result.setIsOperationSuccess(false);
  47. result.setOperationMassage("订单号:" + order.getOrderNo() + ",订单下所有商品都已发货才可进行签收操作,请确认。");
  48. return result;
  49. }
  50. order.setOrderStatus(Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_SIGN.getCode()));
  51. order.setOrderTotalAmount(orderTotalAmount);
  52. order.setPaymentAmount(orderTotalAmount);
  53. order.setUnpaidAmount(orderTotalAmount);
  54. update(order);
  55. orderGoodsBiz.batchUpdate(orderGoodsListForUpdate);
  56. List<Order> orders = new ArrayList<Order>();
  57. orders.add(order);
  58. saveRouteMessage(orders);
  59. return result;
  60. }

类似这样的代码非常常见,通过阅读这段业务逻辑代码,可以发现它处理了以下的任务:
(1)返回结果的处理。
(2)数据库访问。
(3)关联对象的数据库访问。
(4)业务规则。

业务规则代码与数据库访问、关联对象数据库访问、结果处理等其它逻辑在一起实现,通过代码还原业务规则会越来越复杂且随着时间推移,代码逻辑会越来越偏离设计。作为软件系统最核心的部分——业务规则,如果我们仅仅将其从其它任务中剥离,我们的代码将演化如下。(注:以下代码仅演示剥离出来业务逻辑,并非DDD推荐方式,下篇介绍。)

  1. public void signOrder(Order order) {
  2. assertCanBeSigned(order);
  3.  
  4. Double orderTotalAmount = 0d;
  5.  
  6. List<OrderGoods> orderGoodsList = order.getOrderGoods();
  7. for (OrderGoods orderGoods : orderGoodsList) {
  8. Double price = (null == orderGoods.getDiscountPrice() ? orderGoods.getOriginalPrice() : orderGoods.getDiscountPrice());
  9. Integer goodsNum = (null == orderGoods.getDeliveredNum() ? 0 : orderGoods.getDeliveredNum());
  10. orderTotalAmount += price * goodsNum;
  11.  
  12. orderGoods.setReceivedNum(goodsNum);
  13. }
  14.  
  15. order.setOrderStatus(Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_SIGN.getCode()));
  16. order.setOrderTotalAmount(orderTotalAmount);
  17. order.setPaymentAmount(orderTotalAmount);
  18. order.setUnpaidAmount(orderTotalAmount);
  19. }
  20.  
  21. public void assertCanBeSigned(Order order) {
  22. Assert.notNull(order, "OrderDto can not be null.");
  23.  
  24. if (order.getOrderStatus() != Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_WAIT_RECEIVE.getCode())) {
  25. throw new BusinessException("订单号:{" + order.getOrderNo() + "}不是待收货状态,不能进行签收。");
  26. }
  27.  
  28. List<OrderGoods> orderGoodsList = order.getOrderGoods();
  29. if (!EmptyUtil.isNotEmpty(orderGoodsList)) {
  30. throw new BusinessException("订单号:" + order.getOrderNo() + ",订单没有包含商品,是一个空的订单,无法签收。");
  31. }
  32.  
  33. for (OrderGoods orderGoods : orderGoodsList) {
  34. // 该订单下的所有商品的实收数(发货数量)必须都大于0
  35. if (null == orderGoods.getDeliveredNum() || orderGoods.getDeliveredNum() <= 0) {
  36. throw new BusinessException("订单号:" + order.getOrderNo() + ",订单下所有商品都已发货才可进行签收操作,请确认。");
  37. }
  38. }
  39. }

这段代码反映的业务规则是订单签收规则。

(1)如果订单不是待发货状态,不能签收;
(2)校验订单下所有商品的发货数量都要大于0;
(3)计算订单总金额,并设置收货数量为发货数量;
(4)设置签收状态、总金额、支付金额和未付金额。

你可以发现这段单纯实现业务规则的代码,会更加的简单、清晰,也会使软件更加的容易维护。在DDD的方法论里面,业务规则是在领域层来实现的,领域层的代码仅仅是业务规则,这时候,其分层架构的分层逻辑和基于贫血模型的分层逻辑也会不一样了。

通过以上代码的对比我们发现:

  • 剥离业务规则无关的代码,将更加清晰简单,容易和业务规则保持一致。
  • 贫血模型会导致业务逻辑层混杂了太多代码和逻辑,难以还原业务规则,保证代码与设计一致性,是复杂性根源。

3 DDD如何解决软件复杂性

DDD解决软件复杂性的方法核心为两点:

  • 通过领域模型为业务知识建模,领域模型作为业务、技术团队沟通的统一语言。
  • 确保软件实现与领域模型保持一致。

软件实现与领域模型保持一致是本书的核心思想,DDD构建了一套完整的方法论来支持领域模型驱动程序设计。这套方法论简述如下。

  • 分层架构:业务规则的代码只占软件很少的代码却是最核心的部分代码,将其分离出来作为独立的领域层,使领域层的实现与领域模型保持一致,领域层的业务对象不再是贫血模型。
  • 领域驱动设计:领域驱动设计,即领域模型驱动程序设计。这里给出了如何通过代码表达领域模型的编码模式。这些模式包括:关联、实体、值对象、服务、聚合根、Repository、Factory。它们构建了将领域模型表达成代码的方法论,保证了代码和设计一致。
  • 战略设计:复杂领域模型的实现方法论。

我将在下一篇文章中详细解释DDD的核心思想,让你明白它是如何解决复杂性的。

解构领域驱动设计(一):为什么DDD能够解决软件复杂性的更多相关文章

  1. 阅读文章《DDD 领域驱动设计-如何 DDD?》的阅读笔记

    文章链接: https://www.cnblogs.com/xishuai/p/how-to-implement-ddd.html 文章作者: 田园里的蟋蟀 首先感谢作者写出这么好的文章. 以下是我的 ...

  2. DDD 领域驱动设计-如何 DDD?

    注:科比今天要退役了,我是 60 亿分之一,满腹怀念-

  3. 两个字搞定DDD(领域驱动设计),DDD脱水版(一)修订版

    摘自微信公众号丁辉的软件架构说

  4. 从0开发3D引擎(补充):介绍领域驱动设计

    我们使用领域驱动设计(英文缩写为DDD)的方法来设计引擎,在引擎开发的过程中,领域模型会不断地演化. 本文介绍本系列使用的领域驱动设计思想的相关概念和知识点,给出了相关的资料. 上一篇博文 从0开发3 ...

  5. 我的“第一次”,就这样没了:DDD(领域驱动设计)理论结合实践

    写在前面 插一句:本人超爱落网-<平凡的世界>这一期,分享给大家. 阅读目录: 关于DDD 前期分析 框架搭建 代码实现 开源-发布 后记 第一次听你,清风吹送,田野短笛:第一次看你,半弯 ...

  6. DDD 领域驱动设计-如何完善 Domain Model(领域模型)?

    上一篇:<DDD 领域驱动设计-如何 DDD?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 阅读目录: ...

  7. [转]DDD领域驱动设计基本理论知识总结

    领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity i ...

  8. DDD 领域驱动设计-在动手之前,先把你的脑袋清理干净

    惨不忍睹的翻译 英文原文:http://www.codeproject.com/Articles/339725/Domain-Driven-Design-Clear-Your-Concepts-Bef ...

  9. DDD领域驱动设计基本理论知识总结

    领域驱动设计之领域模型 加一个导航,关于如何设计聚合的详细思考,见这篇文章. 2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity i ...

随机推荐

  1. Jmeter选项含义

    最近接了组里压测的任务,开始仔细钻研Jmeter了.之前也压过,但每次RD问压测的指标等问题,感觉都很懵不知道该怎么回答.借这个机会一鼓作气搞明白吧! Jmeter安装插件 有个插件叫jp@gc St ...

  2. Java Concurrency in Practice——读书笔记

    Thread Safety线程安全 线程安全编码的核心,就是管理对状态(state)的访问,尤其是对(共享shared.可变mutable)状态的访问. shared:指可以被多个线程访问的变量 mu ...

  3. 解析时间parse time

    下面是一个解析时间的一个类 <?php /** * @purpose : 解析时间 * author: 袋鼠 * date: 2019/3/1 * time: 19:43 */ class Pa ...

  4. Django model对象接口

    Django model查询 # 直接获取表对应字段的值,列表嵌元组形式返回 Entry.objects.values_list('id', 'headline') #<QuerySet [(1 ...

  5. web项目如何使用Material Icons

    使用文档链接 图标库 最简单的使用方法 引入 <link href="https://fonts.googleapis.com/icon?family=Material+Icons&q ...

  6. linux学习:curl与netcat用法整理

    CURL 语法: curl [option] [url] 常用参数:-A/--user-agent <string> 设置用户代理发送给服务器-b/--cookie <name=st ...

  7. Centos7 编译安装 Nginx Mariadb Asp.net Core2 (实测 笔记 Centos 7.3 + Openssl 1.1.0h + Mariadb 10.3.7 + Nginx 1.14.0 + Asp.net. Core 2 )

    环境: 系统硬件:vmware vsphere (CPU:2*4核,内存2G,双网卡) 系统版本:CentOS-7-x86_64-Minimal-1611.iso 安装步骤: 1.准备 1.0 查看硬 ...

  8. webpack配置非CMD规范的模块

    一.前言 webpack在配置多页面开发的时候 ,发现用 import 导入 Zepto 时,会报 Uncaught TypeError: Cannot read property 'createEl ...

  9. PTA第四次作业

    题目 7-1 计算职工工资 1.设计思路 (1)第一步:观察题意了解各个参数与所需函数在题目中的意义: 第二步:设计算法编写函数,让函数的功能实现题目中所需的功能: 第三步:运行程序检测是否错误. ( ...

  10. Mesos源码分析(2): Mesos Master的启动之一

    Mesos Master的启动参数如下: /usr/sbin/mesos-master --zk=zk://127.0.0.1:2181/mesos --port=5050 --log_dir=/va ...