Skip to content

《领域驱动设计》 #26

@srtian

Description

@srtian

之前在接手BPM的工作时,被大佬推荐学习一下领域驱动设计,因此花了不少时间阅读了 Eric 的这本经典著作,不少章节翻了很多次,也有不少章节则选择性的放弃了(有一说一,很多模式实在不太理解),自我感觉总结起来主要有两点核心的思想:

  1. 创造统一语言:领域专家和开发人员用同一语言讨论问题,术语和概念相统一。
  2. 模型驱动开发:模型和代码相统一,并对数据的更改进行收敛以及限制。

这一过程从中收获了不少,也从中发现了现存的一些问题,在这里总结一下。

一、为什么我们需要领域建模

对于B端产品来说,核心的难点在于如何处理隐藏在业务中的复杂度。这些复杂度一方面来自于业务本身,另一方面则是在多次需求迭代中,需要保证一些关键的“知识”不在其中丢失。而降低这些复杂度,一个非常好的方式就是建立一套业务模型,通过模型来对复杂度进行简化与精炼。而这也正是领域驱动所提倡的方法论:通过领域模型来整理领域知识,从而使用领域模型来构造更易维护的软件。
总的来说,通过模型来驱动代码开发有以下三个优点:

  • 通过模型可以反映代码的结构,理解了模型,也就大致了解的代码的结构。
  • 以模型为基础形成团队的统一语言,方便沟通合作。
  • 将模型作为精炼的知识,用于传递,降低知识的传递成本。

二、领域建模的有效步骤

上面总结了一下模型给我们带来的优点,那如何有效的进行建模呢?Eric 提到了以下五点:

  1. 模型与实践的绑定。
  2. 建立一种基于模型的统一语言。
  3. 开发富含丰富知识的模型。
  4. 精炼模型。
  5. 头脑风暴与试验。

其中前两点,是进行模型提炼的基础,而后三点则是构成了一个提炼知识的闭环。大致关系如下:

不得不说,这块最初读的还是去年,那会儿格局不够,没有体会到统一语言 & 领域建模的重要性,但随着今年开发的不断深入以及迭代的不断进行,遇到了很多问题,愈发对于统一语言和领域建模带来的收益,有了更为深刻的理解。

2.1 模型与实践的绑定

Eric 在他的知识消化中并没有去强调模型的好坏,而更多的强调模型与软件在具体实现上的关联。这里的原因在于:知识消化所提倡的方法,本质上是一种迭代改进的试错法,最初的模型可能不够完善,但通过一次次的迭代,逐步将其优化,因此比起模型最初时候的好坏,关联模型和代码的实现要显得格外重要。
另一个原因就是历史问题了。Eric 写书的时正是面向过程编程大行其道的时候,由此又可以引生出两个东西:充血模型和贫血模型。

  • 充血模型:与某个概念相关的行为与逻辑,都被封装到对应的领域对象中,这也是 DDD 中强调的富含知识的模型
  • 贫血模型:对象仅仅对简单的数据进行封装,而关联关系和业务计算都散落在对象的范围之内

提到这里,不由想起了前端近几年框架的一些修改,譬如 react hooks 和 Vue 的Componzition API 其实都有在逻辑聚合方面发力。
那么一个具体的模型又是由哪些元素组成的呢,下图是我在学习领域驱动设计时,所做的思维导图,可以参考参考:一个具体的领域模型具体会通过界限上下文划分为多个相互独立的子域,子域又由很多的实体组成。

在基于以上的方式建立起一套模型后,我们要做的就是基于我们所设计好的模型将我们的代码实现出来。需要注意的是,后续有关于模型方面的代码的变更,需要同步更新模型以及文档,反之亦然。只有做到这点,才能达到真正的模型和代码的绑定。

2.2 建议一种基于模型的统一语言

Eric 在他的书中在第二章用了整整一章,去阐述统一语言的重要性。所谓的统一语言,其实就是一种在项目内部,多方共同使用的语言,需要注意的是这里的多方可以包含项目中的所有成员。沟通中的共同语言其实有很多种,比如产品之间使用的用户旅程、用户画像等,也可以是研发之间的RPC、OpenAPI等黑话。不过在 DDD 中,统一语言特指的是根据领域模型构造出来的共同语言,且这种语言可为所有项目相关方进行使用。
但单纯基于模型构造出统一语言,会存在一些问题:因为模型其实本质上是一种数据结构,描述的是在不同业务维度下,数据将会如何改变,以及如何支撑起对应的计算与数据。而业务更为关心的是一些流程、交互、规则、所产生的价值等,这其中存在一定的 gap。因此,倘若单纯使用模型构建统一语言,会使其他各方不能很好的 get 到业务价值。
因此,更好的方式是,从模型的基础上,关联出一套可以准确描述业务价值的共同语言,它既能让模型在核心位置扮演关键角色,又能抹平不同角色因为自身背景而存在的代沟。
还有一点需要注意的是,上面我们强调了模型和代码的绑定,而在这里,我们则要做到模型与统一语言的关联。所以相对应的,当我们的代码发生变化时,模型也需要变化,所对应的统一语言应该也要随之变化。只有这样,让能更好的去描述事情,做到沟通信息无代差。

2.3 开发富含丰富知识的模型

在领域模型中,有个很有意思的概念——上下文过载:指领域模型中的某个对象会在多个上下文中发挥重要的作用,甚至是聚合根。
当出现上下文过载时,往往会发生以下一些问题

  • 对象本身会变得过于复杂,导致模型僵化,令人难以理解
  • 会有潜在的性能问题

因此我们将过载的上下文进行有效的分离很有必要。而对于将上下文的分离有很多的方法,其中比较常见的是增加上下文对象来对模型进行上下文隔离。
其实对应的不只是领域模型中的上下文过载,在很多其他的领域也存在一样的问题。
首先我们需要明确的是,上下文过载的根本问题在于:实体在不同的上下文中扮演的多个角色,再借由聚合关系,将不同上下文中的逻辑富集于实体中,就造成了上下文过载。
因此我们可以通过分离不同的上下文,增加上线对象,从而来对单个大的实体进行职责的分离。

2.4 精炼模型

当我们对模型进行分离时,算是有方法论对单个大的模型进行分离了。但在开发的过程中,还有一个需要注意的问题:如何组织领域逻辑和非领域逻辑,才能避免非领域逻辑对模型进行了污染?这里就不得不提及分层架构了。分层架构可以说是存在于软件开发领域的方方面面,小到组件,大到计算机网络,都有分层架构的思路在里面。它的目的也很简单,即:将不同关注点的逻辑封装到不同的层中。
而在领域驱动设计中,我们通常可以将系统分成四层,而依据主要是基于不同层之间需求变化的速率是不同的:

  1. 展现层:人机交互
  2. 应用层:负责支撑具体的业务,将业务逻辑组织为软件的功能
  3. 领域层:核心的领域概念、信息、规则。
  4. 基础设施层:提供通用的技术能力

当然基础设施层也不一定是必要的,我们也可以通过一些诸如:能力提供商模式来对分层进行精简。

2.5 头脑风暴与试验

通过以上的几个步骤,我们终于可以将模型以及统一语言,直面于业务了。那么我们怎么通过在业务中的不断锤炼,来改进我们的模型呢。
在讨论这个之前,我们可以先了解一种建模方式:事件建模法,它是通过事件捕获系统中信息的改变,再发掘出发这些改变的源头,然后通过这些源头发现背后参与的实体与操作,最终完成对系统的建模。那么我们如何进行事件建模呢:

  1. 通过事件表示交互。这里的难点主要有二:
    1. 融入领域模型
    2. 恰当的颗粒度

对于第一点,主要的问题在于,业务关心的是用户的行为,而模型关心的是数据,这两者存在一个的 gap 。而事件就是其中的桥梁。我们可以将事件看做行为的印记。而事件发生的时间点则是事件最重要的属性。当事件发生时,就可能存在数据的变更。

  1. 通过时间线划分不同事件。其实这个原则,就回答了上述难点的第二个——恰当的颗粒度 ,我们可以通过时间线来区分事件,在同一时间线发生的事件,我们可以理解为同一事件。而在不同时间线发生的,我们则可以理解为不同的事件,这样也是将颗粒度保持在一个理想的范围内的一个好的思路。

事件建模法的整体流程大致有以下五步:

这里也有几个名词需要解释一下:

  • 行动者:系统的使用者。(可能是真实的用户,也可能是别的系统)
  • 命令:由行动者发起的行为,它代表了某种决定,通常是事件的起因,也称为行动者发出命令
  • 聚集:领域驱动设计的聚合,可以看作一组领域对象,在头脑风暴阶段泛指某些领域概念,不需要细化。
  • 系统:指代不需要了解细节的三方系统
  • 阅读模型:用以支撑决策的信息,通常与界面布局有关
  • 策略:是对于事件的响应,通常表示不属于某些聚集的逻辑,通过策略可以触发新的命令,而由策略触发的命令,被称为系统触发命令。

而通过事件建模建立起的模型,我们就可以通过不断的事件风暴以及实践,去对事件进行收敛,从而提升我们的模型。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions