DDD领域驱动设计与软件复杂度的那些事

2021-11-11 From 朝闻道 By 朝闻道

软件开发领域中,软件复杂度是一个由来已久的话题,从软件的诞生到成熟再到消亡,或多或少总会伴随着软件复杂度的讨论。

我们不禁发问,软件复杂度究竟从何而来?

谈到软件复杂度,有三个话题不得不提及,他们分别是软件规模,软件结构,以及业务的变化。

首先是 软件规模,它涉及到软件本身的代码量,迭代时长以及迭代的次数/数量,以及该软件经手的开发者数量等。这几个要素都会对软件规模造成显著影响,可以回想一下自己接手别人祖传代码时那种忐忑不安、小心翼翼的心态,生怕因为自己的失误导致出现bug甚至背锅的情况;并且代码规模越大,业务逻辑越冗长,这种心态就越发强烈。

接着说说 软件结构。一般来说,设计质量高的软件系统都有着明显的共同特征:比如良好的架构、规范的接口设计、清晰的业务逻辑与注释、完备的文档、代码具有良好的扩展性,符合开闭原则等等。而质量低下的代码则各有各的恐怖之处:比如到处乱放的实体、随处引用的service、深不可测的嵌套、冗赘的分支、一眼望过去不明所以的结构、核心逻辑与校验代码放在同一个方法中,一屏幕显示不全、缺乏甚至完全不存在有意义的注释…… 对于这种不规范的软件结构所引发的业务复杂度,我想大家也是感同身受了。

最后是 业务的变化,简单说就是需求变更。相信这也是每个开发者经常面临的状况,因为需求变更我们与产品经理相爱相杀,因为需求变更我们经常见证凌晨四点的城市,也因为需求变更让我们在设计的时候不得不考虑可配置、易扩展。之所以需求频繁变更/业务经常变化,主要原因在于如今的互联网,风口频频诞生,机会稍纵即逝,从增量市场逐步过渡到存量市场,获客难度指数级增加,互联网玩法也日新月异,这个过程中又伴随着业务量的激增,产品与运营每日都在被无休止的“增长”KPI压力中裹挟,一定程度上也为需求频繁变更推波助澜。

那么我们应该如何应对软件复杂度呢?

针对软件规模日益庞大,我们需要对复杂的问题域分而治之,化大为小,各个击破。通过分治思想,控制软件规模。

通过对单体应用进行拆分,化大为小,将单体的演化为分布式的;单体拆分的过程也伴随着对代码的重构过程,通过对模型进行分类和拆分,逐步提升通用模型的重用程度,让每个模型的行为都尽量满足单一职责设计原则;通过自顶向下,逐步细化的结构化编程思想,对业务流程进行细粒度的重构和优化,让业务逻辑呈现出清晰的主脉络,不至于化成一锅粥,一眼望过去不知其所以然。

针对软件结构复杂的问题,解决的核心思路是保持软件清晰一致的结构。说起来还是蛮简单,实现起来还是存在相当的复杂度。原因在于,不同的业务,问题域不同,而问题域本身的难易程度又是软件复杂度的决定性因素之一。但是即便如此,深耕软件领域多年的大师们还是总结抽象出了一系列的抽象软件结构(架构模式)供我们参考学习。

比如bob大叔提出的整洁架构;比如DDD的多种分层模式(如:四层架构、五层结构、六边形架构、洋葱架构等);这些各具特色的架构模式,他们都具备分离关注点、划分问题域边界、依赖倒置、隔离变化易扩展的特点。这些特点恰恰都体现了通用的优秀设计原则。

针对软件变化迅速这一问题,我们认为要积极的拥抱变化,既然变化不可逆,那么顺应变化就好。但是在顺应变化的同时还是要尽量对变化因素进行隔离,控制变化范围。我们提出三个要素来拥抱变化:

  • 可进化性:对软件涉及到的问题域划分单元边界,确定每个设计单元应当履行的职责以及需要与其他单元交互的接口;这些设计单元具备不同的设计粒度,如:函数、对象、模块、组件以及服务等;这其中涉及到的每个设计单元都拥有自己的边界,并且边界内实现细节不会影响到外部的其他设计单元,相当于隔离了关注点,这样做的结果就是我们可以非常容易地替换单元内部的实现细节,保证了它们的可进化性;

  • 可扩展性:为了满足系统的可扩展性,关键点在于我们需要学会识别软件系统中的变化点(业务热点)。常见的变化点包括但不限于业务规则、算法策略、外部服务、硬件支持、命令请求、协议标准、数据格式、业务流程、系统配置、界面表现等。处理这些变化点的核心就是“封装”,通过隐藏细节、引入中间层等方式来隔离变化、降低耦合。常见的一些优秀架构风格,如基于事件的集成、引入管道—过滤器等模式,都可以在一定程度上提高系统可扩展性。

  • 可定制性:这个特性意味着我们可以提供定制化的功能与服务。Roy Thomas Fielding博士 (HTTP 和 URI 等 Web 架构标准的主要设计者)在《架构风格与基于网络的软件架构设计》提到:“支持可定制性的风格也可能会提高简单性和可扩展性”。听起来有些抽象,我举个例子大家就理解了,比方说:在 SaaS 风格的系统架构中,我们常常通过引入元数据(Metadata)来支持系统的可定制。插件模式也是满足可定制性的常见做法,它通过提供统一的插件接口,使得用户可以在系统之外按照指定接口编写插件来扩展定制化的功能。比如Java的SPI机制、Dubbo的SPI和Filter机制等,都是可定制性的体现。它们都支持开发者通过它们提供的扩展点实现定制化的业务逻辑,这也是开放-封闭原则的体现。

DDD是如何应对软件复杂度的?

说了这么多软件复杂度以及宏观的应对方法,我们又不禁陷入疑惑:感觉说的都挺有道理,但是这和DDD又有什么关系?

实际上,我们说了这么多的主要目的就是在于引出DDD对软件复杂度的应对方法,正是因为DDD能够应对软件复杂度,为我们提供了一种能够从根本上化解软件腐化的解决方案,而这也恰恰是DDD的价值所在。

我们说过,软件复杂度的原因在于“变化”,而变化的根源因素在于 “需求的不确定性”。Eric Evans认为,“很多应用程序最主要的复杂性并不在技术上,而是来自领域本身、用户的活动或者业务”。

恰巧DDD的关注点正是 “领域 + 领域内以及领域之间的逻辑”。站在DDD的角度,软件系统的本质在于为客户/用户提供具有业务价值的领域能力和功能,从而解决具体的领域内的问题。

从Eric的话中,我们也能提炼出需求的分类:业务类需求和技术类需求。

对于业务类需求而言,它的复杂度核心在于业务复杂度。它包含多个方面:功能数量日益膨胀、功能之间的依赖关系逐步增加、不同涉众之间沟通交流不顺畅,不具备统一的语言交流模型;当这些不良因素聚集起来,到达一定程度,到达腐化的一定的程度,便能够看到经常被开发者诟病的“大泥球“、“shit山”。

对于技术类需求,或者另一种说法叫质量需求而言,它的核心复杂度在于技术本身,比如安全要求、高并发要求、高性能要求以及高可用方面的要求。

DDD解决业务类需求以及技术类需求的方式是清晰且直捣黄龙的:

首先, 典型的DDD实现了业务复杂度和技术复杂度的隔离,通过分层架构隔离了关注点,举个例子,在传统的DDD四层架构中,DDD划分出了领域层、仓储层、基础设施层、接口层;
在领域层中,存放业务逻辑的关注点,即所谓的领域行为;在应用层中,DDD暴露出了 业务用例级别 (Use Case)的服务接口,粘合业务逻辑与技术实现;在基础设施层中,DDD集中放置了支撑业务逻辑的技术实现,如:MQ消息发送、对缓存的操作等;在仓储层中,DDD放置了和领域状态相关的逻辑,打通了领域状态持久化与存储设施之间的联系。

比方说,在一个电商系统的领域逻辑中,业务规则包含了校验订单的有效性、计算订单总金额、提交和审核订单流程等业务逻辑。而技术关注点则从实现的层面保障这些业务能够正确完成,包括确保分布式系统之间的数据一致性,确保服务之间通信的正确性等。

除了划分不同分层外,DDD还提出了一个建设性的概念: “限界上下文(Bounded Context)”,通过限界上下文对业务流程分而治之,切分为不同的子系统,在每个子系统中利用DDD的分层架构/六边形架构等思想分别进行逻辑分层。通过这样的分治之后,DDD帮我们将业务复杂度隔离到了每个细分的领域内部,而且DDD本身的分治思想,也帮助我们隔离了业务需求和技术需求的关注点。

这么一通说明之后,感觉对DDD应对软件复杂度的方式有了一定的了解,但是总觉得不那么深刻,一方面是因为这个问题本身就不容易把握全局,还有个原因是没有一个清晰直观的图谱能够参考。稍安勿躁,我们这就通过一个图片来直观感受一下DDD分层结构的魅力:

ddd-structure.png

这是一个典型的领域驱动设计分层架构,蓝色区域的内容与业务逻辑有关,而灰色区域的内容则与技术实现有关。这二者泾渭分明,最后汇合在应用层。
应用层确定了业务逻辑与技术实现的边界,通过直接依赖或者依赖注入(DI,Dependency Injection)的方式将二者结合起来。充分体现了DDD能够隔离技术复杂度与业务复杂度的特点。

回到问题,我们为什么要用领域模型指导系统建模与落地?

结合讲解,我们再次回到问题本身,为什么要用领域模型指导系统建模与落地?

经过上面的一系列分析与思考,答案呼之欲出:正是因为DDD本身具备的这些特质,它的核心概念 领域模型 本质上就是对业务需求的一种抽象,它表达了领域概念、领域规则以及领域概念之间的关系。

在领域建模过程中提取出的统一语言,会成为具体落地的指导思想,通过它能够降低开发者与需求方的沟通成本,化解歧义;通过 剖析业务需求,抽象领域概念,提炼领域知识,总结统一语言;并运用抽象的领域模型去表达,就可以达到对领域逻辑的化繁为简。

这一切的核心便是模型,模型是对业务流程和参与者的封装,实现了对业务细节的隐藏、封装,赋予了实体以行为,这恰恰是面向对象的精髓:还实体以行为,变贫血为充血。

同时模型还是抽象的一种表达方式,它提取了领域知识的共同特征,并且保留了面对变化时能够良好扩展的可能性。

这么多内容,如果只用记住一句话,那么我们一定要知道:

我们之所以选择领域模型指导系统建模与落地,就是因为它能够很好的对业务需求进行抽象,更适合研发与需求方展开沟通合作,对于复杂场景的业务效果更为明显。

参考资料

《实现领域驱动设计》Vaughn Vernon著 滕云译

《领域驱动设计事件 战术+战略》 gitchat专栏 张逸

本文来源:朝闻道,转载请注明出处!

来源地址:http://wuwenliang.net/2020/12/02/DDD%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%E4%B8%8E%E8%BD%AF%E4%BB%B6%E5%A4%8D%E6%9D%82%E5%BA%A6%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B/

君子曰:学不可以已。
《黑匣子思维》
“黑匣子思维”是一种记录和审视失败并从中吸取经验的积极态度。不惧怕面对失败,反而视失败为学习的途径。不会否认过失、推诿责任和想方设法脱身,而会把失败作为样本深入研究。 缺乏从失败中学习的态度、勇气和能力,会对个体或行业带来严重危害。千方百计避免犯错并不是我们的目标,学习如何聪明而有意义地犯错,将每一次失败作为测试我们成绩的机会。
发表感想

© 2016 - 2022 chengxuzhixin.com All Rights Reserved.

浙ICP备2021034854号-1    浙公网安备 33011002016107号