设计理念

设计理念

领域驱动设计的战略核心即是将问题域与应用架构相剥离,将业务语义显现化,把原先晦涩难懂的业务算法逻辑,通过领域对象(Domain Object),统一语言(Ubiquitous Language)转化为领域概念清晰的显性化表达出来。DDD 要避免业务逻辑的复杂度与技术实现的复杂度混淆在一起,确定业务逻辑与技术实现的边界,从而隔离各自的复杂度,业务逻辑并不关心技术是如何实现的。无论采用何种技术,只要业务需求不变,业务规则就不会变化。理想状态下,应该保证业务逻辑与技术实现是正交的。DDD 通过分层架构与六边形架构确保业务逻辑与技术实现的隔离。

  • 统一语言,软件的开发人员/使用人员都使用同一套语言,即对某个概念,名词的认知是统一的,建立清晰的业务模型,形成统一的业务语义。将模型作为语言的支柱。确保团队在内部的所有交流中,代码中,画图,写东西,特别是讲话的时候都要使用这种语言。例如账号,转账,透支策略,这些都是非常重要的领域概念,如果这些命名都和我们日常讨论以及 PRD 中的描述保持一致,将会极大提升代码的可读性,减少认知成本。。比如不再会有人在会议中对“工单”、“审核单”、“表单”而反复确认含义了,DDD 的模型建立不会被 DB 所绑架。

  • 面向领域,业务语义显性化,以领域去思考问题,而不是模块。将隐式的业务逻辑从一推 if-else 里面抽取出来,用通用语言去命名、去写代码、去扩展,让其变成显示概念;很多重要的业务概念,按照事务脚本的写法,其含义完全淹没在代码逻辑中没有突显出来。

  • 职责划分,根据实际业务合理划分模型,模型之间依赖结构和边界更加清晰,避免了混乱的依赖关系,进而增加可读性、可维护性;单一职责,模型只关注自身的本职工作,避免“越权”而导致混乱的调用关系。通过建模,更好的表达现实世界中的复杂业务,随着时间的发展,不断增加系统对实际业务的沉淀,也将更好的通过清晰的代码描述业务逻辑,模型的内聚增加了系统的高度模块化,提升代码的可重用性,对比传统三层模式中,很有可能大量重复的功能散落在各个 Service 内部。

    • 限界上下文:通过限界上下文对整个系统进行划分,将庞大的软件系统划分为松散耦合的多个小系统,基于业务进行垂直切割
    • 利用分层架构和六边形架构思想:利用 分层架构 与 六边形架构 思想对其进行逻辑分层,以确保业务逻辑与技术实现的隔离

接触到需求第一步就是考虑领域模型,而不是将其切割成数据和行为,然后数据用数据库实现,行为使用服务实现,最后造成需求的首肢分离。以银行账号 Account 为案例,Account 有“存款”,“计算利息”和“取款”等业务行为,但是传统经典的方式是将“存款”,“计算利息”和“取款”行为放在账号的服务 AccountService 中,而不是放在 Account 对象本身之中。DDD 让你首先考虑的是业务语言,而不是数据。DDD 强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。通过领域模型和 DDD 的分层思想,屏蔽外部变化对领域逻辑的影响,确保交付的软件产品是边界清晰的微服务,而不是内部边界依然混乱的小单体。在需求和设计变化时,可以轻松的完成微服务的开发、拆分和组合,确保微服务不易受外部变化的影响,并稳定运行。

设计原则

  • Focusing on the Core Domain: DDD 强调需要将最多精力集中在核心子域上,核心子域是产品成败的关键,它的是产品的独特卖点,是它被建造而不是购买的原因。核心领域,将为您提供竞争优势,并为您的产品创造真正的商业价值,所有团队都了解核心领域是至关重要的。

  • Learning through Collaboration: DDD 强调开发团队和业务专家之间合作的重要性,以产生解决问题的有用模型。如果没有业务专家的这种协作和承诺,很多知识共享将无法进行,开发团队也无法对问题领域有更深的了解。事实也是如此,通过协作和知识紧缩,企业有机会了解到更多关于其领域的知识。

  • Creating Models through Exploration and Experimentation: DDD 将分析模型和代码模型视为一个整体,这意味着技术代码模型通过共享 UL 与分析模型绑定。这意味着技术代码模型通过共享 UL 与分析模型绑定。分析模型的突破会导致代码模型的改变。代码模型的重构,揭示了更深层次的洞察力,又会体现在业务的分析模型和心理模型中。只有当团队有时间去探索一个模型并对其设计进行实验时,才会有突破性的进展。花时间进行原型设计和实验,可以在很大程度上帮助你塑造一个更好的设计。它也可以揭示出一个糟糕的设计是什么样子的。埃里克-埃文斯建议,每一个好的设计至少要有三个坏的设计,这样可以防止团队停留在第一个有用的模型上。

  • Communication: 有效描述为代表问题领域而建立的模型的能力是 DDD 的基础。这就是为什么,毫无疑问,DDD 最重要的一个方面就是创建 UL。如果没有一种共享的语言,业务团队和开发团队之间为解决问题而进行的协作就不会有效。团队之间在知识紧缩会议中产生的分析和心智模型需要一种共享的语言来将其与技术实现结合起来。如果没有一个有效的方式来沟通问题领域内的想法和解决方案,设计的突破就不可能发生。

  • Understanding the Applicability of a Model: 建立的每个模型都是在其子域的背景下理解的,并使用 UL 进行描述。然而,在许多大型模型中,UL 内可能存在模糊性,组织的不同部分对一个共同的术语或概念有不同的理解。DDD 通过确保每个模型都有自己的 UL,且只在特定上下文中有效来解决这个问题。每个上下文定义了一个语言边界;确保模型在特定的上下文中被理解,以避免语言的歧义。因此一个具有重叠术语的模型被分为两个模型,每个模型在自己的上下文中被明确定义。在实现方面,策略模式可以强制执行这些语言边界,使模型能够孤立地发展。这些策略模式导致有组织的代码,能够支持变化和重写。

  • Constantly Evolving the Model: 任何一个从事复杂系统的开发人员都可以写出好的代码,并在短时间内维护好它。然而,如果没有源代码和问题域之间的协同,继续开发很可能最终导致代码库难以修改,从而导致 BBoM。DDD 有助于解决这个问题,它强调团队要不断审视模型对当前问题的有用性。它挑战团队在获得领域洞察力时,对领域的复杂模型进行进化和简化。DDD 仍然不是什么灵丹妙药,需要奉献精神和不断的知识紧缩,才能生产出可维护多年的软件,而不仅仅是几个月。新的业务案例可能会打破以前有用的模型,或者可能需要改变,使新的或现有的概念更加明确。

DDD 价值

Eric Evans 在创造性地提出领域驱动设计时,实则是针对当时项目中聚焦在以数据以及数据样式为核心的系统建模方法的批判。面向数据的建模方法是关系数据库理论的延续,关注的是数据表以及数据表之间关系的设计。这是典型的面向技术实现的建模方法,面对日渐复杂的业务逻辑,这种设计方法欠缺灵活性与可扩展性,也无法更好地利用面向对象设计思想及设计模式,建立可重用的、可扩展的代码单元。领域驱动设计的提出,是设计观念的转变,蕴含了全新的设计思想、设计原则与设计过程。

应对复杂业务

引起软件系统复杂度的主要因素是需求,软件系统需求又可以分两个方面:业务需求和技术需求。我们分析系统的复杂度时就可以从业务复杂度和技术复杂度这两个维度出发。

业务复杂度跟系统的业务需求规模和需求之间的关系层级有直接关系,需求的数量和关系的层级决定代码的规模和逻辑循环或递归的层级,系统的需求数量越大,需求之间的关系越复杂,系统的业务复杂度就越大。John Ousterhout 的著作《A Philosophy of Software Design》从认知的负担和开发工作量的角度来定义软件系统的复杂度,并给出了一个复杂度公式:

$$ C=\sum_{p} \mathrm{C}{p} \mathrm{t}{p} $$

子模块的复杂度(cp)乘以该模块对应的开发时间权重值(tp),累加后得到系统的整体复杂度(C)。可以看到系统整体的复杂度并不简单等于所有子模块复杂度的累加,还要考虑该模块的开发维护所花费的时间在整体时间中的权重占比(tp),这个权重比就跟模块划分是否内聚、设计是否优雅有直接关系。

DDD 的核心思想就是要避免业务逻辑的复杂度与技术实现的复杂度混淆在一起,确定业务逻辑与技术实现的边界,从而隔离各自的复杂度,业务逻辑并不关心技术是如何实现的。无论采用何种技术,只要业务需求不变,业务规则就不会变化。理想状态下,应该保证业务逻辑与技术实现是正交的。DDD 通过分层架构与六边形架构确保业务逻辑与技术实现的隔离。

快速响应业务变化

不确定性和变化是这个时代的主旋律,业务需要快速上线,并根据用户的反馈不停地调整和升级,有生命力的业务主动寻求变化,不变则亡是很多行业目前的共识,企业应对变化的响应力成了成败的关键。同时一个长期困扰软件研发的问题是,需求总是在变化,无论预先设计如何“精确”,总是发现下一个坑就在不远处。相信很多技术人员都有这样的经历,架构和响应能力越来越糟糕,也就是我们常说的架构腐化了,最后大家不得不接受重写。软件架构设计的另一个关键方面是让系统能够更快地响应外界业务的变化,并且使得系统能够持续演进。在遇到变化时不需要从头开始,保证实现成本得到有效控制。

DDD 的核心是从业务出发、面向业务变化构建软件架构,实质是保证面对业务变化时我们能够有足够快的响应能力。面向业务变化而架构就要求首先理解业务的核心问题,即有针对性地进行关注点分离来找到相对内聚的业务活动形成子问题域。让每个字问题的划分尽可能靠近变化的原点,子问题域内部是相对稳定的,未来的变化频率不会很高,是符合深模块特性的,而子问题边界是很容易变化的。DDD 最后在实现层面利用成熟的技术模式屏蔽掉技术细节的复杂度。

与微服务相得益彰

Martin Fowler 和 James Lewis 提出微服务时,提出了微服务的 9 大架构特质,指导组织围绕业务组建团队,把业务拆分为一个个业务上高度内聚、技术上松散耦合、运行在独立进程中的小型服务,微服务架构赋予了每个服务业务上的敏捷性和技术上的自主性,因此可以针对每个服务进行独立地迭代、更新、部署和弹性扩展,从而缩短需求交付周期并加速创新。

在面对复杂业务和快速变化需求时,DDD 从业务视角进行关注点分离和应对复杂度的,让业务具备更高的响应力。DDD 战略设计阶段,引入限界上下文(Bounded Context)和上下文映射(Context Map)对问题域进行合理的分解,确定领域的边界以及它们之间的关系,维持模型的完整性。限界上下文不仅限于对领域模型的控制,而在于分离关注点之后,使得整个上下文可以成为独立部署的设计单元,这就是“微服务”的概念,上下文映射的诸多模式则对应了微服务之间的协作。因此在战略设计阶段,微服务扩展了领域驱动设计的内容,反过来领域驱动设计又能够保证良好的微服务设计。边界给了实现限界上下文内部的最大自由度。这也是战略设计在分治上起到的效用,我们可以在不同的限界上下文选择不同的架构模式和技术实现,这也正好映照了微服务的特点:在技术架构上,系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。

Thought Works 公司技术专家编写的《微服务设计》书中,专门有一章节“限界上下文”,充分说明微服务的落地需要 DDD 来辅助的,起码在建模阶段是需要借助 DDD 强大的战略模式来支撑的。微服务不是简单的指将服务尽可能的拆小,然后一个 RPC 框架搞定了,这太粗糙了,无法落地。

微服务与 DDD 关联

辅助中台战略落地

领域驱动设计让参与者基于统一语言沟通和协作,围绕一个统一和一致的领域模型工作,传统的分析模型和设计模型不再割裂;显式地把业务领域和设计放到了软件开发的核心,软件人员和业务人员合作来构建领域模型,使得软件的交付质量更高且维护成本更低;利用限界上下文来分解问题域,识别核心领域,有效分解和控制了业务的复杂度;利用 DDD 提倡的分层、六边形等架构,分离了业务复杂度和技术复杂度,使得系统具备更强的扩展性和弹性;战术层面提供了元模型(聚合,实体,值对象,服务,工厂,仓储)帮助构建清晰、稳定,能快速响应变化和新需求能力的应用;DDD 构建的应用能快速方便地切到微服务;领域驱动设计给企业应用带来的稳定性、灵活性、扩展性和应对变化的响应力对于建立灵活前台、稳固中台能带来巨大的帮助作用。

设计过程

领域驱动设计是一套面对复杂业务进行建模和设计的方法论和实践,建立了以领域为核心驱动力的设计体系。我们首先可以将领域设计模式分解到问题空间(Problem Space)与解空间(Solution Space)两大类中:问题空间的划分更多着眼于业务上值得注意的、重要的点;解空间则更关注与应用的组织架构,保证应用本身更易于管理。

问题空间

解空间

这里领域专家会起到关键的作用,他应该通晓研发的这个产品需要解决哪些问题,专业术语,关联关系。对于简单的业务领域,一个领域可能就是一个小的子域。领域建模过程相对简单,根据事件风暴可以分解出事件、命令、实体、聚合和限界上下文等,根据领域模型和微服务拆分原则设计出微服务即可。对于复杂的业务领域,领域可能还需要拆分为子域,甚至子域还会进一步拆分,如:保险领域可以拆分为承保、理赔、收付费和再保等子域,承保子域还可以再拆分为投保、保单管理等子子域。对于这种复杂的领域模型,是无法通过一个事件风暴完成领域建模的,即使能完成,其工程量也是非常浩大,效果也不一定好。

从领域驱动设计落地的过程中,又可以划分为战略设计与战术设计:

战略设计与战术设计

  • 在战略设计阶段,面对纷繁复杂的业务需求,领域专家和研发团队进行紧密合作、充分沟通,进行事件风暴或场景驱动设计,分析需求并提炼知识,得到比较清晰的问题域,输出由领域专家和研发团队达成共识的统一语言(UL,Ubiquitous Language),基于统一语言对问题域进行分析和建模,识别业务边界,确定限界上下文,根据限界上下文划分独立的领域,建立限界上下文彼此之间的关系,接着引入系统上下文(System Context)确定系统的边界,并确定它的外部环境,包括与其集成的第三方系统与基础设施。利用 DDD 分层架构或六边形架构界定业务领域和技术实现的边界,让稳定的核心领域模型处于架构的最内部,避免技术实现和架构变动带来的影响。

  • 接着进入战术设计阶段,一个大的业务问题被分解为多个限界上下文(问题域),团队视野和专注就可以聚焦到每一个内聚的限界上下文,进行战术设计。战术设计的重点是利用领域驱动设计的元模型对领域的复杂性进行分解和建模。领域驱动设计强调和突出了领域模型的重要性,通过整个领域驱动设计过程,绑定领域模型和技术模型,以保证领域模型和技术模型在贯穿整个软件开发的生命周期中(需求分析、建模、架构、设计、编码、测试与持续重构)的强一致性。领域模型指导着软件设计以及技术编码实现,接着通过重构实践来挖掘隐式概念,完善统一语言和模型,运用设计模式改进设计与开发质量。

以下是领域驱动设计的粗略过程:

领域驱动设计过程

对于这种复杂的领域,我们可以分三阶段来完成领域模型和微服务设计:

  • 拆分子域建立领域模型:根据业务特点考虑流程节点或功能模块等边界因素(微服务最终的拆分结果很多时候跟这些边界因素有一定的相关性),按领域逐级分解为大小合适的子域,针对子域进行事件风暴,记录领域对象、聚合和限界上下文,初步确定各级子域的领域模型。
  • 领域模型微调:梳理领域内所有子域的领域模型,对各子域模型进行微调,这个过程重点考虑不同限界上下文内聚合的重新组合,同步需要考虑子域、限界上下文以及聚合之间的边界、服务以及事件之间的依赖关系,确定最终的领域模型。
  • 微服务设计和拆分:根据领域模型的限界上下文和微服务的拆分原则,完成微服务的拆分和设计。