一文探寻学习DDD的意义

《阿甘正传》中,阿甘开始了不停地跑步,一段时间后,后面就有了很多追随者一起跑,他们为什么跑哪?

类似地,一开始我也不知道 DDD 是什么,但当发现大家都在提 DDD、都在学 DDD 的时候,我也像跟跑者一样不由自主地加入了前行:既然有大牛提出了 DDD,既然那么多人趋之若鹜,那么肯定有可取的地方。

然而,有一天,阿甘停止了跑步,他不想跑了,追随者遇到了一个问题:我们还要跑么?当我们在学习 DDD 的过程中,感觉学而不得的时候,可能也会问:我们还要学么?这的确引人深思。

本文基于工作经验,尝试谈谈对 DDD 的一些理解,希望能够更好地探寻学习 DDD 的意义。

关注 DDD 的价值

无论做业务,还是做平台、中台,大家常常会被交错复杂的业务逻辑、晦涩耦合的业务代码搞得心力憔悴。我想,大家对 DDD 的追求,也是对轻松支撑业务发展的诉求,在探寻有没有合适的理论可以改善现状。毕竟,美好生活,共同向往。

分层支撑机制

我们选择各种框架、进行各种组织设计,核心是为了提高生产效率。但如果业务逻辑都是 case by case 地进行实现、缺少复用,那么研发成本是非常高的、投入周期也会非常长。

为了增加复用、缩短业务的落地时间,就需要很多通用的能力、产品。在我们的交付过程中,主要有两个层次:

分层支撑框架

腐化:业务逻辑渗透

这样的层次,看上去很美好:在起步阶段,由于缺少历史包袱,的确可以提升一定的生产效率,这是能力本身的收益。但是,越往后,随着业务接入的增多,业务之间开始互相影响,研发的阻力也越来越大。

研发效能降低的重要原因在于:更多的时候,我们还是按照 “业务能跑起来,怎么快怎么来 “的逻辑去做相关工作,遇水搭桥,遇山开洞,然后直达目的地,进行信息的传达、数据库字段的操作。

这样的过程,违背了我们” 希望通过业务场景,丰富平台能力,同时保证内核干净 “的初衷。能力应该是基于相对多的用例、相对完善的思考进行抽象,是横向统一看,有更深刻的理解,但是垂直的交付,让我们更加纵向地处理问题,往往只是 “窥探” 了链路,在交付时长和业务节点的限制下,很难想得更加全面、深刻,难以做出更通用的设计。

抵御:平台框架守卫

那么,为什么关注 DDD?如果说 DDD 直击了软件复杂度的核心——“问题域”,那可能还是比较抽象。具体来说,因为这的确符合我们追求的价值观【提升长期的生产效率】:

对 DDD 的关注,可以类比于我们对 “工匠精神” 的关注,对 DDD 的重视,也是我们对业务理解的重视。贴近业务,更要理解业务,不仅要理解业务,更需要理解大多数的业务。这样的追求,让我们往上看了一个层次,回归了最本质的问题:我们要解决什么问题?如何能够解决得更好?

学习 DDD 的困难

不知道大家是否有这样困惑:DDD 的学习过程好像是” 大海捞针 “的过程。即使能够捞到点东西,使用起来,还是会有种 “东施效颦” 的感觉,并不是很自然。为什么学习 DDD 那么困难呢?

感受:不得其门

论语中的下面这个场景,和我们的困惑是比较相似的:

叔孙武叔语大夫于朝曰:“子贡贤于仲尼。” 子服景伯以告子贡,子贡曰:“譬之宫墙,赐之墙也及肩,窥见室家之好;夫子之墙数仞,不得其门而入,不见宗庙之美、百官之富。得其门者或寡矣,夫子之云不亦宜乎!”

正如要感受到孔子达到的境界,自己的学问也需要有一定的积累。我们要感受到 DDD 的力量,自己本身就要成长到一定程度(如:经历了一些成功或者失败的设计,有自己的经验或者教训),才能形成共鸣和认同。

工作中,的确很少看到 DDD 的最佳实践。在复杂的业务面前,谁也没有勇气说,哪个软件结构是理想的设计:

因此,打开学习的大门不是几个案例就能一蹴而就的,需要结合我们自己的工作,慢慢累积、体会。

困难:模式发散

我们有一种困惑,到底怎么样算是 DDD:

无论往抽象看,还是往具体实现看,都很难找到令人信服的理论与实践(能够有确定性的落地能力)。因为这不像 23 个设计模式那样,可以通过 N 个模版就能涵盖大多数的模式。

为什么不能产生特定的模式呢?可以结合下图进一步来看:

两者不同的地方是:

因此,由于问题的抽象层次较高,各种策略的不确定性较高,很难在 DDD 中产生像”23 个设计模式 “那样精炼的模式。一定要有的话,也是一系列的模式,比较发散。

DDD 理解层次的类比

因此,我们渐渐明白:DDD 面向的是软件的 “软”,涵盖方方面面。DDD 的深入理解,需要 “千锤百炼” 后,才能明白那些深入简出的建议,才能体会那句 “师傅领进门,修行靠个人” 的真言,才能感受到 “众里寻他千百度,那人却在灯火阑珊处” 的美妙瞬间。

基于设计原则看 DDD

虽然 DDD 本身的实践可能千人千面,但是一些核心主题的思考应该是聚焦的,这些高频主题的理解,能让我们更好地进行设计,讨论的性价比也是较高的。接下来,会基于 “6 大设计原则”(solid 原则)为引子,去看看 DDD 中的一些关键理解。

单一职责原则:领域划分

单一职责是说:一个类应该只有一个发生变化的原因。职责的单一,可以更好地内聚,减少耦合,方便演化。

DDD 里面的领域划分可以类比思考。对领域的划分,无论是按领域实体,还是按照功能模块,还是按照服务等划分,其实都想尽量保证领域的正交,能够独立演化和发展。

Single Responsibility Principle:单一职责原则

领域怎么切分比较合适?刚进入业务平台的时候,了解到领域切分是按 “一个或多个实体对象” 的边界来切分。这的确比较合理,因为领域的核心职责就是对领域实体进行管理。但这是果,还是因?在切分的时候,是因为我们有了对领域的判断,所以某些实体被分在一起比较合适;还是因为某些实体有明显边界,所以可以形成一个领域?就比如下面的图:

聚类的例子

这的确引入深思。切分比较容易的时候,往往是因为已经有了行业的标准(如:电商系统有订单、支付、物流、库存等领域是比较合理的)。那行业的标准来自哪里?是来自于演化:

所以,领域的演化和划分,很类似 “启发式算法”(一个基于直观或经验构造的算法,在可接受的花费下给出待解决组合优化问题每一个实例的一个可行解):

往往到最后,我们会发现:

此外,从关联的角度看,往往我们看组织架构,就能看到领域的边界,核心原因还是组织架构也是要适应生产关系,follow 更优解的结构,是相辅相成的,也就能互相窥探。

开闭原则实体行为

开闭原则是说:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。也就是说,对扩展区块是要有设计的,扩展的部分不应该影响稳定逻辑。

在 DDD 中,实体的行为,在保证对外封闭的情况下,也是需要考虑扩展能力的。

Open Closed Principle:开闭原则

在刚开始学习 DDD 的时候,我们可能会强行把一些逻辑放到实体中,进行控制和收敛。但后面随着业务的变化,会发现在实体中承担行为逻辑很难受:

抛弃 POJO 的 get、set,走向实体的丰富行为,让我们编写代码更加困难了么?其实,我们的烦恼来自于,太关注实体行为的收口,忽略了扩展的设计:

但是,要突破这一个困境,能够在实体行为中设计扩展,其实要有这样的认同:要往上看一个层次,就是实体行为的表达,不一定只有一个类完成,可以通过策略模式等方式的路由,由一个模块中的一些类进行完成,只要对外有封装和管控其实就可以了。

突破一个类的限制,走向更多的类的协作设计,也是我们进阶的方向。

里氏替换原则:资源库

里氏代换原则是说:任何基类可以出现的地方,子类一定可以出现。讲究的是合理的抽象和复用。引人深思的一个例子是:正方形是特殊的长方形,正方形如果作为长方形的子类,那么当设置长度的时候就会出现冲突,长方形的长和宽可以独立设置,正方形的长和宽是有约束的,使用继承的关系就比较别扭。

在 DDD 中,关于可替代性,想聊一聊资源库。

Liskov Substitution Principle:里氏替换原则

【资源库的替代性需求】

在原来的分层的架构中,数据库等存储能力作为一种底层基础设施,是被视为稳定的下层服务的。但在实际的交付过程中,往往要遇到不同场景:

这些场景,让大家逐渐深信:当面向更广阔的市场,基础设施也是充满着不确定性,需要具备可替换的能力。

【存储实现间的复用策略】

在具体实现的过程中,并不是每个领域都会独立部署,有些领域因为组织、性能的因素会一起部署。往往这些领域的代码也是在一个项目模块中的。出于横向效率的考虑,会设计统一的存储框架。

不同设施的存储能力不同,但整个存储流程又是类似的(协议转换,存储语句生成、执行与事务,返回结果),这样在不同存储能力的过程复用方式上需要进行取舍(数据库、Tair 等是分开抽象还是统一抽象):

我想,“基于不同的存储能力,设计不同的模版框架” 应该是首选。大一统的抽象,开始时,人力成本可能低一点,但因为抽象层次较高,在理解与维护上将是一个 “人力成本黑洞”,随着时间的推移,会降低整体收益,长期看是得不偿失的。反之,不同的模版复用,最终可以获得更好的整体收益。

迪米特法则:领域协作

迪米特法则,又称最小知识原则,是说:一个软件实体应当尽可能少的与其他实体发生相互作用。应该和一些 “关键类” 进行沟通。

DDD 里面,领域间的协作,也需要相关的规划和设计。如果对领域之间的相互调用不做管理,那么链路关系会膨胀到难以理解的地步。


Law of Demeter:迪米特法则

设计模式中,无论是中介者模式,还是外观模式,都希望通过集中管理,让多对多的复杂关系,简化为多对一、一对多这样易于理解的结构。类似的,在领域协作与对外交付的过程中,往往可以增加一个协调层,去串联各个域的交互。这样即可以降低各域的协作成本,也可以降低外部的理解成本,有更好的研发体验。

协调层该如何产生?好比上课:虽然老师可以教,但是老师不在时,可以指定学生代理上课。学生虽然可以干,但教学技巧并不熟练,自己本身还有学习的职责,角色也很尴尬。下面将讨论一下协调层和域的角色关系。

【域能否成为协调层】

比较值得讨论的是交易里面 “交易域”、“订单域” 这样的概念:

没有实体,为什么会有 “交易域” 一说,本人是这么理解的:在交易流程等可以强管控的情况下,把交易的 API 服务当做域服务(如:下单),“交易域” 在逻辑上是有边界、可以成立的。但本质还是在管理订单,靠订单域成为了域,同时想沉淀协调能力。

【协调层能否成为域】

那么,如果订单的模型的管理不给交易管理呢,就是本人一直想的问题:如果没有自己的数据库实体,只有内存模型,纯粹靠对下游业务活动理解、数据流转的理解,能否成为一个域?

答案大概率可能是不行的:

协调者的角色,想要成为一个比较公认的 “域”,必定要自己持有数据模型,或者,借助基础域的一些数据模型,并享有管理的权限。

【协调层的名称】

无论是域想承担协调者的角色,还是协调者想发展成一个域,其实都不太符合职责单一的逻辑,但是这样 “兼职” 的现象却时常发生,核心还是开发人员角色的重叠。

既然协调层不太适合从域中选出,也不太适合成为一个域,那么介于业务活动、各个域能力之间的协调部分,应该称之为什么呢?目前看 “商业能力”、“解决方案” 这样的词汇都是比较合适的。

接口隔离原则:业务活动

接口隔离原则是说:客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。

DDD 里面除了领域建设的学习,也需要关注应用层如何更好地承接业务请求,并研究业务逻辑分割的依据。

Interface Segregation Principle:接口隔离原则

没有规矩,难成方圆

之前在业务部门做业务的时候,并没有业务活动、流程等相关概念,往往是基于业务需求写业务脚本,经验的多少会影响代码的优雅。但除了经验,大家并没有比较好的结构框架、原则,去承接应用层的各种业务逻辑,因此也充满疑惑:

没有规范的结果是,往往各有各的看法,谁都想立一套结构,谁也不服谁起的一套,各有各的代码区域。

平台的稳定框架

现在工作中,因为在平台,平台思考和治理的时间也比较久了,也有比较稳定的共识。整体的设计,在业务入口和业务入口之间、业务入口和稳定逻辑之间,预留了空间和扩展能力去承接场景化的逻辑,结构也比较确定:

反复 review 形成共识

分而治之,缩小大家的关注点,更好地切分协作,这样的确很容易理解并接受。但是要保证合理的切分,还是要有统一的共识和原则。

往往一致的形成,不是先一致共识,再切分,而是初步沟通后就尝试切分,再 review 达成一致,在曲折中前进。如果大家的看法、冲突较大,那么这个共识达成的过程就相对较慢。好在,这种切分也不是时常发生,也就大需求、大重构的时候。此时,预留的研讨时间、开发资源也是充足的。

依赖倒置原则:六边形架构

依赖倒置原则是说:程序要依赖于抽象接口,不要依赖于具体实现。

DDD 里面提到的六边形架构,也是进一步提升了抽象内核的地位,把领域建设作为架构的核心目标。

Dependence Inversion Principle:依赖倒置原则

以领域为中心,其实是一个比较重要的转变:

这样的转变,让我们有意识地将 “领域理解” 作为核心,形成行业竞争力,把 “知识” 作为资产进行售卖。

为了保证领域内核的抽象,需要定义好领域内核的边界,有两类接口:

可以看到,领域内核与外部之间通过接口进行解耦。对于更基础的服务,会被视为和业务入口一样的外部端口,属于应用层。比如,存储服务:

这样的架构,奠定了领域理解的抽象核心地位,让研发同学更加注重对业务问题的思考,建设更多 “有血有肉”、“贴近业务核心问题” 的软件,不仅仅是 “基础骨架”,让我们更加走近客户的价值,是应对软件复杂性过程中,大家比较认可的方向。

总结

对 DDD 的追求,来自我们渴望优雅地解决各种业务问题,希望有一套框架可以引导我们去分解问题,得到稳定、高效的生产效率。

但是这好比对 “永动机” 的追求,是一个难以有肯定答案的过程。能够解决软件复杂性的方案,必定是结合相关场景并且不断演化的,单纯追求 DDD 是得不到 “银弹” 的。

不过正如对 “永动机” 的研究,能让我们关注能量的转换过程,可以引导我们制造出更加高效的能源机器。对 DDD 的研究与追捧,能够让我们更加关注对业务的深刻理解,可以引领我们写出更加易于扩展的代码实现。

我想,正是 “业务的不断发展”、“软件的复杂性” 的存在,才让编程充满了挑战,才让大家对框架的研究充满热情,这何尝不是一个美妙的事情那~