作为一个领域驱动设计的实践者,我切实感受到了领域驱动这种方法论给软件工程带来的好处,同时也感受到了实践领域驱动的困难,这种困难体现在工程实践的方方面面,例如什么是领域驱动的最佳设计?如何把书本上的设计灵活的应用在自己的项目上?如何跟团队成员普及领域驱动,让大家也能明白这种开发思想?我深知理解并应用领域驱动设计的道路是曲折的,而让广大开发者的思想转变则是这条路上的第一步。
世面上有关领域驱动设计的书籍不多,但是也不利于入门,尤其是对刚入门的开发者,要理解领域驱动的思想更是难上加难。
本文尝试从领域驱动设计解决问题的目的出发,试图通过简单的描述来说明领域驱动设计这门方法论的思想。
为什么需要领域驱动设计
作为一个软件开发者,多数人以为自己的职责就是编写代码,然而软件开发不是工厂流水线,如果所有的软件开发者不停的开发新功能而不关心设计,那么软件开发过程将变得越来越复杂,关于这一点大家应该都有不同程度的感受。
软件开发工程师的工作是通过软件来解决问题,编写代码只是其中的一部分工作,设计和交流同样重要。而领域驱动设计就是一个让软件开发工程师交流和共享领域知识的途径。
共享领域知识的重要性
作为一个问题的解决者,能否理解和认识问题的前因后果至关重要。很明显,如果你只看到了问题的表面,或者对事实有曲解,你显然不会找到一个有效的解决方案。对于开发者,如果你编写的代码只是你的理解,而不是领域专家的理解,你如何保证上线产品的质量?
那么如何保证开发者编写的代码就是领域专家的想法?最简单的办法就是让领域专家来编写代码,但是这种方案可遇不可求,还有没有别的办法?
如果领域专家,开发团队以及代码能够共享一个模型,这将有效减少不同利益相关者的沟通及交流并且会确保所有人都在解决同一个问题。
这个想法要求开发者能够把代码设计为一个反映业务的模型,而这正是领域驱动设计的核心思想。
通过业务事件来理解问题域
为了在领域专家和开发者之间建立一个共享模型,收集需求并理解业务是第一步。收集需求和理解业务的方式多种多样,而事件风暴
<https://en.wikipedia.org/wiki/Event_storming>
经常被用来达到这一目的。业务逻辑可以看做是一系列状态的转换过程,而这些过程转换又被称为业务事件。比如“订单已提交”就是一个领域事件,如果把这个领域事件看做是订单业务的开始,通过梳理”订单已支付”以及”订单已出库”等后续的领域事件,就可以理解整个订单业务。此时对领域业务的理解被称为“问题域”。
把问题域划分为问题子域
通过事件风暴,开发团队和领域专家已经对整个”问题域”有了理解,但是现在着手解决“问题域”还有点早。当我们在面对一个大的问题时,自然而然会想到先将大的问题划分成若干个小问题,然后再考虑各个击破。经过上一个步骤,我们对问题领域已经有了很清楚的认识,接下来的一步就是把问题域划分为若干个小的问题域。我们有一个网上商城的问题域,能不能把它分割为小的问题域?
答案肯定的,我们把网上商城的问题分为:“订单”,“销售”,“市场”,“财务”,“采购”等若干个小问题域,再针对小的问题域分而治之。小的问题域在领域驱动设计中被称为“问题子域”。
使用限界上下文创建解决方案
理解了问题域并划分为问题子域并不意味着你就能创建出一个好的方案,你无法针对问题子域的所有信息设计出一个解决方案,你的解决方案只会专注于那些有助于解决该问题子域的信息,对于不相关的信息则会人为的屏蔽掉。
为什么叫限界?
在现实世界中,领域的边界很模糊,但是要设计一个好的解决方案,我们需要对问题子域加上一个边界,将不重要的信息排除在边界外。让解决方案专心解决重点问题。
为什么叫上下文?
每个上下文都代表着该解决方案的专业知识。在同一个上下文里,我们共享统一的语言和一致的设计。
通过界限上下文人为将问题子域限制在有限的界限内,你才可以着手创建解决方案。
创建统一语言
团队之间共享的术语和词汇被称为统一语言。统一语言用来定义业务领域的共享模型,当然可以用在项目的任何地方,包括需求分析和设计,最重要的是统一语言还需要出现在代码中。另外,统一语言在不同的界限上下文中往往不能够通用,例如在“认证上下文”中提到“用户”,在“机票订单上下文”中叫做“乘客”。
领域建模
有了界限上下文,让解决方案聚焦在最有用的信息里,你才可以着手建立共享模型。
如何才能建立一个不错的共享模型呢?
使用可视化的图示似乎是一个不错的想法,但实际上画出一个能够表达所有领域知识的图示并不是一个简单的工作;如果你有数据库开发相关的经验,你可能会想到通过表和主外键来表达领域知识,如果你有这样的想法那你就错了,在领域驱动设计中讲究通过领域逻辑来驱动设计和开发工作,而不是通过数据库模型来驱动开发。
在领域驱动设计中这一步叫做”领域建模
<https://www.amazon.com/Functional-Reactive-Domain-Modeling-Debasish/dp/1617292249/ref=sr_1_2?ie=UTF8&qid=1542099296&sr=8-2&keywords=domain+modeling>
“,你应该用代码建立一个反映领域知识的模型,这个模型跟领域专家口中的领域知识是一致的。领域模型是提供业务能力的核心部件,也是整个应用程序提供业务能力的核心。
领域建模中的其他概念
对于开发者而言领域建模至关重要,也是最考验开发者功底的一个环节。一方面开发者需要抽象出一个能够跟领域专家口中一致的模型,另一方面开发者还需要通过代码将这个模型表达出来。你需要恰如其分的使用一些面向对象的技巧把领域知识抽象到一个代码模型中,在这个过程中你需要了解”值对象”,”实体“,”聚合根“等概念,在此不再细说。
领域模型的持久化
在领域建模以及之前的步骤中,我们都没有提及数据库,因为领域驱动设计的核心是用代码建立一个共享模型,而数据库设计根本就不是领域驱动设计关心的内容。
但是终究我们还是要把领域模型的状态持久化到数据库中,有没有办法在不关心数据库结构的情况下将已经建立好的领域模型持久化?主流ORM的Code
First恰好匹配我们现在的处境,已经有一点为领域驱动设计而生的味道了。
但是即便是ORM的Code
First也会对领域模型有侵入,你可能需要根据不同的ORM为模型加上一些注解或者配置之类的代码,这跟领域驱动设计其实是相互违背的,我们希望用代码创建一个纯净的领域模型,这个模型封装着领域专家的领域知识,除此之外的代码都跟领域模型是无关的。
领域事件及事件溯源
解决上面问题的思路是引入领域事件和事件溯源。领域模型在提供业务能力的过程就是领域模型状态发生变化的过程,而一旦领域模型的状态发生了变化就会产生一个事件,这跟事件风暴中提到的业务事件是一致的,例如”用户下单“,订单模型提供”用户下单“的业务能力后发生了状态变化,我们通过定义”用户已下单“这样的领域事件来描述这种状态变化。事件溯源的思路就是只持久化领域事件,然后通过还原事件的方式将领域模型还原在最新的状态。React生态中的
Redux <https://redux.js.org/>就是通过事件溯源的方式来进行状态管理。
通过采取事件溯源,就可以将领域模型持久化跟数据库完全解耦。
微服务和领域驱动设计
我们通过领域驱动设计的思路来分析和发现问题域,通过分解把问题域划分为问题子域,通过人为加限制的方式将问题子域转换为限界上下文。而这个过程就是我们分解微服务的过程,一般来说每一个限界上下文都可以映射为一个微服务,但也不是绝对的,具体情况具体分析。
微服务的交互和集成
每一个微服务专注于解决对应的限界上下文中的问题,并不代表微服务之间没有交流。单个微服务的领域模型在提供服务的过程中会产生领域事件,领域事件为基于事件驱动(Event
based)的微服务集成提供了基础,如果在微服务之间架设一条消息总线
<https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications>
(不同于ESB,ESB被认为是反模式 <https://www.thoughtworks.com/pt/radar/tools/esb>
)。不同的微服务将自己产生的领域事件广播在消息总线上,微服务之间通过订阅自己感兴趣的事件就能完成微服务的集成。
HATEOAS和领域模型
迄今为止我们已经建立了领域模型,创建了微服务,通过消息和领域事件完成了微服务的集成。还需要把微服务的能力通过REST
API展现出来,微服务在对外提供能力的过程就是领域模型状态发生变化的过程,如果将领域模型理解为一个设计精良的状态机也一点不为过。如果设法将领域模型在某个状态下能够提供的能力通过REST
API的的返回结果表达出来,这就是HATEOAS的核心思想。REST API不但可以提供某种能力,还可以告诉消费者此时领域模型能够提供的其他能力。
结束语
本文从需求分析到API设计,试图描述领域驱动设计的过程及思想。同时也能看的出领域驱动设计并不是孤立存在的,他为解决开发团队和业务人员之间沟通而生,进而驱动微服务的设计架构以及API的设计,领域驱动设计并不是遥不可及的方法论,每一个专业术语和思想都是为了解决基本的问题而定义,希望本篇博客能够带你走入领域驱动设计。
热门工具 换一换