领域驱动战略设计-《复杂软件设计之道:领域驱动设计全面解析与实战》笔记 - 2

Scroll Down

1. Top Level

领域驱动设计共有两个部分:

  • 战略设计
  • 战术设计

战略设计也可理解为策略设计,是从宏观角度着眼于领域的分析设计,属于系统分析阶段,注重如何从有界上下文中寻找领域模型,战略模式由有界上下文、无所不在的语言和上下文映射组成

战术设计属于设计代码阶段,使用聚合、实体、值对象等对象类型概念表达领域模型

2. 有界上下文

DDD有界上下文属于DDD战略设计的重要概念。

有界上下文:领域即边界,边界靠分类,分类需从内外部入手。

有界上下文是指在空间或时间上有边界的一段环境背景,它确定了每个模型的适用范围,模型体现了这个范围内的逻辑一致

有界上下文是根据逻辑一致性划分软件的模式。也就是说:有界上下文是人根据客观事物中的一致性逻辑去划分软件。注意上下文不是软件中的模块,而是一个微服务。

2.1. 统一语言

统一语言是大家统一使用的术语,不会让别人产生歧义

统一语言是在整个团队的协商下发展起来的,这种无所不在的、通用语言必须在团队成员之间的任何场合都可以表达,以及可以用软件模型来表示。它是由团队中的开发人员、领域专家和其他参与者共享的语言。统一语言不是由领域专家指定的语言,而是应该由每个参与者共同协商,所以,类似ERP里面指定术语的方式是不可行的,因为即使人们被强行接受这些标准用语,对其理解也可能是片面的。它不应该在组织中由上而下,而应该由下而上整理出来

没有统一语言的后果:

  1. 由于缺少统一语言导致人们对其他人的概念需要进行“翻译”,这对领域模型建立非常不利,并会导致错误的领域模型,产品团队和技术团队的沟通产生问题大部分是因为这种翻译。
  2. 更多的公司是缺乏共同的统一语言,团队成员使用不同的术语而没有意识到。不同的术语带来理解的分歧。
  3. 很多公司实际上存在统一语言,但是却并不使用。
  4. 业务数据不开放导致人们接触不到统一语言。(可能因为安全问题,部分业务数据没有开放给所有人)

统一语言必须在领域模型中表达出来,主要体现在领域模型中的名称上。

2.2. 如何发现有界上下文和统一语言

方法论:

  1. 首先,可以通过画图的方式去发现。画图简单扼要,可以在白板上表达自己的业务领域,不要担心它们是否为正式设计;也可以事先使用UML工具画出UML图来表达自己的理解。最好的软件是没有软件,代码实现非常昂贵,因为代码实现涉及太多细节,一旦整个思考方向发生变化,所有的细节就要全部摧毁,重新来一次,而使用画图方式则很便宜
  2. 其次,通过专家小组会议创建词汇表,定义所有所需术语的词汇表。注意这个词汇表应该是在征询意见的基础上,切不可由上而下强行推广。改变人们的语言习惯很难,如同改掉方言口音一样。
  3. 最后,可以通过事件风暴的方式发现统一语言和有界上下文,领域专家和开发人员可以实现对业务流程迭代学习的快速循环,从而促进统一语言的发展。(后文会专门说事件风暴,这里先列在这里)
  4. 通过组织形式去发现。这里存在一个康威定理(ConwaysLaw),它的要义是:组织形式决定架构。它指出系统架构代表实现系统的组织的沟通结构。这是一种根据业务流程涉及的不同组织来划分上下文的方法,这种划分方法是一种朴素直接的方式,但是需要知道,业务流程是业务规则的表象,如果只是根据流程进行划分,有可能没有抓住领域的本质(流程和组织部门可能不断调整,属于变化的因子),造成软件系统成为其落后流程的附属品,可能并没有实现整合资金流、信息流和资源流等高级目相,但是这种方式有助于技术人员第一次接触业务时理解业务项目。

最终流程:

  1. 按上述方法论取发现有界上下文和统一语言
  2. 在过程中发现业务规则,这是一个业务逻辑强有力的约束
  3. 通过业务规则发现强烈的逻辑关系,也就是逻辑一致性,进而发现最小边界的有节上下文

这里贴一段原文作为补充:

2.3. 有界上下文之间的关系

有界上下文之间关系的处理基本原则是以松耦合、解耦合为主,因为不同的有界上下文有不同的团队、代码库、技术体系,如果两两之间过于耦合,就会发生两个团队经常在一起开会沟通、影响效率的情况,也可能是两个上下文的边界划分得不清晰,需要重新审视

常见的关系:

  1. 共享内核:比如两个有界上下文共同使用一份代码内核

  2. **开放主机服务也是一种上下文关系的映射,也称为上下游关系映射或API调用,一个上下文通过RPC等同步方式调用另外一个上下文的API,**调用者是被调用者的客户端。

    这种方式在如今的微服务架构中比较普及

    这种模式在新旧上下文系统之间使用时,需要引入防腐层,防止两个上下文系统直接耦合,旧的上下文系统会影响新上下文系统的代码编写思路,因此必须引入第三层解耦。

  3. 发布订阅模式

  4. 发布的语言(PublishedLanguage):两个有界上下文中的模型需要一种共同语言进行相互翻译转换

2.4. 核心子域、支持子域与通用子域

为了实现公司最终的大目标,必须在不同的小目标范围中工作,它们被称为子域,因为它们自身并不足以使公司取得成功,合起来以后共同构成了公司的业务领域

子域分为核心子域、支持子域和通用子域

  • **核心子域:这是必须尽最大努力的地方,正是它使公司发挥作用,为公司带来价值,使公司在竞争中脱颖而出。它是最重点的地方,业务策略和规则的重点实施地。**核心子域需要集中精兵强干进行DDD领域建模,将最善于解决复杂性的人力投放于此。
  • 支持(辅助)子域:是核心子域的辅助支持,介于核心与通用之间,如果没有它,核心子域无法成功,因此,它也是非常重要的;需要内部开发或外包,因为没有现成的解决方案来实现。支持子域可使用简单的解决方案,使用普通事务脚本或数据库CRUD模式就足够了,当然,在不会危及核心子域的前提下也可以外包。
  • **通用子域:是所有公司以相同方式执行的事务。它通常是现成的解决方案,但也可以外包或内部开发。**通用子域通过购买可能更便宜。

团队管理最佳实践:

**一个有界上下文对应一个子域,对应一个团队,对应一个微服务,**当然,也不能僵化执行这种对应关系,还是需要根据问题空间的子域划分、解决方案空间的上下文划分,以及团队人员能力等因素综合分析。

3. 按时间线发现有界上下文

首先是按照时间顺序对每个功能排序,然后再进入功能内部考察其职责。这个功能的目标是什么?主要关心的是什么?这些都有助于**发现业务规则和不变性约束,将逻辑一致的规则约束合并为同类项,也就是合并成一个有界上下文,这是一种通过合并方式划分有界上下文的方式。**这种方式的核心还是根据业务规则来判断有界上下文,业务规则本身只是逻辑一致性的另外一种表达而已。

DDD建模过程中需要一种语言来描述业务领域,可以使用UML语言(一套面向对象分析设计的统一语言规范),DDD中一般需要用到以下3种:

  • 用例图:表达需求用例,也就是将功能需求用图形表达出来。用例图基本是忠实地完整描述功能,表示用例和参与者之间的关系
  • 时序图:又称为序列图、顺序图,以可视方式模拟领域中的逻辑流程,能够记录和验证逻辑。时序图主要从逻辑顺序角度理顺需求,用于显示对象如何通信,侧重交互通信的顺序关系,可以说是对用例图从时间顺序上的进一步表达
  • 类图:表达类型与结构关系,类似数据表结构,但是有继承、实现等对象表达方式。类图主要用来表达从用例和时序图中推导的领域模型,是建模的最终结果

从整个时序图中鸟瞰所有功能发生的时间顺序,从而能不断迭代调整上下文的边界。这是一个迭代、动态的变化过程,有界上下文不是一次设计完成,设计好了就放到另外一张图中,还要将不同上下文纳入整个领域背景下串联起来,看看是否能完整覆盖整个领域边界。这是一种领域流故事法(Domain Flow Storytelling),有界上下文本身就是故事中的演员。因此,故事始于用户实现的功能目标,然后是有界上下文之间的交互,检查这样能否最终提供整个解决方案

4. 通过领域故事或流程发现有界上下文

**领域故事(Domain Storytelling)**是由Stefan Hofer在https://domainstorytelling.org提出的一种非常轻量级的工作坊形式,通过讲述故事的方式,专注于领域知识,通过领域语言来探索和理解用户流程、工作程序和整个业务流程与规则

领域故事的特点是能够按照字面意思,逐字逐句地“观察”和“改进”领域语言

这个是比较抽象的,这里整理了下书中的一个例子。假设分析航空货运运输的领域系统,技术人员会与相关管理人员或领域专家交谈:

  • 问:“你的工作做什么的?”
  • 答:“进行货运计划调度”
  • 问:“具体怎么回事?”
  • 答:“举个例子,客户需要托运一批货物,然后需要让路线小组规划路线,机场小组再分配计划,最后指定机位。”

最后一句回答中的“客户需要托运一批货物”,主语是“客户”,谓语动词是“托运”,宾语是“货物”,可以使用不同颜色表示它们:黄色表示主语,粉红色表示谓语,蓝色表示宾语,具体颜色取决于自己。

最后一句回答中的三句话其实代表了航空公司的内部流程。

  1. 路线小组进行旅程路线计算(主谓宾)。
  2. 机场小组进行分配计划(主谓宾)。
  3. 机组小组指派机组和机位(主谓宾)。

参会者使用这种讲故事的对话方式,然后将对话结果使用主谓宾的颜色便签贴到墙上,随着时间推移,整个墙面可能被铺满。这时会看到整个领域的一个大流程图,用这种方式应对复杂枯燥的业务领域相当有效

在这之后,可以使用流程标准语言BPMN来准确描述业务流程

领域故事和业务流程常常是相关的,关键是需要从中找到不容易变化的业务能力,根据业务能力划分有界上下文的边界

比如「根据组织架构划分有界上下文」与「根据能力划分有界上下文」比起来,尽管前者更容易,但是组织架构是容易变化的,因此根据后者来划分更合理。但是,没有组织机构作为主语,也难以发现谓语动词的能力。(作者原文说这是传统哲学和海德格尔哲学的区别)

DDD专家NickTune提倡使用填写表格的方式来逐步划分上下文边界,其表格内容如下:

  1. 有界上下文名称
  2. 业务策略分类:分为核心、通用、支持和其他几个类别。
  3. 描述
  4. 业务规则
  5. 统一语言
  6. 能力和职责
  7. 依赖:与其他上下文或服务的依赖关系。

这样的表格填写不是一蹴而就的,需要反复迭代,通过讲故事的形式反复和领域专家确认,注意其主谓宾用法,捕捉其语义中的业务策略、业务规则和能力,通过反复召开的头脑风暴会议最终确认。头脑风暴主要着重于寻找领域中的活动或事件,它们的一致点都是按时间线发现流程中的动词:动作行为、活动或事件。

5. 通过事件风暴会议发现有界上下文

时间线容易与流程混淆,并不是所有业务流程都是按照时间顺序编排的,有的流程可以并行发生,流程也可以组合,在事件风暴法中,通过发现发生的事件或活动来划分上下文边界,这种方法与流程更相关些,因为事件活动是流程的重要组成部分,而通过时间线发现上下文谈论的是从纯时间线来探索上下文边界

5.1. 关注关系而非实体对象

事件风暴是围绕领域事件展开的头脑风暴会议,领域事件是什么,这是领域专家对其业务领域感兴趣的、发生的事情与情况。领域专家对数据库、网络接口或设计模式并不感兴趣,但关注业务领域中会发生的事实(动词),而领域事件就是捕获这些事实

20世纪颇具影响力的哲学家维根斯坦说:世界是事实的总和,而非事物的总和。而传统哲学的观点是:世界是由存在着的万物组成的,他认为是世界是由发生着的事实组成的,这是他的《逻辑哲学论》中的基本出发点。**“存在着的万物”和“发生着的事实”两者的区别其实是主语和谓语动词的区别,“万物”通常是主语名词,而“事实”是一种动态的过程活动,涉及谓语动词。**从谓语动词中才能抽取出“万物对象”等名词的概念,他的逻辑分析观点认为:重要的不是对象,而是事实中对象之间的关系,对象所处的情势(事情的现实状况与发展趋势),对象只能在事实(对象之间的关系)中存在,离开事实的对象就毫无意义了。

在进行ER数据模型分析时,一般是先有实体(Entity)表,然后才表达关系(Relation),**维根斯坦逻辑思想的革命性在于:先有关系,才有实体,实体在关系中才有意义。**DDD中的领域事件、有界上下文和聚合都是这种关系的不同表达形式,或者说,数据库的关系表往往才是关注的重点,它是业务逻辑的关键所在。

5.2. 领域事件

在DDD建模领域,使用“领域事件”代表“事实”,表示对象之间的关系,既然领域事件表达了关系,需要涉及至少两个对象,领域事件通常也用于表达两个以上有界上下文之间的通信交流的方式

事件代表过去发生的事件,它既是技术架构概念,也是业务概念。以事件为驱动的编程技术模型称为事件驱动架构(EDA),这里强调的是事件的业务概念。

一个事件代表某个已经发生的事实,在计算机系统中,事件是由一个对象表达,其包含有关事件的数据,比如发生的时间、地点等。这个事件对象可以存在于一个消息或数据库记录或其他组件的形式中,这样一个对象称为“一个事件”。

事件概念在业务系统中应用,诞生了领域事件和事件溯源等DDD实现方式:**通过引入事件,像引入服务概念一样,跨越业务和技术鸿沟,同时又能表达函数编程式思维。**在业务上将事件和DDD结合在一起,可以形成统一语言DSL。

领域事件的命名需要使用过去式,表示已经发生的事情。因为领域事件表示与领域相关的事件,不能简单说员工已创建、已删除,而是已雇用、已解雇、已退出。命名需要代表深刻的业务领域含义。

命名反例:

  • SomethingChanged
  • SomethingUpdated

比较好的命名:

  • SomethingAssigned(分配任务业务)
  • SomethingUnassaigned(未分配任务业务)

5.3. 命令

命令是事件的“触发器”,从UX(用户体验)设计角度看,命令体现了人的一种意图,因为有这个意图,所以才触发这个命令。命令进入领域系统后,系统是否响应执行这个命令,还是要依据领域自身的逻辑:如果逻辑允许,领域系统正确执行了这个命令,那么意味着系统内已经发生了一个事实,这个事实就是领域事件;如果违背领域逻辑,系统可以拒绝执行命令

命令和事件的区别可以总结如下。

  • 命令表达一个正在发生的动作,有待执行;命令是职责目标,是方向的确定,是主动的
  • 事件代表已经发生的某个事情,必须响应;事件是不可控的,因为已经发生,无法改变
  • 命令是请求执行,而事件是执行已完成
  • 命令命名和事件命名也有区别的,命令一般是动词在前,或结尾使用“ing”表示正在发生的动作,区别于事件命名中结尾使用“ed”表示过去式的动作

命令+事件的模式符合意图、执行和结果的范式,日常生活中这种方式很多,例如上级给下级命令,下级一定要执行,并返回结果。

命令在领域事件之前,是领域事件发生的因,但是在一个业务流程中,上一环节的事件可能直接变成下一环节的命令,这样就组成了一条因果链,通过这样的命令+事件建模分析,可以发现流程能力环节和有界上下文:

%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E6%88%98%E7%95%A5%E8%AE%BE%E8%AE%A1%2072eae/Untitled.png

命令/事件只适合在发布订阅的有界上下文关系中。

5.4. 事件风暴建模法

事件风暴(EventStorming)是一种用于快速探索复杂业务领域的研讨会格式。

开一场事件风暴会议,领域专家、产品经理和技术人员等都参与其中,其中包括那些知道要问的问题(以及哪些人很想听听答案)以及知道答案的人,协调人必须使团队保持专注和参与,指导会议进展直到能完成完整的领域模型。

从领域事件开始探索领域,按时间线向前和向后发现领域事件,探索领域事件的起源。某些事件是用户操作的直接后果,因此使用不同颜色的便签区分命令和事件。

  • 蓝色便笺表示命令。
  • 橙色便笺表示事件。

在便签上写上命令或事件的名称,贴到墙上或白板上,所以需要有足够大的墙面。如果讨论变得热烈,就需要固定前进的目标方向,以下是一些标准路线和方向。

  1. 首先,探索子域。每个专家只谈自己最擅长的子域,将领域中的其他部分留给其他人。不同的责任区域可以很好地映射到不同子域中的一部分。
  2. 其次,探索有界上下文。在讨论过程中,可能会出现一些冲突领域,具有DDD思维模式的开发人员和推动者会对同一术语有不同解释,这可能是在共享的多个同一模型之间划分边界的好时机。
  3. 然后,注意草绘用户角色。在谈论命令时,对话倾向于指向发出命令的有界上下文和触发动作的人:谁在什么情况下发出这些命令?谁在什么场景下发出这些命令?可以使用黄色便签表示主语角色。
  4. 最后,识别关键测试场景,因为除了明确定义验收测试之外,没有其他更好的方法来消除歧义。最好有一些图表输出。

5.5. 业务平台与中台设计

如果发现大部分的相关信息暴露给了很多其他产品(有界上下文),这时可以抽象设计更通用的产品,主要针对使用者角色设计该产品,并公开一个更简单的服务;但是如果在整个业务平台找不到对应的使用者角色,则通过数据复制或重复知识方式实现不同产品或上下文之间的解耦。

总之,将有界上下文与产品和角色联系起来,能够从一个更高的高度也就是从业务知识层次去划分团队,提高团队生产效率

5.6. 微服务与有界上下文

“微服务就是有界上下文”是一种过分简化。微服务系统中存在四种有界上下文类型。

  • 服务内部(的上下文)。
  • 服务的API(的上下文)。
  • 服务集群(的上下文)。
  • 服务之间的交互(的上下文)。

6. 补充

6.1. 理解DDD中的“服务”一词。

根据OASIS定义,**“服务”是一种允许访问一个或多个功能的机制,也就是提供操作功能以便于被访问。**比如“商品组件”这个术语不是一种服务,它的操作应该是“提供商品操作的功能”,可以表达为“商品服务组件”是较为准确的。