领域驱动模型(DDD)

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

什么是领域驱动模型?

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

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

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

image.png

传统软件开发与贫血模型

传统的开发思想

传统开发四层架构

image.png

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

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

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

image.png

传统架构的特点:

  • a. 以数据库为中心
  • b. 贫血模型
  • c. 业务逻辑散落在大量的方法中
  • d. 当系统越来越复杂时,开发时间指数增长,维护成本很高

DDD设计思想

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

建立领域知识(Build Domain Model)

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

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

  • 起点->飞机->终点

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

  • 飞机->路线->起点/终点 既然点构成线,那何不:
  • 飞机->路线->points(含起点,终点) 这个过程,是我们不断建立领域知识的过程,其中的重点就是寻找领域专家频繁沟通,从中提炼必要领域元素。

通用语言(Ubiquitous Language)

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

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

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

(a) UML

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

(b) 文档/绘图

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

(c) 伪代码

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

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

image.png

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

image.png

image.png

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

领域驱动模型的一些要素

image.png

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

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

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

服务(Services)

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

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

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

服务具有以下特点:

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

模块(Moudles)

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

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

聚合(Aggregates)

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

image.png

工厂(Factory)

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

image.png

资源库(Repositories)

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

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

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

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

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

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

image.png

public
interface AccountRepository {
    Account findAccount(String accountId);
    void addAccount(Accountaccount);
}

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

image.png

持续集成与模型一致性

规约(Factory)

规约是一种布尔断言。

image.png

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

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

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

image.png

image.png

image.png

image.png

限界上下文(Bounded Context)

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

上下文映射(Context Map)

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

image.png

共享内核(Shared Kernel)

image.png

总结

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

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

DDD开发案例

超市收银业务

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python中文社区

Python数据分析之基情的择天记

專 欄 ❈ 罗罗攀,Python中文社区专栏作者 专栏地址: http://www.jianshu.com/u/9104ebf5e177 ❈ 人一生都可能无...

23460
来自专栏java一日一条

程序员面试技巧总结

热爱技术。表达你对你所做的一切感到骄傲,你对自己的选择充满自信,你对语言和工作流有着自己的看法。

6820
来自专栏java工会

科大讯飞人工智能方向的一次面试经历

28650
来自专栏牛客网

趋势科技、美团、小米、网易互娱 C++后台开发offer面经

前言 经过六月到九月这几个月的努力,九风有幸拿到了趋势科技、小米、美团、网易互娱的offer,之前看了好多分享的面经,是时候该回馈了一波。 先介绍下九风的基本情...

57870
来自专栏数据结构与算法

1018. 打电话

1018. 打电话 (Standard IO) 时间限制: 1000 ms  空间限制: 262144 KB  具体限制  题目描述 某市的IC卡电话计费标准...

39590
来自专栏python小白到大牛

一行代码结果叹为观止,能做到这么极致的也只有python了

Python 这门语言非常的有趣,不仅可以做高大上的人工智能、大数据、机器学习。还可以用来做 Web、爬虫。还有其它很多的应用。今天我就给大家展示下一行 Pyt...

16030
来自专栏企鹅号快讯

Python实现拼图游戏

最近,要说最火的编程语言莫过于Python了。除了占据各种网站编程语言的排行榜之外,最劲爆的消息就是教育部将Python语言纳入计算机等级考试,并且山东某学校已...

64290
来自专栏PPV课数据科学社区

Eric Raymond 对于几大开发语言的评价

来源:孟岩 英文:Eric Raymond 链接:blog.csdn.net/myan/article/details/1923 【译注】:Eric Raym...

36660
来自专栏PPV课数据科学社区

为什么要选择Python语言实现机器学习算法

点击上方 “蓝色字” 可关注我们! 我们选择Python作为实现机器学习算法的编程语言:(1) Python的语法清晰;(2) 易于操作纯文本文件;(3) 使用...

38380
来自专栏博客园

设计模式学习---策略模式

最近在看"Head First 设计模式"这本书,便想将自己所学的记录下来以加深理解,文中肯定有许多不足之处,请各位前辈们指出.

11040

扫码关注云+社区

领取腾讯云代金券