领域驱动模型(DDD)

  1. 云栖社区>
  2. 博客>
  3. 正文

领域驱动模型(DDD)

高广超 2018-03-16 21:32:00 浏览1261
展开阅读全文

本文作者是组内同事 杜宁,目前负责美团外卖活动管理模块业务。

什么是领域驱动模型?

2004年Eric Evans 发表《领域驱动设计——软件核心复杂性应对之道》(Domain-Driven Design –Tackling Complexity in the Heart of Software),简称Evans DDD,领域驱动设计思想进入软件开发者的视野。领域驱动设计分为两个阶段:

  • 1、以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;

  • 2、由领域模型驱动软件设计,用代码来实现该领域模型;

简单地说,软件开发不是一蹴而就的事情,我们不可能在不了解产品(或行业领域)的前提下进行软件开发,在开发前,通常需要进行大量的业务知识梳理,而后到达软件设计的层面,最后才是开发。而在业务知识梳理的过程中,我们必然会形成某个领域知识,根据领域知识来一步步驱动软件设计,就是领域驱动设计的基本概念。而领域驱动设计的核心就在于建立正确的领域驱动模型。

img_a4edcc27106e6093559594146d0049e6.png
image.png

传统软件开发与贫血模型

传统的开发思想

传统开发四层架构

img_6c2dcb4f35b4c69b1510d2d0c1c15bfa.png
image.png

在传统模型中,对象是数据的载体,只有简单的getter/setter方法,没有行为。以数据为中心,以数据库ER设计作驱动。分层架构在这种开发模式下,可以理解为是对数据移动、处理和实现的过程。

以商家活动为例,首先设计数据库表配置

img_968de431acbe9f571a5a3ee41dd9c1f3.png
image.png

设计WmActPoi对象,只有简单的get和set属性方法

public class WmActPoi {
 
    private Long id;
 
    private String wmPoiId;
 
    private Integer startTime;
 
    private Integer endTime;
    public Long getId() {
        return id;
    }
 
    public void setId(Long id) {
        this.id = id;
    }
 
    public Integer getWmPoiId() {
        return wmPoiId;
    }
 
    public WmActPoiDB setWmPoiId(Integer wmPoiId) {
        this.wmPoiId = wmPoiId;
    }
    ......
}

Service层代码实现

class WmActPoiService {
    saveWmActPoi();   //保存活动
    checkWmActPoi();   //活动校验
    ....
}
  
class WmActPoiQueryService {
    queryWmActPoi();   //查询活动
    queryWmActPoiByWmPoiId();   //根据门店查询活动
    ....
}

可以看到,业务逻辑都是写在Service中的,WmActPoi充其量只是个数据载体,没有任何行为,是一种贫血模型。简单的业务系统采用这种贫血模型和过程化设计是没有问题的,但在业务逻辑复杂了,业务逻辑、状态会散落到在大量方法中,原本的代码意图会渐渐不明确,我们将这种情况称为由贫血症引起的失忆症

img_8a5a07de63ad4b776b16c05bcc1eed6e.png
image.png

传统架构的特点:

  • a. 以数据库为中心

  • b. 贫血模型

  • c. 业务逻辑散落在大量的方法中

  • d. 当系统越来越复杂时,开发时间指数增长,维护成本很高

DDD设计思想

采用DDD的设计思想,业务逻辑不再集中在几个大型的类上,而是由大量相对小的领域对象(类)组成,这些类具备自己的状态和行为,每个类是相对完整的独立体,并与现实领域的业务对象映射。领域模型就是由这样许多的细粒度的类组成。

建立领域知识(Build Domain Model)

说了这么多领域模型的概念,到底什么是领域模型呢?作为一个软件开发者,我们很难在对一个领域不了解的情况下着手开发,所以我们首先需要和领域专家沟通,建立领域只是。以飞机航行为例子:

现要为航空公司开发一款能够为飞机提供导航,保证无路线冲突监控软件。那我们应该从哪里开始下手呢?根据DDD的思路,我们第一步是建立领域知识:作为平时管理和维护机场飞行秩序的工作人员来说,他们自然就是这个领域的专家,我们第一个目标就是与他们沟通,也许我们并不能从中获取所有想要的知识,但至少可以筛选出主要的内容和元素。你可能会听到诸如起飞,着陆,飞行冲突,延误等领域名词,让们从一个简单的例子开始:

  • 起点->飞机->终点

这个模型很直接,但有点过于简单,因为我们无法看出飞机在空中做了什么,也无法得知飞机怎么从起点到的终点,那么如此似乎会好些:

  • 飞机->路线->起点/终点

    既然点构成线,那何不:

  • 飞机->路线->points(含起点,终点)

    这个过程,是我们不断建立领域知识的过程,其中的重点就是寻找领域专家频繁沟通,从中提炼必要领域元素。

通用语言(Ubiquitous Language)

上面的例子的确看起来简单,但过程并非容易:我们(开发人员)和领域专家在沟通的过程中是存在天然屏障的:我们满脑子都是类,方法,设计模式,算法,继承,封装,多态,如何面向对象等等;这些领域专家是不懂的,他们只知道飞机故障,经纬度,航班路线等专业术语。

所以,在建立领域知识的时候,我们(开发人员和领域专家)必须要交换知识,知识的范围范围涉及领域模型的各个元素,如果一方对模型的描述令对方感到困惑,那么应该立刻换一种描述方式,直到双方都能够接受并且理解为止。在这一过程中,就需要建立一种通用语言,作为开发人员和领域专家的沟通桥梁。

可如何形成这种通用语言呢?其实答案并不唯一,确切的说也没有什么标准答案。

(a) UML

利用UML可以清晰的表现类,并且展示它们之间的关系。但是一旦聚合关系复杂,UML叶子节点将会变的十分庞大,可能就没有那么直观易懂了。最重要的是,它无法精确的描述类的行为。为了弥补这种缺陷,可以为具体的行为部分补充必要说明(可以是标签或者文档),但这往往又很耗时,而且更新维护起来十分不便。

(b) 文档/绘图

文档耗时很长,可能不久就要变化,为模型从一开 始到它达到比较稳定的状态会发生很多次变化, 可能在完成之前它们就已经作废了。对于复杂系统,绘图容易混乱。

(c) 伪代码

极限编程推荐这么做,但是使用难度大

领域驱动设计的分层架构和构成要素

img_a84783067b3358bf6abb7a0f2a04f23b.png
image.png

一个通用领域驱动设计的架构性解决方案包含4 个概念层:

img_5da7e18219e926324a3567104af49a92.png
image.png
img_34c41434b94c8a3b0372f6a1456ed8c5.png
image.png

层结构的划分是很有必要的,只有清晰的结构,那么最终的领域设计才宜用,比如用户要预定航班,向Application Layer的service发起请求,而后Domain Layler从Infrastructure Layer获取领域对象,校验通过后会更新用户状态,最后再次通过Infratructure Layer持久化到数据库中。

领域驱动模型的一些要素

img_508672702822d65712f7fde5ff1d40b7.png
image.png

实体(Entity) & 值对象(Value Object)

实体与面向对象中的概念类似,在这里再次提出是因为它是领域模型的基本元素。在领域模型中,实体应该具有唯一的标识符,从设计的一开始就应该考虑实体,决定是否建立一个实体也是十分重要的。

值对象和我们说的编程中数值类型的变量是不同的,它仅仅是没有唯一标识符的实体,比如有两个收获地址的信息完全一样,那它就是值对象,并不是实体。值对象在领域模型中是可以被共享的,他们应该是“不可变的”(只读的),当有其他地方需要用到值对象时,可以将它的副本作为参数传递。

服务(Services)

当我们在分析某一领域时,一直在尝试如何将信息转化为领域模型,但并非所有的点我们都能用Model来涵盖。对象应当有属性,状态和行为,但有时领域中有一些行为是无法映射到具体的对象中的,我们也不能强行将其放入在某一个模型对象中,而将其单独作为一个方法又没有地方,此时就需要服务.

服务是无状态的,对象是有状态的。所谓状态,就是对象的基本属性:高矮胖瘦,年轻漂亮。服务本身也是对象,但它却没有属性(只有行为),因此说是无状态的。

服务存在的目的就是为领域提供简单的方法。为了提供大量便捷的方法,自然要关联许多领域模型,所以说,行为(Action)天生就应该存在于服务中。

服务具有以下特点:

  • a)服务中体现的行为一定是不属于任何实体和值对象的,但它属于领域模型的范围内
  • b)服务的行为一定涉及其他多个对象
  • c)服务的操作是无状态的

模块(Moudles)

对于一个复杂的应用来说,领域模型将会变的越来越大,以至于很难去描述和理解,更别提模型之间的关系了。模块的出现,就是为了组织统一的模型概念来达到减少复杂性的目的。而另一个原因则是模块可以提高代码质量和可维护性,比如我们常说的高内聚,低耦合就是要提倡将相关的类内聚在一起实现模块化。

模块应当有对外的统一接口供其他模块调用,比如有三个对象在模块a中,那么模块b不应该直接操作这三个对象,而是操作暴露的接口。模块的命名也很有讲究,最好能够深层次反映领域模型。

聚合(Aggregates)

聚合表示一组领域对象(包括实体和值对象),用来表述一个完整的领域概念。而每个聚合都有一个根实体,这个根实体又叫做聚合根。举个简单的例子,一个电脑包含硬盘、CPU、内存条等,这一个组合就是一个聚合,而电脑就是这个组合的聚合根。在聚合中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他Entity都有本地表示,但这些标识只有在聚合内部才需要加以区别,因为外部对象除了根Entity之外看不到其他对象。

img_b6210419609f52c1c2f5966675413b67.png
image.png

工厂(Factory)

一个对象的创建可能是它自身的主要操作,但是复杂的组装操作不应该成为被创建对象的职责。组合这样的职责会产生笨拙的设计, 也很难让人理解。因此,有必要引入一个新的概念,这个概念可以帮助封装复杂的对象创建过程,它就是工厂(Factory)。工厂用来封装对象创建所必需的知识,它们对创建聚合特别有用。当聚合的根建立时,所有聚合包含的对象将随之建立。

img_15647d50ef1e70b2f3303147187e7cb4.png
image.png

资源库(Repositories)

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

资源库会保存对某些对象的引用。当一个对象被创建出来时,它可以被保存到资源库中,然后以后使用时可从资源库中检索到。如果客户程序从资源库中请求一个对象,而资源库中并没有它,就会从存储介质中获取它。换种说法是,资源库作为一个全局的可访问对象的存储点而存在。

Repository的接口应当采用领域通用语言。作为客户端,不应当知道数据库实现的细节。

Repository和DAO的作用类似,二者的主要区别:

DAO是比Repository更低的一层,包含了如何从数据库中提取数据的代码。

Repository以“领域”为中心,所描述的是“领域语言”。Repository把ORM框架与领域模型隔离,对外隐藏封装了数据访问机制。

img_6311530025461a4fff9e67970dd3f682.png
image.png
public
interface AccountRepository {
    Account findAccount(String accountId);
    void addAccount(Accountaccount);
}

工厂和资源库之间存在一定的关系。它们都是模型驱动设计中的模式,它们都能帮助我们关联领域对象的生命周期。然而工厂关注的是对象的创建,而资源库关心的是已经存在的对象。资源库可能会 在本地缓存对象,但更常见的情况是需要从一个持久化存储中检索 它们。对象可以用构造函数创建,也可以被传递给一个工厂来构 建。从这个原因上讲,资源库也可以被看作一个工厂,因为它创建对象。不过它不是从无到有创建新的对象,而是对已有对象的重建。我们将不把资源库视为一个工厂。工厂创建新的对象,而资源库应该是用来发现已经创建过的对象。当一个新对象被添加到资源库时,它应该是先由工厂创建过的,然后它应该被传递给资源库以便将来保存它,见下面的例子:

img_19addc1d87533eb8bbce2e4657cedf9a.png
image.png

持续集成与模型一致性

规约(Factory)

规约是一种布尔断言。


img_e3ba74dd8703ce032decf9497ecf56a9.png
image.png

规约是业务规则的 部分 理论上规约类中的方法只有个:isSatisfiedBy(Object obj)。

规约用来测试对象是否满足某种条件,用来进行对象查询,也可以作为某个对象的创建条件。

单一规约规则。多个规约通过组合表现复杂的规约。

img_2ca94b33101963948832fdb068cae29a.png
image.png
img_e21b88e4f830b275d2e0f6ecc814012a.png
image.png
img_1ba221ac8f4e48ee5763aa04c947b177.png
image.png
img_1ba221ac8f4e48ee5763aa04c947b177.png
image.png

限界上下文(Bounded Context)

明确的定义模型所应用的上下文。根据团队的组织、软件系统的功能和物理表现(代码数据库)来设置模型的边界。在这些边界中严格保持模型的一致性,而不要受到边界之外问题的混淆。每个团队负责自己的模型,并为其他模型提供服务。

上下文映射(Context Map)

一个企业应用有多个模型,每个模型有自己的界定的上下文。建议用上下文作为团队组织的基础。在同一个团队里的人们能更容易地 沟通,也能很好地将模型集成和实现。但是每个团队都工作于自己 的模型,所以最好让每个人都能了解所有的模型。上下文映射(Context Map)是指抽象出不同界定上下文和它们之间关系的文 档,它可以是像下面所说的一个试图(Diagram),也可以是其他任 何写就的文档。详细的层次各有不同。它的重要之处是让每个在项 目中工作的人都能够得到并理解它。

img_d08196381ca8d9967270ffd065d0315f.png
image.png

共享内核(Shared Kernel)

img_da1c7f74db7a6840d136fecfbf1253c1.png
image.png

总结

领域驱动设计的核心是领域模型,这一方法论可以通俗的理解为先找到业务中的领域模型,以领域模型为中心驱动项目的开发。而领域模型的设计精髓在于面向对象分析,在于对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。

在面向对象编程中讲究封装,讲究设计低耦合,高内聚的类。而对于一个软件工程来讲,仅仅只靠类的设计是不够的,我们需要把紧密联系在一起的业务设计为一个领域模型,让领域模型内部隐藏一些细节,这样一来领域模型和领域模型之间的关系就会变得简单。这一思想有效的降低了复杂的业务之间千丝万缕的耦合关系。

DDD开发案例

超市收银业务

领域驱动设计在互联网业务开发中的实践

本文作者是组内同事 杜宁,目前负责美团外卖活动管理模块业务。


欢迎关注 高广超的简书博客 与 收藏文章 !
欢迎关注 头条号:互联网技术栈

个人介绍:

高广超:多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能、可扩展的互联网架构。

本文首发在 高广超的简书博客 转载请注明!

img_31e2e3075b097cabfd9b3643cd9abaa5.png
简书博客
img_3b1610566b09db3358ca5dcb3e015a52.png
头条号

网友评论

登录后评论
0/500
评论
高广超
+ 关注