上一周,我参加了一个为期一周的 Event Storming 的工作坊,便想写一篇文章梳理一下对于 DDD 的理解。
好吧,我承认我标题党了。
所谓的 DDD,并非 Deadline Drive Deveopment(大雾),而是指领域驱动设计(Domain-Driven Design)。它是由 Eric Evans 在 2003 年写的《领域驱动设计:软件核心复杂性应对之道》一书中,提出的综合软件系统分析和设计的面向对象建模方法。
重要的事情先说第一遍:与这种软件设计模式相比,过程中的协作远比系统架构重要得多。
发展到了今天,DDD 已经变成了一套方法论,其贯穿在应用开发的生命周期里,从需求的分析到最终的编码实现。为此,它可以划分为三个阶段:
战略设计。提炼问题域并塑造应用程序的架构。为了保持模型的完整性,我们需要对业务流程进行梳理、统一语言,识别并划定上下文的边界,以及其对应的问题边界和问题域。
战术设计。围绕领域模型要素(如值对象、实体、聚合等)进行设计。用于创建复杂有界上下文的有效模型的模式集合。
战术实施。即通过选择合理的技术来实施架构,并对架构进行更详细的设计。(笑~~ ,可以通过架构金字塔的方式进行设计。)
所以,从某种意义上来说,DDD 又把软件设计拉回了瀑布时代——大量的预先设计。只是从粒度上比过去的瀑布式要小,还要将设计融入迭代中不断调整。
简单的项目并不需要 DDD,就好比一个人开发的项目,可能并不需要微服务——除非,在这个项目中使用了不同的优势语言,诸如于 AI 与 Python。(如果过程中存在 KPI,你当我没说过这句话)。
DDD 更适合于解决复杂的业务问题,并且其过程远比结果重要。业务越复杂,整个系统的架构便越复杂,设计出来的模块、服务间依赖也更容易出现问题。
DDD 无法解决纯技术问题。诸如于如何优先数据库性能、消息通信机制 。但是
与初创公司的生死项目相比,DDD 适合于中大型组织的长期项目——因为清晰、完整的业务规则。初创团队在业务上拥有太多的不确定性,无法确认出完整的业务全景,便无法保证建模的准确性。
这是 DDD 中最有价值的部分之一,通过统一语言达成共识。
实现 DDD 最大的挑战就是,花费大量的时间和精力来思考业务领域,研究概念和术语,并且和专家交流,以发现、捕捉和改进通用语言。——《实现领域驱动设计》
通用语言是团队(技术 + 业务)共享的语言,用于让领域专家和开发者使用它来进行沟通。举一个简单的例子,从技术的角度来看,我们对于获取(Get)、检索(Query)、查询(Search)的定义都是很明确的,而对于业务人员来说,他/她说搜索的时候,ta 可能要的是 Query 这种按条件过滤,而非 Search 这种任意条件的搜索方式。
通过不断消除领域专家和技术人员之间的歧义,可以避免谓地浪费及过度设计。并且在这个时候,领域专家知道问题的答案,而开发人员要提出问题。对于统一语言的命名来说,要尽量避免二义性,可以使用带定语的语句。用句人话来说,就是长的词语优于短的词语,比如笔记本电脑比电脑更有明确性。
定语是用来修饰、限定、说明名词或代词的品质与特征的。
宾语,也称受词,是指一个动作(动词)的接受者。
是的,在过程中,除了成为一个领域专家,也会成为一个语言专家的。所以,如果你们的表达都不好,你还需要一个语言专家(大雾)。如果你们刚接触一个新的领域,而技术人员都不是这方面的专家,那么越早统一语言,越能减少浪费。这也是我们在实施事件风暴的时候,最早看到的成果之一。
值得一提的是,知识提炼是一个持续的过程。
领域(Domain)是一个组织所做的事件,以及其中所包含的一切。问题域是我们在软件设计中,所需要解决的问题域。DDD 的整个过程,便是从问题域到解决方案域的过程。
围绕这个复杂问题域构建的大型系统,往往会由一组组件和子系统构成。而在解决方案中,某些部分会比其它部分有价值,我们需要关注于更高价值的部分,集中最好的资源在这上面。
为此,我们有了第一个基本的策略。
对于一个复杂的问题域来说,我们需要对它进行提炼,划分出相关的子域。通过区分出它的核心域、支撑域和通用域,来作为资源投入优先级的重要参考:
核心域。即系统的核心部分(差异化竞争力),也是业务的盈利来源,它需要投入最好的开发人员。
支撑域。非系统的业务核心,但是没有它系统就无法运转。由于存在部分的差异化,往往需要个性化的定制。
通用域。它不是业务的核心,但是在业内非常常见,有现成的解决方案。可以直接购买,或者使用初级开发人员。
为此简单来说,我们可以通过核心域开发,支撑域外包,通用域购买。
剩下的问题便是,如何划分出这些子域。
这是一个非常好的问题。在《领域驱动设计模式:原理与实践》一书中提到了多种有效的方式来理解问题域:
事件风暴。它是由 AlbertoBrandolini 提出的,一种用于领域驱动设计的协作设计方法。事件风暴基于现实业务流程,以系统实现为视角,通过一次只关注一个维度的分层抽象方式,将现实业务流程进行抽象并转化为系统实现的业务逻辑。
它的流程是,通过识别决策命令(decision command)和领域事件(domain event)的聚合(aggregate) or 业务承载物(carrier),并开始将聚合分组到有界上下文中。梳理限界上下文依赖关系,再划分问题子域中。在此过程中,识别关键测试场景,用户和目标并将其合并到模型中。最后,添加有古董界上下文之间的关系以创建上下文映射。然后用代码对所得模型进行挑战,以验证组学习并验证模型。
它通常由三个步骤组成:
头脑风暴,以识别领域事件。
识别事件触发器(决策命令)。
识别聚合(业务承载物)。
模型探讨旋涡(whirlpool)是由 DDD 的创造者 Eric Evans 创建的一个模型探讨漩涡的文档草案(domainlanguage/ddd/whirlpool/ )。这份文档提出了一种建模和知识提炼的方法,它能补充其它的敏捷方法,并能在应用的开发生命周期中随时使用。不过,它主要不是用来建模的,而是为了解决在创建模型期间遇到的问题。
除此,还书中还提及了如何更好地理解业务模型。
影响地图(Impact Mapping)。它是一门战略规划技术。通过清晰地沟通假设,帮助团队根据总体业务目标调整其活动,以及做出更好的里程碑决策,影响地图可以避免组织在构建产品和交付项目的过程中迷失方向。
理解业务模型。即通过商业画面来可视化业务模型。
刻意发现( Deliberate Discovery )。由 BDD 的创建者 Dan North 发布的提升领域知识的方法(
dannorth2010/08/30
/introducing-deliberate-discovery/ )。
如果说事件风暴是一个以技术人员为主的协作设计方式,那么影响地图则是一种以业务的 KPI 为主的协作方式。影响地图能够在知识提炼环境提出更好的问题。
值得注意的是,对于面向开发(如云服务)来说,它的核心域可能是技术相关的——这一点有些时候会让人困惑不已。
领域模型位于领域驱动设计(DDD)的中心,它是在知识提炼环节,通过开发团队和业务专家之间的协作形成的。它的主要目的是,描述领域中的复杂逻辑和策略,以便解决业务问题。
即其产生的过程是:大量的业务用例 -> 知识提炼 -> 领域模型。
DDD 战术模式的作用是管理复杂性,并确保领域模型中行为的清晰明确。这些构成模型的要素包含了:
实体,只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。它具有唯一标识和生命周期。
值对象,当你只关心某个对象的属性时,该对象便可作为一个值对象。它是实体的附加业务概念,用来描述实体所包含的业务信息。
在技术实践实现上,会通过以下的要素来完成设计:
领域服务(domain service)。封装了没有在模型中自然建模为值对象或实体的领域逻辑和概念。它的主要职责是使用实现和值对象编排业务逻辑。
领域事件(domain event)。它用于表明问题域中发生了一些业务人员关心的事情。在命名领域事件时,我们往往选择动词的过去分词,以明确表达事件的属性,其中文形式往往是『XXX已YYY』。
资源库(repository)。公开聚合根在内存中的集合的接口,提供聚合根的检索和持久化需要。
工厂(factory)。即在实体或者值对象创建复杂时,可以委托给工厂(模式)进行创建。。
聚合(aggregate)。是一种边界内的领域对象的集群,可以将其视为一个单元。可以封装一个到多个实体与值对象,用来维护该边界范围之内的业务完整性。
应用服务(application service)。
事件溯源。事件溯源是一种以事件为中心实现持久化的方法。在创建或更新一个聚合时,服务将一个或多个事件保存在数据库中,这时我们也将数据库称为事件存储。事件溯源通过加载并回放事件,重建了聚合的当前状态。
形成充血模型
经过 DDD 这一系列战术模式的操作,从某种程度上,可以避免贫血模型的产生。贫血模型是指使用的领域对象中只有 setter 和 getter 方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。
与此,同时,它还能减少上帝类(God Class,在整个应用中使用的全局类)的产生。
所以,反过来讲,DDD 可以有效地用于遗留系统的拆解与迁移。
从某种意义上来说,划分有界上下文的原则和 SOLID 中的 SRP(单一职责原则) 和 OCP (开闭原则)类似。
为了识别限界上下文,我们需要使用到先前提到的事件风暴方法。基于领域事件,分析领域模型,以驱动设计出我们想要的限界上下文。
除此,如《实现领域驱动设计》的译者张逸在文章中所说,也可以通过引用例场景分析来帮助团队剖析业务场景,驱动业务架构的设计。
随着近年来微服务的流行,我们经常将 DDD 与微服务配合使用,这不并意味着微服务是必须的,只是可以让微服务更香。DDD 和事件风暴可以让我们有理有据的划分模块与依赖。
通常来说,一个限界上下文就是一个微服务。
但是呢,出于成本和维护原因考虑,我们并不会这么去做。所以变更频率(步速),成为了我们在合并微服务的一个考虑。即,通常数据区分出不同的限界上下文的
变更频率
依赖项上下文
……
对应的我们也会有一系列的策略:各种独立更新的限界上下文,可以独立为服务;变动较小的部分,可以考虑合并在一起,以降低微服务的成本;相互依赖的限界上下文,可以考虑合并到同一个微服务中。
对于小型的团队来说,我们仍然可以将应用做成一个模块化单体应用,又或者是通过『应用微化架构』来在构建时拆分、运行时微服务化。
出于字数的考虑,这里就引用康威定律和康威逆定律。
康威定律,即『设计系统的架构受制于产生这些设计的组织的沟通结构。』
康威逆定律,如果想改变一个设计架构方式,首先要改变组织结构。因为很多构建架构(如微服务)的公司围绕服务边界构建团队,而不是按孤立的技术架构来划分。
当然不可避免的,会存在一些开发人员跨多个团队的存在。
既然所有微服务相关的书,都提到了架构相关的部分。(PS:那我也得复制一下,但是我比他们都多了一个整洁架构。)
分层架构。
六边形式架构。
CQRS 架构。
事件驱动架构。
整洁架构。(推荐,哈哈)
限于篇幅所限,这里就不详细描述了(我已经不想拷贝了)。
关于这部分的详细内容,可以考虑加入我的下篇文章,架构金字塔。
『软件工程学的历史过程,也就是人类试图通过建立简化方法,降低随着问题规模的扩大而提高的问题复杂度,从而不断对规模/复杂度动力提出挑战的历史过程』。—— Weinberg。
LB:“听说,『DDD 能提高团队的整体智商』”
AJ:“是的,因为 DDD 很南”
DDD 是一个有效的方式,但是如果 DDD 的落地方式过于复杂,一定会催生出更简化的方式。即:复杂度同力一样不会消失,也不会凭空产生,它总是从一个物体转移到另一个物体或一种形式转为另一种形式。