领域驱动设计(DDD)入门&概要

时间:2024-03-17 20:56:18

我们为什么需要领域驱动设计

在说什么是领域驱动设计之前,我觉得需要先说一下我们为什么需要领域驱动,我个人认为领域驱动设计对于研发来说改进点主要有下面三个:

  1. 从大泥球风格中解脱出来,控制代码复杂度
  2. 回归面向对象编程本质,而不是面向过程编程
  3. 专注于业务,实现不同业务领域解偶

什么是领域驱动设计

领域驱动设计(DDD)是一种软件设计思路,领域指的是业务领域,比如银行业务领域,医药销售领域;不同于传统以数据表为中心的建模方式,它以业务领域为中心来建模,能促使我们以正确的方式使用面向对象,建立饱满的领域对象。

在进行领域建模和开发时我们主要需要关注以下几点:

  1. 和领域专家一起构建一套通用语言,团队成员使用通用业务语言进行交流;
  2. 关注领域的战略设计,DDD的战略设计从更高层次抽象系统,划分出不同的系统和业务关注点,战略设计包含:领域/子域划分、通用语言的构建、限界上下文定义以及架构风格选择等;
  3. 实现DDD的战术设计,战术设计主要包含:领域对象(实体、值对象、聚合)、模块、领域服务、工厂等;

下面的思维导图是领域驱动设计需要掌握的一些点:

领域驱动设计(DDD)入门&概要

 

Tips:

1.领域驱动设计是2004年Eric Evans在《领域驱动设计:软件核心复杂性应对之道》这本书中提到的,DDD的出现是为了应对复杂业务场景下软件越来越复杂问题;也就是说通过DDD可以将软件的复杂度控制在合理的范围内。

2.领域驱动设计主要解决的是业务复杂度问题(避免大泥球风格:大泥球风格就是没有任何清楚的结构,例如随意共享的数据,随意全局化的数据结构。这样风格的系统可维护性(maintainability)和可扩展性(extensibility)都很差,最终导致整个系统难以改动,维护不下去),如果业务不复杂,则不需要使用DDD方式来处理(推荐用三层架构)。

领域驱动设计的优缺点

领域驱动设计的优缺点很明显,我这边整理了几个优缺点,供参考。

优点:

  1. 由领域专家进行领域模型设计,通过业务驱动开发而不是数据库设计来驱动项目开发,业务逻辑更加清晰;
  2. 再次强调了面向对象编程的重要性,减少面向过程编程(提高内聚性、降低上下文之间耦合);
  3. 对单元测试(自测)比较友好;

缺点:

  1. 需要有对DDD比较精通的人员进行领域的建模,如果建模不当,后续会比较麻烦;
  2. 对开发人员要求比较高,开发人员需要理解DDD才能进行开发;
  3. DDD本身概念性的东西比较多,有些概念不好理解,需要长时间的历练才能很好的掌握DDD;

如何进行领域建模

按照实现领域驱动设计一书中描述的DDD步骤主要有4步:

  1. 根据业务需求划分出初步的领域和限界上下文,以及上下文之间的关系;
  2. 进一步分析每个上下文内部,识别出哪些是实体,哪些是值对象;对实体、值对象进行关联和聚合,划分出聚合的范畴和聚合根;
  3. 为聚合根设计仓储,并思考实体或值对象的创建方式;
  4. 在工程中实践领域模型,并在实践中检验模型的合理性,倒推模型中不足的地方并重构。

划分领域和限界上下文

提炼问题域

软件开发实际作用就是为客户解决问题或者软件升级变革,所以在领域中我们划分了问题空间和解空间两个纬度空间,用于描述客户的问题(需求)和如何解决问题(解决方案)。

问题的来源:

  1. 客户痛点需求:比如电脑蓝屏、文档忘记保存等。
  2. 行业变革:移动支付、网购等。

问题域划分

首先我们需要对问题域进行拆分,拆分的理念就是由大化小,逐个击破。拆分完毕问题空间,我们需要做到逐一击破,为每一个问题形成一套落地解决方案。一个问题对应一个答案,解空间与问题空间基本上应该是一一对应的。

构建限界上下文

我们给出的解方案是一个个限界上下文,一个限界上下文负责应对其对应的问题进行解决,承载了该问题中的业务知识及规则。限界上下文是解决某一类问题的单一功能模块,例如订单限界上下文,处理开单、通知订单状态等一切与订单关联的功能。

随着问题的划分,将领域拆分成了不同的子域,每个子域有其独有的业务知识,而这些知识需要靠一门统一语言来描述。而不同限界上下文间的统一语言是相互独立的。可能在不同的限界上下文有同样的名词,但同样的名词在不同的限界上下文中可能表示的不是同一事物,即使是同一事物可能其在不同上下文中的关注点也不一致。

限界上下文之间的映射关系:

  • 合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。
  • 共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
  • 客户方-供应方开发(Customer-Supplier Development):上下文之间有组织的上下游依赖。
  • 遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
  • 防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上下文交互。
  • 开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上下文进行访问。
  • 发布语言(Published Language):通常与OHS一起使用,用于定义开放主机的协议。
  • 大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
  • 另谋他路(SeparateWay):两个完全没有任何联系的上下文。

子域类型

核心域

核心域是一款软件的所能获得的竞争优势的根本所在。所以在核心的建模和开发主要投入大量的时间和人力。

通用域

通用域是许多大型软件系统都有的子域。通用域例如像电子邮件和短信发送、OSS、MQ等。

支撑域

系统中的其他域被称为支撑域。支撑域的主要作用就是有助于支撑核心域的业务。比如:用户的注册等。

分析模型(从领域建模角度分析需求)

DDD分析建模主要有两种方法可以使用:用例分析法、四色建模法、事件风暴、领域驱动建模。

用例分析法

通过对需求进行分析构建出用例图如下所示(举例子不一定正确):

领域驱动设计(DDD)入门&概要

四色建模法

这里列一下标准的四色建模法(https://www.infoq.cn/article/xh-four-color-modeling/),其实还是需要多联系才行。

时标型(Moment-Interval)对象:具有可追溯性的记录运营或管理数据的时刻或时段对象,用红色表示 -> 操作

PPT(Party/Place/Thing)对象:代表参与到流程中的参与方/地点/物,用绿色表示 -> 名词

角色(Role)对象:在时标型对象与 PPT 对象(通常是参与方)之间参与的角色,用黄色表示 -> 操作角色

描述(Description)对象:对 PPT 对象的一种补充描述,用蓝色表示 -> 表述备注

领域建模并不是没有方法,而是选择太多,有用例法、四色建模、领域驱动建模、事件风暴等,人有一个特点是喜欢找最简单、最易用的方法,每个方法都有自己的特点,并没有好坏优劣之分,只有适不适合。听到一个方法就去尝试,尝试到一半就放弃,一种种方法尝试,最终就会迷失在方法选择上。

识别模型(主要构成)

 

构造模型(找到聚合根)

 

细化模型(反复优化)

对于DDD而言领域建模完成后,我们就需要对照模型进行开发,在开发之间我们需要知道领域战略设计中的几种架构风格。

  1. 名词做类、动词为方法,生成渐层次模型
  2. 将渐层次模型归纳划分,做更抽象的

DDD四层架构

DDD四层架构是一个松散分层架构(任意上层可以调用下层,但是下层不能调用上层),DDD四层架构的含义如下:

  1. 用户接口层:用户界面层,或者表现层,负责向用户显示解释用户命令类似于传统三层架构中的Controller层);
  2. 应用层:定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识。这层和传统三层架构中不同点在于MVC中所有的业务逻辑都在Service层,但是DDD四层结构中将业务逻辑下层到领域层,应用层只负责定义方法,然后调用领域层进行处理。应用服务可以用于控制持久化事务和安全认证等。应用服务本身并不处理业务逻辑,但它确却是领域模型的直接客户。
  3. 领域层:领域层是系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手。
  4. 基础设施层:该层主要有两方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现;

如下是DDD四层架构图:

领域驱动设计(DDD)入门&概要

 

依赖倒置原则(DIP)

依赖倒置原则:我们要依赖不变或稳定的元素(类、模块或层),抽象不应该依赖于细节,细节应该依赖于抽象。依赖倒置原则可以改进分层架构,即底层服务应该依赖于高层组件所提供的接口。

领域驱动设计(DDD)入门&概要

 

采用了依赖注入方式后,其实可以发现事实上已经没有分层概念了。无论高层还是底层,实际只依赖于抽象,整个分层好像被推平了,这就引入下一个架构六边形架构。

六边形架构

六边形架构是Alistair Cockburn在2005年提出的,在这种架构中,不同的客户通过“平等”的方式与系统交互。在《实现领域驱动设计》一书中,作者将六边形架构应用到领域驱动设计的实现,六边形的内部代表了application和domain层。外部代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。个人理解其实就是变成内层、适配层&外部资源,内层是应用层和领域层;适配层是之前的接口层和基础设施层;外部资源就是对外接口、缓存、MQ、数据库等等。六边形架构图如下所示:

领域驱动设计(DDD)入门&概要

 

下面是对六边型架构的一个落地实践示例:

领域驱动设计(DDD)入门&概要

四层架构、DIP、六边形架构可以参考:

https://www.jianshu.com/p/c405aa19a049

http://it.hzqiuxm.com/ddd%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E6%88%98%E7%95%A5%E7%AF%87%EF%BC%884%EF%BC%89/#DDD-2

CQRS

在DDD设计思路中,DDD 通过领域对象之间的交互实现业务逻辑与流程,并通过分层的方式将业务逻辑剥离出来,单独进行维护,从而控制业务本身的复杂度。但是作为一个业务系统,查询的相关功能也是不可或缺的。在实现各式各样的查询功能时,往往会发现很难用领域模型来实现。假设在用户需要一个订单相关信息的查询功能,展现的是查询结果的列表。列表中的数据来自于订单、商品、品类、送货地址等多个领域对象中的某几个字段。这样的场景如果还是通过领域对象来封装就显的很麻烦,其次与领域知识也没有太紧密的关系。所以命令查询职责分离的设计架构(CQRS)是可以很好的解决上述的问题。

CQRS将系统分为两类操作:命令(command)、查询(Query)。命令则是对会引起数据发生变化操作的总称,即我们常说的新增,更新,删除这些操作,都是命令。而查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查找数据。CQRS 的核心思想是将这两类不同的操作进行分离,然后在两个独立的服务中实现。这种独立服务可以是两个不同的应用或者同一个应用中不同的包进行隔离。同时理论上命令和查询的数据源也是不同的(实现读写分离)。

当然读写分离模式,肯定会遇到事务的问题,事务问题主要的包含两种情况:

  1. 当 command 端完成数据更新后,需要通过事件的形式通知 query 端系统,这就存在着一定的时间差,如果你的业务对于数据完整的实时性非常高,那么可能 CQRS 不一定适合你。
  2. 其次一个 command 触发的事件在 query 端可能需要更新数个数据模型,而这也是有可能失败的。一旦更新失败那么数据就会长时间的处于不一致状态,需要外部的介入。

所以从事务的角度来看 CQRS,你需要面对的是问题从根本来说是个最终一致性的问题。对于团队处理这种最终一致性问题需要有对应的方案。

CQRS架构如下所示:

领域驱动设计(DDD)入门&概要

 

领域对象

了解了战略设计后,我们主要做具体的实施,具体实施时我们需要了解实体、值对象和聚合这几个领域对象。

实体&值对象

我们先看一下他们的定义:

实体:许多对象不是由它们的属性来定义,而是通过一系列的连续性(continuity)和标识(identity)来从根本上定义的。只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即使这些属性对系统用户非常重要),那它就是一个实体。

值对象:当你只关心某个对象的属性时,该对象便可作为一个值对象。为其添加有意义的属性,并赋予它相应的行为。我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。

对于实体Entity,实体核心是用唯一的标识符来定义,而不是通过属性来定义。即即使属性完全相同也可能是两个不同的对象。同时实体本身有状态的,实体又演进的生命周期,实体本身会体现出相关的业务行为,业务行为会实体属性或状态造成影响和改变。

如果从值对象本身无状态,不可变,并且不分配具体的标识层面来看。那么值对象可以仅仅理解为实际的Entity对象的一个属性结合而已。该值对象附属在一个实际的实体对象上面。值对象本身不存在一个独立的生命周期,也一般不会产生独立的行为。

对于实体和值对象详细说明可以参考:https://www.jianshu.com/p/da51d16dbdc4

聚合&聚合根

聚合是业务和逻辑紧密关联的实体和值对象组合而成,聚合是数据修改和持久化的基本单元,一个聚合对应一个数据的持久化;同时将选择一个实体作为每个聚合的根,并仅允许外部对象持有对聚合根的引用。作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合根或指定的框架机制。

https://www.cnblogs.com/laozhang-is-phi/p/9916785.html

最小化集成

在所有的DDD项目中,通常存在多个限界上下文,这意味着我们需要找到合适的方法对这些上下文进行集成。当模型概念从上游上下文流入下游上下文中时, 尽量使用值对象来表示这些概念。这样的好处是可以达到最小化集成,即可以最小化下游模型中用于管理职责的属性数目。使用不变的值对象使得我们做更少的职责假设。

贫血模型&充血模型

贫血模型:指使用的领域对象中只有setter和getter方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。贫血模型中领域对象只有属性没有丰富的操作,使用时需要单独的Dao层配合使用,风格比较面向过程。

充血模型:将大多数业务逻辑和持久化放在领域对象中,业务逻辑只是完成对业务逻辑的封装、事务和权限等的处理。充血模型更加倾向于面向对象的设计风格(可以参考:https://www.jianshu.com/p/fae3da337ebc)。

模块&领域服务&工厂

模块(Module)是DDD中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文。一般的工程中包的组织方式为{com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。
领域服务:领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。
工厂:在DDD中有可能存在复杂对象的创建,如果使用对象本身进行创建有可能会破坏单一责任原则,所以需要工厂进行对象创建。