简介

领域驱动设计(英语:Domain-driven design,缩写 DDD)是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。领域驱动设计的前提是: - 把项目的主要重点放在核心领域(core domain)和域逻辑 - 把复杂的设计放在有界域(bounded context)的模型上 - 发起一个创造性的合作之间的技术和域界专家以迭代地完善的概念模式,解决特定领域的问题

起源于2004年出版的Eric Evans《Domain-Driven Design: Tackling Complexity in the Heart of Software》(《领域驱动设计:软件核心复杂性应对之道》

实践案例

出现的问题

  1. 过度耦合 过多的业务逻辑耦合在一起,剪不断理还乱,牵一发而动全身。
  2. 贫血模型与失忆症 贫血领域对象(Anemic Domain Object)是指仅用作数据载体,而没有行为和动作的领域对象。 当系统很复杂时,业务知识四散在各处,导致代码原有的意图越来越不明确,即失忆症。反之则为充血模型。关于贫血模型: 《AnemicDomainModel》https://martinfowler.com/bliki/AnemicDomainModel.html 中文翻译:http://www.ituring.com.cn/article/25

贫血模型-事务脚本-基于设计库设计 VS 充血模型-领域模型-基于对象设计

通过以上案例,我们可以知道DDD这套理论应对的是复杂业务系统,并且DDD不是没有指导意义的大道理,也不是高深莫测无法使用的,而是可以落地并带来巨大收益的。

笔者呓语:如果读者发现本文纯粹浪费时间,DDD也不过如此,甚至糟糕透顶,那么是笔者道行浅薄,而不是DDD不好,毕竟存在着上述成功案例。

基本概念

通用语言与领域专家

领域专家

领域专家并不是一个职位,他可以是精通业务的任何人。他们可能了解更多的关于业务领域的背景知识,他们可能是软件产品的设计者,深圳有可能是销售。

通用语言

通用语言是团队共享的语言。领域专家和开发者使用相同的语言与领域专家通用语言进行交流。

理解通用语言,必须牢牢记住一下几点:

  • 通用意思是“普遍的”,或者“到处都存在的“。通用 语言在团队范围内使用,并且只表达一个单一的领域模型。
  • “通用语言”并不表示全企业、全公司或者全球性的万能领域语言。
  • 限界上下文和通用语言间存在一对一的关系。
  • 限界上下文是一个相应较小的概念,通常比我们起初的想象的要小。限界上下文刚好能够容纳下一个独立的限界上下文中所使用的通用语言。
  • 只有当团队工作在一个独立的限界上下文中时,通用语言才是”通用“的。
  • 虽然我们只工作在一个限界上下文中,但是通常我们还需要和其他限界上下文打交道,这时可以通用上下文映射图对这些限界上下文进行集成。每个限界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的地方。
  • 如果你试图将某个通用语言运用到整个企业范围之内,或者更大的、跨企业的范围内,你将失败。

战略建模

领域

从广义上讲,领域(Domain)即是一个组织所做的事情以及其中所包含的一切。

子域

在DDD中,一个领域被分为若干子域(Sub-domains),领域模型在界限上线文中完成开发。在DDD中鼓励细分领域,创造一个大一统领域通常是会失败的。

子域分为: - 核心子域 - 通用子域 - 支撑子域

界限上下文(Bounded Context)

界限上下文是一个显示边界, 语义上的边界。 领域模型便存在于这个边界之内。领域模型把通用语言表达成软件模型。

在不同的模型中存在名字相同或者相近的对象,但它们的意思却不同。

通常一个子域对应一个界限上下文。

划分界限上下文?

经典书籍中似乎没有讲到具体如何划分界限上下文。那么就通过深刻理解业务,建立通用语言,通过通用语言各种联系进行划分。

一个界限上下文图:

上下文映射图???

上下文映射图描述上下文之间的关系。 限界上下文之间的映射关系:

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

一个上下文映射图:

图纸U表示上游,D表示下游。

上下文集成

通常集成上下文的手段有多种,常见的手段包括开放领域服务接口、开放HTTP服务以及消息发布-订阅机制。

架构

通用架构解决方案

  • 用户界面层(或表示层):负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
  • 应用层: 很薄的一层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道。 应用层要尽量的简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们相互协作。它没有反应业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
  • 领域层(或模型层): 负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反应业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心
  • 基础设施层:为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式。

注意: - 分层架构的一个重要原则是:每层只能与位于其下方的层发生耦合。 - 通过战略建模和战术建模设计形成的领域模型应该是架构中立的。

六边形架构

依赖倒置原则 ???

有一种方法可以改进分层架构,即依赖倒置原则(Dependency Inversion Principle, DIP),它通过改变不同层之间的依赖关系达到改进目的。

依赖倒置原则由Robert C. Martin提出,正式定义为:

1
2
高层模块不应该依赖于底层模块,两者都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。

根据该定义,DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了。如果我们把分层架构推平,再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,即六边形架构。六边形架构是Alistair Cockburn在2005年提出的,在这种架构中,不同的客户通过“平等”的方式与系统交互。需要新的客户吗?不是问题。只需要添加一个新的适配器将客户输入转化成能被系统API所理解的参数就行。同时,对于每种特定的输出,都有一个新建的适配器负责完成相应的转化功能。

六边形架构示意图

六边形架构也称为端口与适配器,如下图所示:

关于DDD分层架构的讨论可以参见: 《DDD分层架构的三种模式》https://www.jianshu.com/p/a775836c7e25

战术建模

领域模型是关于某个特定业务领域的软件模型。 在领域驱动设计中,3个基本用途决定了模型的选择:

  1. 模型和设计的核心互相影响

  2. 模型是团队所有成员使用的通用语言中枢

  3. 模型是浓缩的知识

有效建模的要素

  1. 模型和实现的绑定

  2. 建立了一种基于模型的语言

  3. 开发应该蕴含丰富知识的模型

  4. 提炼模型

  5. 头脑风暴和试验

领域模型间的关系如下:

实体(Entity)

当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。

例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。

在实践上建议将属性的验证放到实体中。

值对象(Value Object)

当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。

例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。

值对象很重要,在习惯了使用数据库的数据建模后,很容易将所有对象看作实体。使用值对象,可以更好地做系统优化、精简设计。

它具有不变性、相等性和可替换性。

在实践中,需要保证值对象创建后就不能被修改,即不允许外部再修改其属性。在不同上下文集成时,会出现模型概念的公用,如商品模型会存在于电商的各个上下文中。在订单上下文中如果你只关注下单时商品信息快照,那么将商品对象视为值对象是很好的选择。

聚合(Aggregate)

聚合是一组相关对象的集合,作为一个整体被外界访问,聚合根(Aggregate Root)是这个聚合的根节点。

聚合是一个非常重要的概念,核心领域往往都需要用聚合来表达。其次,聚合在技术上有非常高的价值,可以指导详细设计。

聚合由根实体,值对象和实体组成。

如何创建好的聚合?

边界内的内容具有一致性:在一个事务中只修改一个聚合实例。如果你发现边界内很难接受强一致,不管是出于性能或产品需求的考虑,应该考虑剥离出独立的聚合,采用最终一致的方式。 设计小聚合:大部分的聚合都可以只包含根实体,而无需包含其他实体。即使一定要包含,可以考虑将其创建为值对象。 通过唯一标识来引用其他聚合或实体:当存在对象之间的关联时,建议引用其唯一标识而非引用其整体对象。如果是外部上下文中的实体,引用其唯一标识或将需要的属性构造值对象。 如果聚合创建复杂,推荐使用工厂方法来屏蔽内部复杂的创建逻辑。 聚合内部多个组成对象的关系可以用来指导数据库创建,但不可避免存在一定的抗阻。如聚合中存在List<值对象>,那么在数据库中建立1:N的关联需要将值对象单独建表,此时是有id的,建议不要将该id暴露到资源库外部,对外隐蔽。

工厂(Factory)

工厂用来封装复杂的对象的创建过程。

创建一个对象可以是它自身的主要操作。但是复杂的组装操作不应该成为被创建对象的职责。

工程被用来封装对象创建所必需的知识,它们对创建聚合特别有用。当聚合的根被创建后,所有聚合包含的对象奖随之创建,所有的不变量得到的了强化。

仓库(Repository)

一种吧存储、检索和搜索行为封装起来的机制

例如为了从一个聚合中获得一个值对象,客户程序需要向聚合的根发送请求。问题是客户程序必须先拥有和对根的引用。对于大型程序,这回带来性能和设计复杂度的问题。实际上大量的对象可以从数据库中直接获取,但这对设计产生负面影响:1. 客户程序需要知道访问数据库的细节。 2. 数据库细节知识会扩散到整个模型中。

资源库的目的是封装所有获取对象引用的所需的逻辑。领域对象不需要处理基础设施,以得到领域中对其他对象的引用。只需要从资源库中获取它们,于是模型重获它应有的清晰和专注。

注意:

  1. count这样的查询也是仓库的职责

  2. 复杂的查询可能需要引入CQRS(命令和查询职责分离)???(但其实复杂的查询是有限的)

工厂和仓库

工厂关注对象的创建,资源库关注的是已经存在的对象,是重建对象。

领域服务(Domain Service)

一种作为接口提供的操作。

一些重要的领域行为或操作,可以归类为领域服务。它既不是实体,也不是值对象的范畴。

例如转账操作,应该放在转出的账户还是在接收的账户?

服务的3个特征:

  1. 服务执行的操作代表了一个领域概念,这个领域概念无法自然的隶属于一个实体或者值对象。

  2. 被执行的操作涉及到领域中的其他对象。

  3. 操作是无状态的。

领域事件

领域事件是领域专家所关心发发生想领域中的一些事件。将领域中所发生的活动建模成一系列的离散事件,表示领域中发生的事情。

领域事件是对领域内发生的活动进行的建模。

模块

模块(Module)是DDD中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文。

其他

实施DDD所面临的挑战

3点最常见的挑战: - 为创建通用语言腾出时间和精力 - 持续地将领域专家引入项目 - 改变开发者对领域的思考方式甚至引起抵触。两种可能的看法: - 我建几个表写几个SQL就能解决的问题,搞这么复杂干嘛? - 这些理论太高大上了,我看不懂或者不想看。 以上两个看法应该是还没有吃够没有设计的苦,苦受够了自然就会寻找救赎之道。

为什么没有流行?

  • 有学习成本
  • 有实施成本,如前文描述
  • DDD是软件核心复杂度应对之道,并不是所有系统都适合,也并不能解决所有问题
  • DDD是一套最佳实践的集合、升华,即使不用DDD也可能已经非系统化的使用了其中理论

DDD新的契机

微服务

与微服务架构相得益彰,DDD中的领域是一种拆分微服务的方式,界限上下文和微服务十分契合。参考: - https://microservices.io/patterns/decomposition/decompose-by-subdomain.html - https://tech.meituan.com/DDD_in_%20practice.html

更多的新技术

更多的新技术让DDD的一些概念实现变得更为简单。参加:《Eric Evans:领域驱动设计(DDD)当前更为适用》https://www.infoq.cn/article/2017%2F09%2Fevans-ddd-relevant

一点实践感悟

  • 和随心所欲的使用ORM或SQL说再见,接受约束。
  • 业务代码里不再出现独立的直接处理业务函数或静态方法,业务代码应该通过领域模型实现。
  • 对于代码,想清楚究竟是什么再东搜,而不是把领域模型当做代码模板去生搬硬套。
  • 对于概念,要记住DDD是为了实现业务而不是相反,可落地的DDD比完美的DDD更重要。DDD也是在不断发展的,切不可教条主义。
  • 命名很重要。我们需要仔细地考虑什么样的对象做什么样的事情,这是关于对象行为设计的。我们希望对对象行为的命名能够传达准确的业务含义。如果一个领域模型不在通用语言中,领域专家亦无法理解,这个建模可能是有问题的。
  • 即使没有完全理解DDD,学习、实践一部分也有收获。

我认为实施DDD最重要的

两点原则: - 一个团队,一个语言 - 绑定模型和实现

一个信条: DDD的作用是简化而不是复杂化,复杂的是业务。用最简单方式对复杂领域进行建模。

参考资料