20

如何通过 DDD(领域驱动设计) 降低软件开发的成本?

 4 years ago
source link: https://www.tuicool.com/articles/JFJzymj
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一周,我参加了一个为期一周的 Event Storming 的工作坊,便想写一篇文章梳理一下对于 DDD 的理解。

好吧,我承认我标题党了。

DDD

所谓的 DDD,并非 Deadline Drive Deveopment(大雾),而是指领域驱动设计(Domain-Driven Design)。它是由 Eric Evans 在 2003 年写的《领域驱动设计:软件核心复杂性应对之道》一书中,提出的综合软件系统分析和设计的 面向对象建模 方法。

重要的事情先说第一遍: 与这种软件设计模式相比,过程中的协作远比系统架构重要得多

发展到了今天,DDD 已经变成了一套方法论,其贯穿在应用开发的生命周期里,从需求的分析到最终的编码实现。为此,它可以划分为三个阶段:

  • 战略设计。提炼问题域并塑造应用程序的架构。为了 保持模型的完整性 ,我们需要对业务流程进行梳理、统一语言,识别并划定上下文的边界,以及其对应的问题边界和问题域。
  • 战术设计。围绕领域模型要素(如值对象、实体、聚合等)进行设计。用于创建复杂有界上下文的有效模型的模式集合。
  • 战术实施。即通过选择合理的技术来实施架构,并对架构进行更详细的设计。(笑~~ ,可以通过架构金字塔的方式进行设计。)

所以,从某种意义上来说,DDD 又把软件设计拉回了瀑布时代——大量的预先设计。只是从粒度上比过去的瀑布式要小,还要将设计融入迭代中不断调整。

URbY3qZ.jpg!web

DDD 不是银弹

简单的项目并不需要 DDD,就好比一个人开发的项目,可能并不需要微服务——除非,在这个项目中使用了不同的优势语言,诸如于 AI 与 Python。(如果过程中存在 KPI,你当我没说过这句话)。

DDD 更适合于解决复杂的业务问题,并且其过程远比结果重要。业务越复杂,整个系统的架构便越复杂,设计出来的模块、服务间依赖也更容易出现问题。

DDD 无法解决纯技术问题。诸如于如何优先数据库性能、消息通信机制 。但是

与初创公司的生死项目相比,DDD 适合于中大型组织的长期项目——因为清晰、完整的业务规则。初创团队在业务上拥有太多的不确定性,无法确认出完整的业务全景,便无法保证建模的准确性。

1. 统一语言,达成共识

这是 DDD 中最有价值的部分之一,通过统一语言达成共识。

实现 DDD 最大的挑战就是,花费大量的时间和精力来思考业务领域,研究概念和术语,并且和专家交流,以发现、捕捉和改进通用语言。——《实现领域驱动设计》

通用语言是团队(技术 + 业务)共享的语言,用于让领域专家和开发者使用它来进行沟通。举一个简单的例子,从技术的角度来看,我们对于获取(Get)、检索(Query)、查询(Search)的定义都是很明确的,而对于业务人员来说,他/她说搜索的时候,ta 可能要的是 Query 这种按条件过滤,而非 Search 这种任意条件的搜索方式。

通过不断消除领域专家和技术人员之间的歧义,可以避免谓地浪费及过度设计。并且在这个时候,领域专家知道问题的答案,而开发人员要提出问题。对于统一语言的命名来说,要 尽量避免二义性 ,可以使用带定语的语句。用句人话来说,就是长的词语优于短的词语,比如笔记本电脑比电脑更有明确性。

  • 定语 是用来修饰、限定、说明名词或代词的品质与特征的。
  • 宾语 ,也称受词,是指一个动作(动词)的接受者。

是的,在过程中,除了成为一个领域专家,也会成为一个语言专家的。所以,如果你们的表达都不好,你还需要一个语言专家(大雾)。如果你们刚接触一个新的领域,而技术人员都不是这方面的专家,那么越早统一语言,越能减少浪费。这也是我们在实施事件风暴的时候,最早看到的成果之一。

值得一提的是,知识提炼是一个持续的过程。

2. 集中优势兵力

领域(Domain)是一个组织所做的事件,以及其中所包含的一切。问题域是我们在软件设计中,所需要解决的问题域。DDD 的整个过程,便是从问题域到解决方案域的过程。

围绕这个复杂问题域构建的大型系统,往往会由一组组件和子系统构成。而在解决方案中,某些部分会比其它部分有价值,我们需要关注于更高价值的部分,集中最好的资源在这上面。

为此,我们有了第一个基本的策略。

核心域开发,支撑域外包,通用域购买

对于一个复杂的问题域来说,我们需要对它进行提炼,划分出相关的子域。通过区分出它的核心域、支撑域和通用域,来作为资源投入优先级的重要参考:

  • 核心域。即系统的核心部分(差异化竞争力),也是业务的盈利来源,它需要投入最好的开发人员。
  • 支撑域。非系统的业务核心,但是没有它系统就无法运转。由于存在部分的差异化,往往需要个性化的定制。
  • 通用域。它不是业务的核心,但是在业内非常常见,有现成的解决方案。可以直接购买,或者使用初级开发人员。

为此简单来说,我们可以通过 核心域开发,支撑域外包,通用域购买

剩下的问题便是,如何划分出这些子域。

问题:如何提炼问题域?

这是一个非常好的问题。在《领域驱动设计模式:原理与实践》一书中提到了多种有效的方式来理解问题域:

事件风暴。它是由 AlbertoBrandolini 提出的,一种用于领域驱动设计的协作设计方法。 事件风暴基于现实业务流程,以系统实现为视角,通过一次只关注一个维度的分层抽象方式,将现实业务流程进行抽象并转化为系统实现的业务逻辑。

它的流程是,通过识别 决策命令 (decision command)和 领域事件 (domain event)的 聚合 (aggregate) or 业务承载物(carrier),并开始将聚合分组到有界上下文中。梳理限界上下文依赖关系,再划分问题子域中。在此过程中,识别关键测试场景,用户和目标并将其合并到模型中。最后,添加有界上下文之间的关系以创建上下文映射。然后用代码对所得模型进行挑战,以验证组学习并验证模型。

它通常由三个步骤组成:

  1. 头脑风暴,以识别领域事件。
  2. 识别事件触发器(决策命令)。
  3. 识别聚合(业务承载物)。

模型探讨旋涡(whirlpool)是由 DDD 的创造者 Eric Evans 创建的一个模型探讨漩涡的文档草案( http://domainlanguage.com/ddd/whirlpool/ )。这份文档提出了一种建模和知识提炼的方法,它能补充其它的敏捷方法,并能在应用的开发生命周期中随时使用。不过,它主要不是用来建模的,而是为了解决在创建模型期间遇到的问题。

除此,还书中还提及了如何更好地理解业务模型。

  • 影响地图(Impact Mapping)。它是一门战略规划技术。通过清晰地沟通假设,帮助团队根据总体业务目标调整其活动,以及做出更好的里程碑决策,影响地图可以避免组织在构建产品和交付项目的过程中迷失方向。
  • 理解业务模型。即通过商业画面来可视化业务模型。
  • 刻意发现( Deliberate Discovery )。由 BDD 的创建者 Dan North 发布的提升领域知识的方法( https://dannorth.net/2010/08/30/introducing-deliberate-discovery/ )。

如果说事件风暴是一个以技术人员为主的协作设计方式,那么影响地图则是一种以业务的 KPI 为主的协作方式。影响地图能够在知识提炼环境提出更好的问题。

值得注意的是,对于面向开发(如云服务)来说,它的核心域可能是技术相关的——这一点有些时候会让人困惑不已。

3. 抽象建模,良好的设计

领域模型位于领域驱动设计(DDD)的中心,它是在知识提炼环节,通过开发团队和业务专家之间的协作形成的。它的主要目的是,描述领域中的复杂逻辑和策略,以便解决业务问题。

即其产生的过程是:大量的业务用例 -> 知识提炼 -> 领域模型。

DDD 战术模式形成架构基础要素

DDD 战术模式的作用是管理复杂性,并确保领域模型中行为的清晰明确。这些构成模型的要素包含了:

  • 实体 ,只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。它 具有唯一标识和生命周期
  • 值对象 ,当你只关心某个对象的属性时,该对象便可作为一个值对象。它是实体的附加业务概念,用来描述实体所包含的业务信息。

在技术实践实现上,会通过以下的要素来完成设计:

  • 领域服务(domain service)。封装了没有在模型中自然建模为值对象或实体的领域逻辑和概念。它的主要职责是使用实现和值对象编排业务逻辑。
  • 领域事件(domain event)。它用于表明问题域中发生了一些业务人员关心的事情。在命名领域事件时,我们往往选择动词的过去分词,以明确表达事件的属性,其中文形式往往是『XXX已YYY』。
  • 资源库(repository)。公开聚合根在内存中的集合的接口,提供聚合根的检索和持久化需要。
  • 工厂(factory)。即在实体或者值对象创建复杂时,可以委托给工厂(模式)进行创建。。
  • 聚合(aggregate)。是一种边界内的领域对象的集群,可以将其视为一个单元。可以封装一个到多个实体与值对象,用来维护该边界范围之内的业务完整性。
  • 应用服务(application service)。
  • 事件溯源。事件溯源是一种以事件为中心实现持久化的方法。在创建或更新一个聚合时,服务将一个或多个事件保存在数据库中,这时我们也将数据库称为事件存储。事件溯源通过加载并回放事件,重建了聚合的当前状态。

下图是各要素之间的关系:

UnmYjii.png!web

形成充血模型

经过 DDD 这一系列战术模式的操作,从某种程度上,可以避免贫血模型的产生。贫血模型是指使用的领域对象中只有 setter 和 getter 方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。

与此,同时,它还能减少上帝类(God Class,在整个应用中使用的全局类)的产生。

所以,反过来讲,DDD 可以有效地用于遗留系统的拆解与迁移。

4. 限界上下文,防止大泥球产生

从某种意义上来说,划分有界上下文的原则和 SOLID 中的 SRP(单一职责原则) 和 OCP (开闭原则)类似。

识别限界上下文,划清模型边界

为了识别限界上下文,我们需要使用到先前提到的事件风暴方法。基于领域事件,分析领域模型,以驱动设计出我们想要的限界上下文。

除此,如《实现领域驱动设计》的译者张逸在文章中所说,也可以通过引 用例场景分析 来帮助团队剖析业务场景,驱动业务架构的设计。

5. 有理有据划分服务,降低维护成本

随着近年来微服务的流行,我们经常将 DDD 与微服务配合使用,这不并意味着微服务是必须的,只是可以让微服务更香。DDD 和事件风暴可以让我们有理有据的划分模块与依赖。

限界上下文拆分微服务

通常来说, 一个限界上下文就是一个微服务

但是呢,出于成本和维护原因考虑,我们并不会这么去做。所以 变更频率(步速) ,成为了我们在合并微服务的一个考虑。即,通常数据区分出不同的限界上下文的

  1. 变更频率
  2. 依赖项上下文
  3. ……

对应的我们也会有一系列的策略:各种独立更新的限界上下文,可以独立为服务;变动较小的部分,可以考虑合并在一起,以降低微服务的成本;相互依赖的限界上下文,可以考虑合并到同一个微服务中。

对于小型的团队来说,我们仍然可以将应用做成一个模块化单体应用,又或者是通过『应用微化架构』来在构建时拆分、运行时微服务化。

组织与架构划分

出于字数的考虑,这里就引用 康威定律康威逆定律

康威定律,即『设计系统的架构受制于产生这些设计的组织的沟通结构。』

康威逆定律,如果想改变一个设计架构方式,首先要改变组织结构。因为很多构建架构(如微服务)的公司围绕服务边界构建团队,而不是按孤立的技术架构来划分。

当然不可避免的,会存在一些开发人员跨多个团队的存在。

领域架构模式

既然所有微服务相关的书,都提到了架构相关的部分。(PS:那我也得复制一下,但是我比他们都多了一个整洁架构。)

  • 分层架构
  • 六边形式架构
  • CQRS 架构
  • 事件驱动架构
  • 整洁架构 。(推荐,哈哈)

限于篇幅所限,这里就不详细描述了(我已经不想拷贝了)。

关于这部分的详细内容,可以考虑加入我的下篇文章,架构金字塔。

结论

『软件工程学的历史过程,也就是人类试图通过建立简化方法,降低随着问题规模的扩大而提高的问题复杂度,从而不断对规模/复杂度动力提出挑战的历史过程』。—— Weinberg。

  • LB:“听说,『DDD 能提高团队的整体智商』”
  • AJ:“是的,因为 DDD 很南”

DDD 是一个有效的方式,但是如果 DDD 的落地方式过于复杂,一定会催生出更简化的方式。即: 复杂度同力一样不会消失,也不会凭空产生,它总是从一个物体转移到另一个物体或一种形式转为另一种形式。

相关文章:

  • https://www.infoq.cn/article/microservices-aggregates-events-cqrs-part-2-richardson
  • http://zhangyi.xyz/overview-of-ddd/

参考书籍:

  • 《高可用可伸缩微服务架构》第二章(出版社送的, 本书 8 人合著 ,你懂的,超过 3 人就不适合买)
  • 《领域驱动设计模式:原理与实践》
  • 《微服务架构设计模式》
  • 《实现领域驱动设计》

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK