如何从0到1实践DDD

5 评论 10996 浏览 75 收藏 24 分钟

编辑导语:DDD(Domain-driven design,领域驱动设计)是一种架构设计方法论,通过边界划分,将复杂业务领域简单化,帮助我们设计出清晰的领域和应用边界,保证业务模型与代码模型的一致性。本文作者结合实际经验,介绍了如何从0到1实践DDD,一起来看看吧。

随着业务的不断发展,我们发现自己的系统开始变得有点臃肿,为了减少复杂性,我们尝试借助DDD来改善我们的系统。本文记录了自己对DDD的理解和实践过程,欢迎大家一起探讨。见识所限,难免有理解不到位,希望路过的大佬不吝赐教。

一、为什么需要DDD

  • 当朋友和你聊工作时,你能否一语中的,说清你在开发中的业务内容及其价值?
  • 当产品和你聊需求时,你是否遇到过反复沟通之后才发现讲的不是同个东西的情况?
  • 当你在做需求评估时,你是否经常发现一个小的需求改动,总是牵一发动全身?
  • 当你在快乐写代码时,你是否经常觉得有些类可有可无,有些接口望文不知义?

如果你有以上的一些疑问,那你可以试试领域驱动设计:

DDD(Domain-driven design,领域驱动设计)是一种架构设计方法论,通过边界划分,将复杂业务领域简单化,帮助我们设计出清晰的领域和应用边界,保证业务模型与代码模型的一致性。

在细看这个定义之前,我们可以思考一下,为什么我们的业务系统会慢慢变得复杂?

常见的情况是,业务在发展过程中为了探寻发力点,需要不断地试错迭代,调整方向,而系统在设计之初,难以预期到后面的瞬息万变,为了应付业务,修修改改,久之,系统也变得复杂起来。

可以怎么办呢?及时重构呗——不改变软件系统外部行为的前提下,改善它的内部结构。

然而重构是从技术层面上抽炼出来的模型,往往不具有实际的业务含义,其他同学可能难以自然地将业务问题映射到对应的设计模型。另外,如果不能如实映射业务模型,随着业务方向调整,代码可能又开始腐败……有点像芝诺悖论中,阿基里斯永远追不上小乌龟。

如何从0到1实践DDD

那DDD怎么搞?

DDD是这么想的:”将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构”。可能大家平时有这样的想法,但是比较模糊,未形成体系,而DDD就提供了一套完整的方法论。从业务角度去审视我们的系统,从而实现高内聚低耦合的代码。

整体而言,领域驱动设计包括战略建模和战术建模: 战略设计侧重于高层次、宏观上去划分和集成限界上下文,而战术设计则关注更具体使用建模工具来细化上下文。

二、 如何实现DDD之战略建模

1. 基本概念

1)领域、子域

在讨论问题之前,我们需要先定义好问题。

领域即问题域,通常是根据一个组织所处的行业进行识别,它基于业务的愿景,定义了系统要解决的现实问题的目标和范围。领域越大,业务的范围也越大,大的领域可以拆分成小的问题域,称之为子域。根据子域重要性和功能属性划,可以将其分为三类。

核心域、支撑域和通用域:

  • 核心域:决定产品核心竞争力的子域
  • 支撑域:实现核心域目标所需的,但重要程度不如核心域的子域,一般具备强烈的个性化需求
  • 通用域:具有通用功能,可被多个子域使用的的是通用域。该子域所解决的问题一般是业界常见问题,有成熟的解决方案,可直接购买或简单修改来使用

这个几个概念其实很容易理解,不过在划分的时候,注意要从业务的视角,而不是技术功能模块来划分。

2)限界上下文

我们语言博大精深,同样的话在不同语境下就可演变出不同含义,这在沟通时总是带来不必要的麻烦。为了准确地沟通,我们需要统一语言的边界,在相同的语言边界内沟通,才不容易出差错。

一则阿凡提当理发师惩罚一个狡猾牧师的趣事:理发时,阿凡提刮脸时问牧师:“牧师,是否要眉毛?”牧师答:“这还用问,眉毛岂能不要?”.“好,你要我就给你!”,说着就把牧师的眉毛刮下来递到他手里,牧师气得说不出话来,谁叫自己说要呢。阿凡提又问:“牧师,胡子要吗?”.“不要,不要!”牧师连忙说。“好,你不要就不要。” 嗖嗖几刀就把牧师的胡子刮下来。

在一个系统中,一个名词在不同语境可能有不同的含义,我们对它关注的属性和行为也有所不同。例如,在电商系统中,对于产品Product, 在采购上下文,需要关注产品的进价、最小起订量与供货周期;在市场上下文中,则关心产品的品质、售价,以及用于促销的精美图片和销售类型;在仓储上下文中,仓库工作人员更关心产品放在仓库的哪个位置,产品的重量与体积,是否易碎品以及订购产品的数量。

限界上下文在《实现领域驱动设计》中,用了很大篇幅去讲,它有几个重要的意义:

  1. 限界上下文是领域概念的语言边界与业务边界:在这个边界内,领域概念的内涵是清晰、无歧义的
  2. 限界上下文是团队的工作边界:组织边界与限界上下文对齐
  3. 限界上下文是技术方案的实施边界:在这个边界内,技术方案是独立自治的,业务逻辑不会落入不同技术边界的间隙

经过战略建模之后,我们可以得到以下的一个模型:

如何从0到1实践DDD

2. 业务实践

为了更好地理解,我们对手上的一个项目:“IoT设备增值产品管理系统”进行实践。该项目中,我们提供给商户在IoT设备上管理增值运营产品的能力。这里的IoT设备主要是微信支付刷脸设备等。商户可以在系统中创建我们业务中的增值运营产品,如电子海报、互动海报等,创建完之后,相关的增值产品会被投放到IoT设备上,进行展示、运作:

如何从0到1实践DDD如何从0到1实践DDD

一开始我们从业务的用例出发,认为我们的系统主要是商户在我们页面网站使用,以及IoT设备通过接口连接我们后台服务,认为这两个分属不同的子域,然后梳理了一些支撑的功能:

如何从0到1实践DDD

画完草图之后,感觉不是很确定,于是便去咨询部门的DDD专家王老师(十分感谢王立老师的指导),得到了一些宝贵的建议:我们应该避免直接从表现层去看业务,表现层就像是冰山露在水面上的棱角,这些棱角看起来毫不相干,但是实际上底层是连成一块的,这些才是我们需要关注的。

就像这个项目,表面上商户和设备是分开的,实际上它们在操作都是我们的增值运营产品,应该看成我们的系统提供统一对外的服务,然后商户和设备来使用我们的服务。UGC内容存储业务用例其实没有涉及到的,属于实现时候的东西。一番建议让我们理清了思路,于是重新梳理,得到以下的战略建模图:

如何从0到1实践DDD

整体而言,我们将整体系统梳理为8个子域:

  1. 增值运营服务子域:核心域,是我们业务主要竞争力。从业务上来讲,我们的核心是通过提供业务中IoT设备上的增值运营服务
  2. 增值运营产品子域:支撑域,这里主要是我们提供增值运营产品,如电子海报、互动海报等
  3. 生效场景子域:支撑域,业务中增值运营产品有不同生效场景,这里统一进行管理
  4. 准入子域:支撑域,现主要是业务中对使用者的一些限制规则
  5. 权限管理子域:支撑域,基于角色来管理使用者的权限
  6. 商户信息子域:支撑域,提供商户的信息
  7. IoT设备信息子域:支撑域,提供IoT设备的信息
  8. 风险识别子域:通用域,识别业务中一些安全风险,如不合规的UGC素材等。这部分是业界常见问题,可以使用通用方案来解决,实际上我们也是接入TEG的能力来实现

其中我们系统中的商户信息依赖了微信支付商户账号信息和IoT设备铺设服务信息,这里使用防腐层进行隔离,将外部的商户信息“翻译”为我们业务中的商户信息。三、如何实现DDD之战术建模梳理清楚上下文之间的关系后,我们基本了解业务的概貌,接下来需要细化上下文,进一步完善我们的模型。这里也需要用到DDD的一些基本概念。

3. 基本概念

1)实体、值对象

实体和值对象是组成领域模型的基础单元。当一个对象由其标识(而不是属性)区分时,这种对象称为实体。如在校园教务系统中,每个账户是对应着一个学生,根据学号来唯一标识,可以认为是一个实体。传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应 N 个实体从表。

与其不同,DDD 是先构建领域模型,再将业务对象映射为持久化对象。这可能导致DDD建立出来的实体,映射到具体数据库表时,可能是1对多,多对1的关系。

如一个账户实体,有它的基本信息和权限角色信息,可能就对应了2个持久化对象。另一方面,有时候为了某些查询场景的方便,会把教师账户、学生账户等对应成一个持久化对象,就成了多对1。

通过对象属性值来识别的对象,则可以认为是一个值对象。如地址信息{“省”: “广东省”,”市”:”深圳市”},我们是通过它的属性来区分出不同的地址。值对象实际上是想把一些不变的属性组合起来,减少系统的复杂性。在设计值对象的时候,需要满足以下的特性:

  1. 值对象相等性:可以通过对其属性的比较,来区分不同的值对象
  2. 不变性:需要保证值对象创建后就不能被修改,即不允许外部再修改其属性
  3. 可替换性:值对象是一个整体,当其描述的对象有变化时,需要用一个新的值对象来替换对于值对象,由于其具有不变性,且是通过属性来判断相等的,在设计对应的数据库持久化对象时,可以将其以JSON形式存储在数据库表的某一字段中

如何从0到1实践DDD

2)聚合、聚合根

在 DDD 中,实体和值对象是基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但是我们的一个业务流程中,一般会同时涉及多个实体、值对象的操作,这里业务逻辑紧密的实体和值对象便组合成一个聚合。

从数据层面来看,同个聚合内的数据需要保持强一致性

每一个聚合有一个聚合根实体,设置聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。聚合根可以看成是聚合的管理者,或是说handle。对内其协调实体和值对象完成业务逻辑。对外则提供通过聚合ID供其他聚合关联引用,屏蔽外部对内部实体的直接访问和修改。

建议的聚合设计原则:

  1. 在一致性边界之内确保不变性:聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性。
  2. 设计小聚合:如果聚合聚合包含过多的实体,会提高管理实体的复杂性,高频操作下容易并发冲突,降低了系统的性能。
  3. 在边界之外使用最终一致性:不同的聚合之间不要求强一致性,保证最终一致性。一次事务操作中,只修改一个聚合实例,如果需要修改多个实例,可以考虑通过异步的方式保证最终一致性。

3)领域服务

领域服务的定义:领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合(实体)或值对像上时,最好的方式便是使用领域服务。

举个例子,在一个路线导航的项目中,“路线”可能是其中的一个实体,如果业务中有“推荐路线上相关的美食”这样一个功能,那我们会想,这个功能应该归给哪个领域对象,给“路线”实体吗?有点不合适,应该路线本身关注的是起终点,时间人物等。

此时可以将其这个功能归为领域服务,它是一个路线状态无关的服务,输入路线各个节点,来得到沿路的各种美食。当然,要注意不要过度地使用领域服务,因为这很可能导致你把实体的行为都放在里面了,实体本身都变成了一些只有getter和setter的“贫血模型”。

4)领域事件

领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

领域事件含义很广泛,可以是业务流程的一个步骤,也可以是一个事件发生后触发的后续动作,缴费完成之后,触发短信通知;上面在设计聚合的时候,我们提到一个原则:在边界之外使用最终一致性,一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应通过领域事件,达到最终一致性。

实际上是通过事件驱动的这种异步方式,对系统进行解耦。当然,如果你觉得某两个步骤,业务流程上不允许是不一致的,那就得重新考虑将其归在同个聚合中了。

4. 业务实践

我们以增值运营服务上下文为例,根据上面的理解,结合业务实际,得到以下模型:

如何从0到1实践DDD

其中增值产品是其中的一个聚合根,通过该聚合根进行各种领域操作。海报缩略图是其中的一个领域服务,通过输入产品素材中的海报url,来得到一个海报的缩略图。

四、工程实践

传统的三层架构和DDD的分层结构:

如何从0到1实践DDD

在《领域驱动设计——软件核心复杂性的应对之道》一书中,Eric提出了这样的一种分层结构,将整个系统划分为四层:用户接口层、应用层、领域层和基础设施层。

用户接口层:用户接口层负责向用户显示信息和解释用户指令。

应用层:应用层相对来说是较“薄”的一层,主要是部署了应用服务。应用服务的实现中,它负责编排和转发下一层的领域层的接口,将要实现的功能委托给一个或多个领域对象来实现,本身只负责处理业务用例的执行顺序以及结果的拼装。

领域层:领域层是比较“厚”的一层,它包含聚合根、实体、值对象、领域服务等领域模型中的领域对象,实现了核心的业务逻辑。领域层和应用层的职责看起来有点模糊。

个人觉得,可以理解是应用层描述了一个具体操作从开始到结束的每一个环节,而领域层则是对其的细化,用来处理具体的某一个环节。

比如,比如线上购物中,购物车结算这一场景可看成是一个应用行为。而这个行为又主要包括金额计算、支付、生成订单,这些子环节就可以理解为一个领域层的服务了。

基础设施层:可以看到上面三层都有箭头指向基础设施层,它的作用就是为其它各层提供通用的技术和基础服务,如数据持久化、消息中间件等DDD 分层架构中的要素与传统三层架构(用户界面层、业务逻辑层、数据访问层)还是挺相似的,一个主要的变化是将业务逻辑层的服务拆分到了应用层和领域层。应用层响应业务用例的变化,领域层关注不变的领域模型。

如何从0到1实践DDD

图片来自极客时间

《DDD实战课》在实际的代码工程便是按照这样的目录来划分,最近部门在推的整洁Git,也是这样划分目录:

如何从0到1实践DDD

接下来,便是将领域对象映射到实际的类,实现对应的属性和行为。当然,具体实现中有很多范式可参考和讨论,我们也在摸索中,待后续慢慢补充……

五、总结

DDD首先不是关于技术的,而是关于讨论、聆听、理解、发现业务价值的。——Vaughn Vernon《实现领域驱动设计》

如Vernon所说的,DDD首先是关注业务的价值的。一开始我们对业务的边界、目标可能有个大概了解,但是见解还是不尽相同。

通过一起对业务的讨论与思考,我们了解了业务的概貌及核心,明确价值所在。关注到了核心,自然可以帮助我们实现与业务契合的系统。通过这次学习与实践,我们进一步接触了DDD。

当然,这也还只是开始,更多的关联知识还隐藏在冰山之下。同时我们也明白,DDD也只是一种方法论上的参考,不是“银弹”,需要不断地去实践与思考,才能体会出它的价值。

参考:

  • Eric Evans.领域驱动设计.赵俐 盛海艳 刘霞等译.人民邮电出版社,2016.
  • 美团技术团队.领域驱动设计在互联网业务开发中的实践:https://tech.meituan.com/2017/12/22/ddd-in-practice.html?spm=a2c4e.10696291.0.0.428119a4uu9Gpl
  • 极客时间.DDD实战课:https://time.geekbang.org/column/article/152677

 

作者:bryanzhao,微信支付后台开发工程师

本文由 @腾讯大讲堂 原创发布于人人都是产品经理,未经许可,禁止转载。

题图来自 Pixabay,基于CC0协议。

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 产品经理在DDD中应该输出哪些内容?领域划分?由领域抽象出来的实体?

    来自福建 回复
  2. 个人觉得DDD不应该替代代码层的MVC结构,主要还是用来做产品和微服务架构的指导,用来划分清楚系统模块边界

    来自天津 回复
  3. 业务架构都已经理解,但是关于技术方面的架构设计还是不太理解

    来自湖南 回复
  4. 同上,不适合小白

    来自湖南 回复
  5. DDD,哈哈哈,我感觉因该叫3D,乍一眼还真的不知道要讲什么,看完之后,果然不是干这个的,真的不怎么清楚。

    来自河南 回复