查看原文
其他

【吐血推荐】什么是领域驱动设计?DDD?

Java3y 2021-01-12

以下文章来源于我没有三颗心脏 ,作者我没有三颗心脏

本文公众号来源:我没有三颗心脏  作者:我没有三颗心脏


本文作者是我在大三认识一个朋友,以前就经常看他的文章。这次他写了一篇《DDD》(虽然我也没学过,但是觉得写得很不错!给大家分享一下)


PS:这篇文章我得多次阅读才能好好理解(也建议各位收藏)

一、Hello DDD


刚开始接触学习「DDD - 领域驱动」的时候,我被各种新颖的概念所吸引:「领域」、「领域驱动」、「子域」、「聚合」、「聚合根」、「值对象」、「通用语言」…..总之一大堆有关的、无关的概念从我的脑海经过,其中不乏让我陷入思考的地方,我原以为我会很开心地 “享用” 这些新知识带给我的营养(参照下图)

可事实上,我为学习「DDD - 领域驱动」付出了很多的精力,我尝试用「DDD CRUD」、「DDD vs CRUD」、「Domain-Driven Design」、「DDD CQRS」、「领域驱动设计」等等一系列的关键字搜集我想要的资料(翻遍了 Google 前排的所有文章&手动感谢谷歌让我能获得一些精彩的文章),可似乎都不太近人意,一方面这个「新概念」我对它的困惑太多了,另一方面真正「落地」并实践起来的经验有很少是可以直接借鉴的,再结合一些实际的场景(没有人解答),我感到更加困惑。

传统开发面临的问题

我们先来讨论一下传统开发面临的一些问题吧,就先从传统开发中被广泛应用于 Web 开发的传统三层框架:「MVC」 开始说起吧。

  • 图片来源:https://draveness.me/mvx

传统的「MVC」模型把框架分成了三层:显示层、控制层、模型层,而传统的模型层又被拆分成了业务层(Service)和数据访问层(DAO,Data Access Object)。

显示层负责显示用户界面、控制层负责处理业务逻辑、而模型则负责与数据库通信,对数据进行持久化的操作。这样的结构不仅结构松散,而且各个模块职责分离,有什么问题呢?

让我们来看一个实际的例子吧。

假设我们做了一个会议室预定系统,我们的一个设备坏了。我们需要通知预定这个会议室的所有人,于是我们需要发邮件,伪代码如下:

@Service
public class EquipmentServiceImpl implements EquipmentService {
    @Autowired private EmailService emailService;
    @Autowired private EquipmentRepository equipmentRepository;

    public void setEquipmentBroken(Long id) {
        Equipment equipment = equipmentRepository.findById(id);
        equipment.setStatus(Equipment.StatusEnum.BROKEN);

        emailService.sendEmail();
    }
}

问题来了,如果我们后来发现设备坏了并且需要更改可用库存的数量,这时候我们是不是要在这里加入 InventoryService 库存服务的代码呢?后来如果经理说设备坏了应该通知他才对啊,所以我们要不要加入 emailService.sendEmailTo(Manager) 这样的代码呢?

就算不考虑职责单一原则和关注分离原则,程序员也会疯掉的,这样做 Service 太重了,并且糟糕的是它可能还不止考虑这些,还有权限、事务等等一系列的事情等着 Service 层去做,如此产生了大量的依赖和循环依赖,当业务复杂度上升时,直接导致了服务层所含的代码过于庞大和复杂、测试成本直线上升,并且各个 Service 的逻辑散落在各处,维护的成本也非常大。

我相信很多公司正在经历这样的事情,并且问题还远不止于此。

最近我就经历过另一种问题。作为实习生刚入公司的我接到产品了一个需求,虽然有正规的需求文档可以供我阅读,但对于业务还不熟悉的我读起来就感觉是:摁,我想要这个页面这样。

当产品经历耐心的过来给我解释的时候,我仍然感到无奈,因为他尽可能详细地在描述他想要在哪一个页面的哪一个地方加上什么东西的同时,我看着眼前屏幕上的一堆模型和代码,感到无从下手,只能找来大佬帮忙充当一下 “翻译”。

必须承认自己对业务的生疏是主要的原因,但根本原因还是:开发与产品之间的「沟通」不能保持一致,双方对于同一事物的「表达和理解」有很大的区别。产品看到的是实际的「业务场景」,而开发则更关注背后的「实现逻辑」。

CRUD 的各种问题

上面或许有说得不对的地方,但这样的现象确实的存在。(例如我审视了一下我之前写过的代码,突然感慨幸好自己之前都是独立开发且功能简单,嘻嘻嘻)

另一个想要讨论的问题是关于后端开发者常常拿来自嘲的「CRUD」。经常有开发人员苦着脸说:每天除了写「BUG」,就一直在写「CRUD」代码,没有很大长进。当然这只是一种自嘲,但可能写「BUG」是真的,也可能没有长进是真的,当然两个都可能都是真的。

「CRUD」 其实对应的是数据库中的增删改查的操作。现实的情况中,只有极少有企业不用到数据库,数据库就像是现代软件开发的一剂灵丹妙药,不仅提供可靠、快速、大容量的存储服务,还支持强大的事务管理机制,满足了大部分场景中对数据的一致性需求。

数据库如此的强大,以至于我们从接触软件开发开始就一直使用「CRUD」的模式进行开发。我们的「潜意识」中就形成了「以数据为中心」的开发模式,这没有什么不好,并且大多数情况下是适用的,这里只是讨论:「CRUD」有什么问题?

问题一:面向对象和数据库天然阻抗

面向对象编程的语言和数据库都是我们几乎“最熟悉”的东西了,我们甚至使用他们编织出了绝大多数复杂多样的网络应用,为什么说它们之间存在着天然阻抗?

当然我觉得这里有一点「强行找不同」的味道,但也不失为一种思考和讨论。并且我觉得还是有点道理的。

A1:对象和关系数据库累赘转换

在一个面向对象的系统中,对象是数据的承载方式,每一个 DAO 对象都对应着关系数据库中的一条数据。

但通常视图层只显示完整实体对象的一小部分数据,那么其余的「无关数据」你准备怎么处理呢?

  • 是否要把对象中包含的「所有数据」一起返回给视图层?

  • 是否需要创建一个新的专用的「数据传输对象」?

  • 或者你想直接把「无关数据」字段设置成 null

显然,在绝大多数应用中都采取了第二种方案,于是我们看到各种冗余、繁多的「传输层对象」,随着时间的推移,系统中堆积的「传输层对象」越来越多,不仅增加了系统的「复杂度」,而且还降低了我们的「开发效率」。我猜这也是人们说 Java 复杂的一方面原因吧。

更重要的是,万一有一个字段发生变化,更改量就很大。(当然这也有解决方案)

A2:继承关系的尴尬实现

继承是面向对象的一个重要特性,而关系数据库却难以复现对象世界中的继承关系。

我们来试着还原一下上面的继承关系吧。

如果我们按照把 StudentProfessor 建成两张表,问题就是:关系数据库分割了两个对象的共性 Person从语义上说:也就是将一个对象分割成两个部分了;而且当你要获取这个对象时,需要两次Select。同样道理增删改查都要两次。

如果我们把 StudentProfessor 合并成一张表,问题就是:会产生许多空白字段。这很容易理解。

这些都反映了面向对象和关系数据库天然不匹配,只能一方作出妥协,并且大部分情况是面相对象作出妥协。

A3:类的复杂关系实现

当我们需要创建一个部门(Department),而一个部门将拥有多个教授(Professor)这样一个模型的时候,我们发现面向对象和关系数据库「表达方式」的是两种不同的形式:

  • 面向对象:
    我是一个部门,在我里面有很多的教授;

  • 关系数据库,由于外键会在 Professor 上:
    我是一个教授,我属于那个部门。

问题二:是一种数据模型,与业务脱节

没有一个「真实的人」会在支付一笔订单的时候说:(大概意思..)

=> 先通过我这个订单的编号找到原始在系统上的记录;
=> 把支付金额改成我实际支付的金额;
=> 把这个订单的状态修改成已支付
=> ……..

而一个「真实的人」会直接说:我为这笔订单付了xxx钱。

关系型数据库(Relational Database)的核心实体就是数据表,核心操作就是在定义好的数据表上的「CRUD」操作。这套东西实在是太好用了,也太深入人心了,以至于你能在好多地方都能看到这种将关系模式直接用作业务模式的系统:

比如我之前写的所有东西。(就拿我写的个人博客为例吧:https://github.com/wmyskxz/MyBlog)

问题出在:我的「Entity层」只是数据库表结构的一种映射用于承载数据,我的「DAO层」只是封装了对「Entity层」的增删改查,我的「Controller层」只是简单的把地址和对应「Service层」的对应方法做了关联返回结果给「视图层」,而我的「Service层」则大部分工作也只是在做一些「查询」、「拼接数据」的工作,这样的系统是声称套上了业务的外衣,而实则只是「皇帝的新衣」,几乎无法保证业务逻辑的正确性、完整性。

我还记得朋友问过我一个问题,大意就是有一部分的系统其实只是对数据库的简单封装,感觉就像是系统只是数据库的「简单代理」一样。我一开始有点儿感同身受,但现在回过头想,只是我们当时做的东西太简单了而已。简单的系统也就是对数据库的「CRUD」。

但这还不是重点,重点是大部分的「CRUD工程师」对「业务理解」出了问题。

让我们拿国际象棋举个例子:

  • 图片引自:https://zhuanlan.zhihu.com/p/25442175

作为一枚「CRUD工程师」,在完成了左边部分的数据库设计和右边的数据展现之后,往往就认为已经万事大吉了。但这样的产品交付之后,对现实中使用它的用户提出了很多的潜在要求。「CRUD工程师」从来不会提示你这些潜在需求,谁会对自己并不知道的事情加以说明呢?

简而言之,这样的一个国际象棋程序,自身对国际象棋规则完全是一窍不通的。就是拿出个表格给你,随你填成啥样。在这件事情上,完全指望使用者不犯错,这是何等的心大!

  • 图片引自:https://zhuanlan.zhihu.com/p/25442175

于是,这个国际象棋程序完全有可能出现 Bad case 的这种诡异情况:黑色骑士(knight)走出一个华丽的斜线,和其中一个白色兵(pawn)共处一室(什么鬼?!)「国际象棋填表系统」并不会阻止你这样做,因为它并没有正确与错误之分。

这时候,「CRUD工程师」被客户、老板抓出来收拾残局了。经过一番调研,原来客户是想把黑色骑士走到 6d,并吃掉(capture)另一个白色兵。“产品已经够简单的了,客户怎么都这么蠢?”「CRUD工程师」嘀咕道,“哎,这工作坑真是多啊”。

  • 原片段摘自:https://zhuanlan.zhihu.com/p/25442175

问题三:CRUD 缺少意图(intent)

事实上我们可以使用「CRUD」架构很好的服务绝大多数的应用。但是正如上面提到的问题所说的那样,当系统的「复杂度」上升的时候,「CRUD」可能会缺少一件事:意图(intent)。

例如:

我们想要改变一个 Customer 的地址,在「CRUD」体系中,我们只需要发出更新语句就能实现。但是我们无法弄清楚这种变化是由不正确的操作引起的,还是客户真的转移到了另一个城市。也许我们有一个业务场景,需要再重新定位时触发对外部系统的通知。在这种情况下,「CRUD」显得有所缺失。

问题四:实施协作“困难”

在大多数的「CRUD」应用中,最新的更改将覆盖其他用户并行执行的其他更改。也就是说如果一个团队中的两个人同时对同一个文件的同一行进行修改,那么合并代码的时候就会产生「冲突」。

在上面我们论述了在传统「CRUD」这样的矛盾是如何产生的:散落在各处分散的逻辑代码。

问题五:被人诟病的「U」

「CRUD」中的「U」指的是「更新」操作。通常在我们的系统中「U」作为一种通用的方法可以更新资源的任何字段,然后使用新版本覆盖掉旧版本。

并且现在由于「REST」的流行,大多数的「API」都是围绕「资源模型」来进行「CRUD」操作的,这样做不仅确实极大地方便了开发人员的工作,并且借由「HTTP动词」和「资源URI」结合起来有很好的可读性。

但这有什么问题呢?

我们考虑一个简单的「银行账户」资源的问题。当我们需要把账户的余额更新为想要的数量的时候,我们应该允许客户端直接调用更新方法吗?任何余额调整的动作都应该作为某种类型的交易事务被记录下来才对,例如「充值」、「取钱」,还是「转账」?另外账户是否存在?可能变更吗?等等一系列问题都可能使你的通用「U」变得臃肿难以维护。

基于上述的多种多样的「场景」,我们的通用「U」方法被推向了尴尬的境地。事实上这可能属于设计的问题,不知道一般的公司中是如何解决的,至少在我之前写的代码中,我是这样实现的。(并且可能觉得没有什么问题)

另外也有的人说「CRUD」限制了描述业务的语言的问题。因为增删改查只有四个动词,而我们实际的业务场景可能更加复杂。

问题六:提供变更历史记录的操作很复杂

还有一个问题:「CRUD」会丢失应用程序的历史记录。例如,如果用户在一段时间内多次变更记录,我们则无法再跟踪单个更改。更糟糕的是,甚至无法确定该条目是否曾经被改变过。

当然,这可以通过为最后更新的时间戳添加字段来处理,但这只会帮助我们能够获得最新的更新。如果你对整个历史感兴趣,事情就会变得复杂:你必须从一开始就额外引入一组字段or一张新表。

这里的问题是:由于你不知道将来会询问哪些关于你数据的问题,因此你无法针对相应的情况对表做出优化。因为你收集太多或者太少的数据,似乎都存在一定问题。

总结

现在早已经不再是 PC Web 的时代了,原生 APP、移动 Web 等等多种客户端技术在近几年爆发(IOS、Android、JavaScript、…),青出于蓝而胜于蓝。原先「MVC」中的视图(Web页面)渲染工作,面临被新技术的完全替代。「CRUD工程师」手中的系统们,面临向「SOA」的转型。

夜深人静,四下无人的时候,「CRUD工程师」再次陷入深深的困惑:一边是臃肿不堪的模型和控制器层,另一边是逐渐收缩和服务化的视图层,难道建表、写表、读表就要成为我的唯一主题了吗?

「CRUD工程师」认为自己没有创造任何东西,他们只是数据库表的搬运工。而如果不是「CRUD」,业务系统后端工程师的价值在哪里?

理解并抽象出业务逻辑,建立满足需求的业务模型,以此设计实现出可靠的系统,并有效地控制复杂性。这才是大部分业务系统后端工程师的工作重点,也是解决他们工作中遇到的问题和难点的关键。

  • 观点来自:https://zhuanlan.zhihu.com/p/25442175

爱因斯坦说:“如果给我 1 个小时解答一道决定我生死的问题,我会花 55 分钟来弄清楚这道题到底是在问什么。一旦清楚了它到底在问什么,剩下的 5 分钟足够回答这个问题。”

虽然目前为止我们还不太了解「DDD」是如何帮助我们解决传统开发中的各种问题,但是听说「DDD - 领域驱动设计」似乎是能够用来设计和实现业务逻辑的一剂良药。

所以「Hello - DDD」

二、DDD 是什么?


「DDD」的全称是「Domain-driven Design」,即「领域驱动设计」。是由「Eric Evans」最早提出的综合软件系统分析和设计的面向对象建模方法,如今已经发展为一种针对大型复杂系统的领域建模与分析方法。

它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为,通过合理运用面向对象的封装、继承、多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。

  • 总结: 目前为止,您只需要知道「DDD」是一种致力于降低或隐藏整个系统业务复杂性,让系统具有更好扩展,应对纷杂繁多的现实也问题的架构方法就行了。

DDD 简史

  • 图片引自:https://www.jianshu.com/p/e1b32a5ee91c

领域驱动设计这个概念出现在 2003 年,那个时候的软件还处在从 CS 到 BS 转换的时期,敏捷宣言也才发表 2 年。但是「Eric Evans」作为在企业级应用工作多年的技术顾问,敏锐的发现了在软件开发业界内(尤其是企业级应用)开始涌现的一股思潮,他把这股思潮称为领域驱动设计,同时还出版了一本书,在书中分享了自己在设计软件项目时采用的建模方法,并为设计决策者提供了一个框架。

但是从那以后「DDD」并没有和「敏捷」一样变得更加流行,如果要问原因,我觉得一方面是这套方法里面有很多的新名词新概念,比如说「聚合」,「限界上下文」,「值对象」等等,要理解这些抽象概念本身就比较困难,所以学习和应用「DDD」的曲线是非常陡峭的。另一方面,做为当时唯一的“官方教材”《领域驱动设计》,阅读这本书是一个非常痛苦的过程,在内容组织上经常会出现跳跃,所以很多人都是刚读了几页就放下了。

虽然入门门槛有些高,但是对于喜欢智力挑战的软件工程师们来说,这就是一个难度稍为有一点高的玩具,所以在小范围群体内,逐渐有一批人开始能够掌控这个玩具,并且可以用它来指导设计能够控制业务复杂性的软件应用出来了。虽然那时候大部分的软件应用都是单体的,但是使用「DDD」依然可以设计出来容易维护而且快速响应需求变化的单体应用出来。

到了 2013 年,随着各种分布式的基础设施逐渐成熟,而「SOA架构」应用在实践中又不是那么顺利,Martin Fowler 和 James Lewis 把当时出现的一种新型分布式架构风潮总结成微服务架构

然后微服务这股风就呼呼的吹了起来,这时候软件工程师们发现一个问题,就是虽然指导微服务架构的应用具有什么特征,但是如何把原来的大单体拆分成微服务是完全不知道怎么做了。

然后熟悉「DDD」方法的工程师发现,由于「DDD」可以有效的从业务视角对软件系统进行拆解,并且「DDD」特别契合微服务的一个特征:围绕业务能力构建。所以用「DDD」拆分出来的微服务是比较合理的而且能够实现高内聚低耦合,这样接着微服务「DDD」迎来了它的第二春。

DDD 思辨

从计算机发明以来,人类用过表达世界变化的词有:电子化,信息化,数字化。这些词里面都有一个 “化” 字,代表着转变,而这些转变就是人类在逐渐的把原来在物理世界中的一个个概念一个个工作,迁移到虚拟的计算机世界。

但是在转变的过程中,由于两个世界的底层逻辑以及底层语言不一致,就必须要有一个翻译和设计的过程。这个翻译过程从软件诞生的第一天起就天然存在,而由于有了这个翻译过程,业务和开发之间才总是想两个对立的阶级一样,觉得对方是难以沟通的。

于是乎有些软件工程界的大牛就开始思考,能不能有一种方式来减轻这个翻译过程呢。然后就发明了「面向对象语言」,开始尝试让计算机世界有物理世界的对象概念。面向对象还不够,这就有了「DDD」,「DDD」定义了一些基本概念,然后尝试让业务和开发都能够理解这些概念名词,然后让「领域专家」(这里你可以理解为熟悉业务的人)使用这些概念名词来描述业务,而由于使用了规定的概念名词,开发就可以很好的理解领域业务,并能够按照领域业务设计的方式进行软件实现。

这就是DDD的初衷:让业务架构绑定系统架构。

后来发现这个方法不仅仅可以做好翻译,还可以帮助业务划分领域边界,可以明确哪个领域是自己的核心价值所在,以后应该重点发展哪个领域。甚至可以作为组织进行战略规划的参考。而能够做到这点,其实背后的原因是物理世界和虚拟世界的融合。

三、为什么使用 DDD?


DDD 帮助解决微服务拆分困境

上面介绍了使用DDD可以做到绑定业务架构和系统架构,这种绑定对于微服务来说有什么关系呢。所谓的微服务拆分困难,其实根本原因是不知道边界在什么地方。而使用DDD对业务分析的时候,首先会使用「聚合」这个概念把关联性强的业务概念划分在一个边界下,并限定「聚合」和「聚合」之间只能通过「聚合根」来访问,这是第一层边界。

然后在「聚合」基础之上根据「业务相关性」「业务变化频率」「组织结构」等等约束条件来定义「限界上下文」,这是第二层边界。有了这两层边界作为约束和限制,微服务的边界也就清晰了,拆分微服务也就不再困难了。

DDD 帮助应对系统复杂性

解决复杂和大规模软件的武器可以被粗略地归为三类:「抽象」、「分治」和「知识」。

  • 分治: 把问题空间分割为规模更小且易于处理的若干子问题。分割后的问题需要足够小,以便一个人单枪匹马就能够解决他们;其次,必须考虑如何将分割后的各个部分装配为整体。分割得越合理越易于理解,在装配成整体时,所需跟踪的细节也就越少。即更容易设计各部分的协作方式。评判什么是分治得好,即高内聚低耦合。

  • 抽象: 使用抽象能够精简问题空间,而且问题越小越容易理解。举个例子,从北京到上海出差,可以先理解为使用交通工具前往,但不需要一开始就想清楚到底是高铁还是飞机,以及乘坐它们需要注意什么。

  • 知识: 顾名思义,「DDD」可以认为是知识的一种。

「DDD」提供了这样的知识手段,让我们知道如何抽象出「限界上下文」以及如何去「分治」。

  • 图片来源:https://servicecomb.apache.org/cn/docs/crm-part-I/

另外一个感受就是我们可以使用「领域事件」来应对多样的变化。参考上面提到发邮件的例子,我们可以把它改造成这样:

public void setEquipmentBroken(Long id) {
    Equipment equipment = equipmentRepository.findById(id);
    equipment.broken();

    eventBus.publish(new EquipmentBrokenEvent(equipment.id));
}

这样,通知会议室预订者的模块就会去通知相应的人员,而不用我们自己操心了。

更为重要的是,「DDD」架构区别于传统的方式。

  • 图片引自:https://blog.pragmatists.com/domain-driven-design-vs-anemic-model-how-do-they-differ-ffdee9371a86

我们需要先了解一个概念:「贫血模型」。也就是只有属性的类,贫血的意思就是没有行为,像木乃伊一样。这种模型唯一的作用就是将一些 ORM 映射到对应的数据库上,而我们的「服务层」通过「DAO层」加载这些「贫血模型」进行一些拼接之类的操作,功能越复杂,这种操作就越频繁,这是我们的软件复杂度上升的直接原因。

而「DDD」则把大多数的业务逻辑都包含在了「聚合」、「实体」、「值对象」里面,简单理解也就是实现了对象自治,把之前暴露出来的一些业务操作隐藏进了「域」之中。每个不同的区域之间只能通过对外暴露的统一的聚合根来访问,这样就做了收权的操作,这样数据的定义和更改的地方就聚集在了一处,很好的解决了复杂度的问题。

DDD 帮助统一语言

在UML作为建模主流的时代,软件设计被明确分为面向对象分析(OOA),面向对象设计(OOD)和面向对象编码(OOP)阶段。实际操作中OOD的工作往往被OOA和OOP各自承担一部分,并同时存在分析模型和设计模型两个割裂的模型。

领域驱动设计的核心是建立统一的领域模型。领域模型在软件架构中处于核心地位,软件开发过程中,必须以建立领域模型为中心,以保障领域模型的忠实体现。

  • 图片来源:http://kaelzhang81.github.io/2017/10/20/DDD%E4%B9%8B-%E9%81%93%E6%9C%AF%E5%99%A8/

简单理解起来的话,也就是把业务人员和开发人员的语言统一起来,用代码来感受一下大概就是:

userService.love(Jack, Rose)  =>  Jack.love(Rose)
companyService.hire(company,employee)  =>  Company.hire(employee)

四、领域驱动设计过程


领域驱动设计强调领域模型的重要性,并通过模型驱动设计来保障领域模型与程序设计的一致。从业务需求中提炼出统一语言(Ubiquitous Language),再基于统一语言建立领域模型;这个领域模型会指导着程序设计以及编码实现;最后,又通过重构来发现隐式概念,并运用设计模式改进设计与开发质量。这个过程如下图所示:

  • 图片来源:http://zhangyi.xyz/overview-of-ddd/

这个过程是一个覆盖软件全生命周期的设计闭环,每个环节的输出都可以作为下一个环节的输入,而在其中扮演重要指导作用的则是“领域模型”。这个设计闭环是一个螺旋上升的迭代设计过程,领域模型会在这个迭代过程中逐渐演进,在保证模型完整性与正确性的同时,具有新鲜的活力,使得领域模型能够始终如一的贯穿领域驱动设计过程,阐释着领域逻辑,指导着程序设计,验证着编码质量。

如果仔细审视这个设计闭环,我们发现在针对问题域和业务期望提炼统一语言,并通过统一语言进行领域建模时,可能会面临高复杂度的挑战。这是因为对于一个复杂的软件系统而言,我们要处理的问题域实在太庞大了。在为问题域寻求解决方案时,需要从宏观层次划分不同业务关注点的子领域,然后再深入到子领域中从微观层次对领域进行建模。宏观层次是战略的层面,微观层次是战术的层面,只有将战略设计与战术设计结合起来,才是完整的领域驱动设计。

战略设计 (Do Right Things)

Ubiquitous language

领域驱动开发让业务专家(Domain Expert)和开发人员一起来梳理业务,而双方有效沟通的方式是使用通用语言,在这个项目里,一开始我们就定义了很多词汇表, 就是我们自己的通用语言。

Bounded Context 和 Domain

有了通用语言,词汇表 每一个词汇一定是有边界的,不同的边界内是不一样,比如你爱人在你家这个 Bounded Context 是你的 Wife, 但是如果她是一个老师,那么在学校这个边界里就是一个 Teacher. 我们经过多次讨论,采取的方法是拆成多个子系统(Bounded Context,是不是很像现在的微服务?),每个子系统进行自治。

随后我们把一个个业务抽象为领域对象(Domain Model), 每一个 Domain 对领域进行自治。而模型里的属性和行为表达为业务专家都可以理解的代码,用比如Job.Publish(). 虽然这里面最终产生了聚合根、实体、值对象等,但是我们和业务专家沟通的时候尽量不要说这些词汇,比如我们可以说, 在招聘这块儿,职位是不是必须经过公司进行管理,那样我们就知道 Job 是属于公司这个聚合根。 对领域进行“通用”(类名,方法名等都用自然语言表达)建模,业务人员可以直接读懂我们的代码,从而可以知道是否表达了业务需求。

战术设计 (Do Things Right)

在战术设计方面,由于业务行为和规则都在领域里,而且系统被拆分成多个子系统,这对技术实现上带来了非常大的挑战,尤其是大部分人都是有牢固的基于数据驱动开发的思想。 技术上有不同实现方式。

Event Sourcing

Event Sourcing 就是我们不记录数据的最终状态,我们记录对数据的每一次改变(Event),而读取的时候我们把这些改变从头再来一遍来取得数据状态,比如你有100块钱,现在剩下10块了,我们记录的不是money.total=10, 而是记录你每一次取钱的记录,然后从100块开始一步步重放你取钱的过程,来得到10.

一开始,我们写的过程中,时常回想起数据驱动的好,(每次开始一个新东西的时候,是不是很熟悉的感觉?),觉得用Event Sourcing各种麻烦,直到后来随着系统的复杂性不断增加,我们才感觉到带来了非常大的好处, 这个随后单独来说。

CQRS

由于使用了 Event Sourcing, 对数据查询,尤其是跨业务(Aggregate)的查询非常麻烦,很难像关系数据那样有查询优势,CQRS是解决这一问题非常好的方法,CQRS让查询和写入分开,把界面需要查询的数据进行原样写入,原样的意思就是界面显示什么样的,就提前保存成什么样的,类似于原来的缓存,没有任何join操作,这样查询是非常高效的。

  • 图片来源:http://vitiy.info/how-to-make-simple-cqrs-implementation-without-event-sourcing/

演进的领域驱动设计过程

战略设计会控制和分解战术设计的边界与粒度,战术设计则以实证角度验证领域模型的有效性、完整性与一致性,进而以演进的方式对之前的战略设计阶段进行迭代,从而形成一种螺旋式上升的迭代设计过程,如下图所示:

面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰的问题域。通过对问题域进行分析和建模,识别限界上下文,利用它划分相对独立的领域,再通过上下文映射建立它们之间的关系,辅以分层架构与六边形架构划分系统的逻辑边界与物理边界,界定领域与技术之间的界限。之后,进入战术设计阶段,深入到限界上下文内对领域进行建模,并以领域模型指导程序设计与编码实现。若在实现过程中,发现领域模型存在重复、错位或缺失时,再进而对已有模型进行重构,甚至重新划分限界上下文。

两个不同阶段的设计目标是保持一致的,它们是一个连贯的过程,彼此之间又相互指导与规范,并最终保证一个有效的领域模型和一个富有表达力的实现同时演进。

总结


结合自己的学习经过,本篇有意识的避免了繁杂纷乱的「新概念」。如果有兴趣详细了解「DDD」中的那些概念,可以参照这篇文章:http://qinghua.github.io/ddd/

借大佬的总结来收个尾吧:领域驱动开发好处多多,概念比较多,门槛相对较高,对人员要求较高,团队里至少需要有领路人,不然代价会比较大。 尤其慎用Event Sourcing, 而领域驱动尤其适合业务相对复杂的项目。 对那些很小的项目,CRUD仍然是好的选择。

乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,关注即可获取!


有趣的灵魂在等你

长按扫码可关注 


推荐阅读:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存