mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
mod
This commit is contained in:
114
极客时间专栏/软件设计之美/设计一个软件—设计方法/27 | 领域驱动设计:如何从零开始设计一个软件?.md
Normal file
114
极客时间专栏/软件设计之美/设计一个软件—设计方法/27 | 领域驱动设计:如何从零开始设计一个软件?.md
Normal file
@@ -0,0 +1,114 @@
|
||||
<audio id="audio" title="27 | 领域驱动设计:如何从零开始设计一个软件?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f1/69/f180f97cce21d54f36yy4bd01f4ce569.mp3"></audio>
|
||||
|
||||
你好!我是郑晔。
|
||||
|
||||
在前面的内容中,我给你讲了软件设计的各种基本工具。我们学习了程序设计语言,知道怎样把设计更好地落地;学会了各种编程范式,知道了可以用什么样的元素去做设计;我们还学习了设计原则与模式,知道了怎样组合分解出来的各个元素。
|
||||
|
||||
工具都有了,理论也武装上了,那么我们该如何实践呢?或者说,我要去分解组合的东西是从哪而来的呢?这就需要你对**设计方法**有一个基本的认知了,也就是说,我们要理解,在真实世界中,解决具体问题是怎样的一个过程。
|
||||
|
||||
那从这一讲开始,我们就来谈谈设计方法的话题,一起了解一下设计的基本过程。
|
||||
|
||||
首先,你知道哪些设计方法呢?
|
||||
|
||||
我知道的一种做法是,有些人一上来会先设计数据库,因为他们觉得,程序就是数据加函数。数据呢,就要存到数据库里,剩下的就是根据需要对数据库表进行增删改查。但是从我们之前的讲解中,你可以看出,这种思路实际上是一种结构化编程的思路。
|
||||
|
||||
后来有人就用面向对象的思路,先来找实体,也就是对象,当然这些实体也要有一些能力。最终,这些对象还是要写到数据库里,同样也是要提供增删改查的能力。
|
||||
|
||||
你看,这两种做法本质上没什么太大的区别,都是**围绕着数据在做文章**。在业务需求不复杂的年代,围绕数据做文章的做法还能满足开发的要求,但随着软件日益深入到人们日常工作和生活中,软件变得越来越复杂,这种做法就越发显得笨拙了。
|
||||
|
||||
对,软件会越来越复杂的。当软件变得复杂起来,如果我们靠着程序员们本能的做法,就会遇到各种问题,所以,很多人探索了不同的做法。
|
||||
|
||||
在诸多的探索之中,有一种做法逐渐脱颖而出,它成功地解决业务软件开发中遇到的大部分问题,这就是**领域驱动设计**。虽然它不是万能药,但对大部分人面对的场景而言,它都能够有效地应对。
|
||||
|
||||
## 领域驱动设计
|
||||
|
||||
领域驱动设计(Domain Driven Design,简称 DDD),作为一个新的设计方法正式登上历史舞台,是从 Eric Evans 的著作《领域驱动设计》正式出版开始的。
|
||||
|
||||
这种设计方法通过使用通用语言,让业务人员加入到设计过程中,拉近了业务人员与开发人员之间的距离,打破了组织的藩篱。同时,还提供了一套标准的建模方法,帮助团队识别业务模型,避免程序员犯下一些低级错误。
|
||||
|
||||
按理说,这种设计方法这么好,应该很快流行起来才对。然而真实情况是,很多程序员都不知道 DDD,一个重要的原因就是 Eric 的这本书写得实在不怎么样。要想从中读出味道,你得比较懂DDD,但是,大多数人并不懂,这就是矛盾的地方。所以,DDD在很长一段时间都被埋没了。
|
||||
|
||||
不过,后来,随着**微服务**的兴起,人们越发认识到,微服务的难度并不在于将一个系统拆分成若干的服务,而在于如何有效地划分微服务。这个时候,人们发现,DDD才是最恰当的指引。关于微服务和 DDD 之间的关系,我在[《10x 程序员工作法》](https://time.geekbang.org/column/intro/100022301)里已经讲过了,有兴趣的可以去了解一下,这里就不再赘述了。
|
||||
|
||||
现在,你已经知道了DDD的好,准备学习DDD 了。但你只要一打开 DDD 的书,一大堆名词就会扑面而来:限界上下文、聚合根、实体、值对象,等等。我该如何下手学习呢?这是摆在每个DDD学习者面前最严峻的问题。
|
||||
|
||||
**学习 DDD,就要从理解 DDD 的根基入手:通用语言(Ubiquitous Language)和模型驱动的设计(Model-Driven Design)**,而领域驱动设计的过程,就是建立起通用语言和识别模型的过程。
|
||||
|
||||
## 通用语言
|
||||
|
||||
**通用语言,就是在业务人员和开发人员之间建立起的一套共有的语言**。要知道,在从前的设计方法中,业务人员总是把问题扔过墙头,让开发人员去解决。可是,业务人员说的都是业务名词,比如:产品、订单等等,而开发人员嘴里全是技术,比如:线程、存储等等,二者除了最基础的几个概念之外,其他的内容基本是没法沟通的。所以,一道人为鸿沟就在开发人员和业务人员之间形成了。
|
||||
|
||||
我们在[第1讲](https://time.geekbang.org/column/article/240177)说过,**软件设计是要在问题和解决方案架设一座桥梁,好的设计要更接近问题**。开发人员对解决方案一端简直再熟悉不过了,但是对业务一端理解则通常不够充分。而通用语言所做的事情,就是把开发人员的思考起点拉到了业务上,也就是从问题出发,这就在一定程度上填平了那道人为的鸿沟。
|
||||
|
||||
通用语言是什么呢?就是这个业务中有哪些概念以及哪些操作。比如说,我要做一个电商平台,就要有产品、订单的概念。其中,产品就要有上架、下架、修改产品信息等操作,而订单就会有下单、撤单、修改订单等操作。
|
||||
|
||||
在业务人员看来,这里说的都是自己擅长的事情,自己就可以有更多的发言权。在开发人员的视角,概念就是一个一个的类,操作就是一个一个的方法,也很好理解。所以,有一套通用语言,双方皆大欢喜。
|
||||
|
||||
但是,通用语言是从哪来的呢?也就是说,如何设计通用语言呢?最简单的做法就是让业务人员和开发人员一起,找一块白板,把各种概念都写在上面。然后,双方重新进行分类整理。
|
||||
|
||||
这里面的重点是,让业务人员和开发人员在一起。如果只让一方出现,结果又会是原来的样子,因为你没法判断,这里面的语言对方是否听得懂。
|
||||
|
||||
这种做法很简单,但通常都不够系统,会存在各种遗漏。所以,有人探索出一种更正式的实践:**事件风暴**(Event Storming)。
|
||||
|
||||
[事件风暴](https://www.eventstorming.com/)是一个工作坊,基本做法就是找一面很宽的墙,上面铺上大白纸,然后,用便利贴把识别出来的概念贴在上面。当然,前提依然是让业务人员和技术人员都参与其中。
|
||||
|
||||
这个实践之所以叫作事件风暴,因为它的关注点在于**领域事件**。领域事件是用来记录业务过程中发生过的重要事情,比如,作为电商平台的工作人员,你想知道产品是不是已经上架了,这个领域事件就是产品已上架;作为消费者,你会关心我的订单是不是下成功了,这个领域事件就是订单已下。
|
||||
|
||||
人们做了一个动作,都会关心做过这个动作之后的结果,所以,领域事件用的描述方式都是过去式,比如:OrderPlaced。
|
||||
|
||||
事件风暴这个工作坊主要分成三步:
|
||||
|
||||
- **第一步就是把领域事件识别出来**,这个系统有哪些是人们关心的结果。有了领域事件,下面一个问题是,这些事件是如何产生的,它必然会是某个动作的结果。
|
||||
- **第二步就是找出这些动作,也就是引发领域事件的命令**。比如:产品已上架是由产品上架这个动作引发的,而订单已下就是由下单这个命令引发的。
|
||||
- **第三步就是找出与事件和命令相关的实体或聚合**,比如,产品上架就需要有个产品(Product),下单就需要有订单(Order)。
|
||||
|
||||
至此,我们已经把最核心的内容找出来了。通常,在工作坊过程中,为了增强趣味性和清晰性,不同的概念会用不同的颜色的便利贴标识出来,比如,领域事件用橙色、命令用蓝色、实体/聚合用黄色等等。
|
||||
|
||||
其实,用不同的颜色建模,事件风暴并不是独一份。Peter Coad也曾提出过一种[四色建模](https://en.wikipedia.org/wiki/Object_Modeling_in_Color)的方法:
|
||||
|
||||
- 粉色表示时标性对象(moment-interval);
|
||||
- 黄色表示角色(role);
|
||||
- 蓝色表示描述(description);
|
||||
- 绿色表示人、地点、物(party/place/thing)。
|
||||
|
||||
他还写了一本[《彩色UML建模》](https://book.douban.com/subject/3354137/)(Java Modeling in Color with UML)介绍这种方法。我在ThoughtWorks的前同事徐昊按照自己的理解,对这种方法做了一些更新,有兴趣的话,可以去[了解一下](https://www.infoq.cn/article/xh-four-color-modeling)。
|
||||
|
||||
当然,这里的事件风暴,我只是描述了最简单的一个过程。在具体实施的过程中,还会有更多的细节。不过,最重要的还是,让不同角色的参与其中,让知识在所有人的头脑中进行构建,得到一个大家都认同的结果。
|
||||
|
||||
## 模型驱动设计
|
||||
|
||||
有了通用语言,接下来就进入模型设计阶段了。虽然有了通用语言,但是业务人员能够帮到开发人员的还是很少,他们只能告诉开发人员哪些模型是符合业务概念的。
|
||||
|
||||
但这么多的业务模型,该如何组织呢?怎样补全欠缺的模型,使之成为一个可以落地的方案呢?这就是开发人员要想办法解决的事情了。
|
||||
|
||||
也正是因为在通常情况下,业务模型数量众多,所以在 DDD 的过程中,我们将设计分成了两个阶段:**战略设计**(Strategic Design)和**战术设计**(Tactical Design)。
|
||||
|
||||
战略设计是高层设计,是指将系统拆分成不同的领域。而领域驱动设计,核心的概念就是领域,也就是说,它给了我们一个拆分系统的新视角:按业务领域拆分。
|
||||
|
||||
比如,我把一个电商系统拆分成产品域、订单域、支付域、物流域等。拆分成领域之后,我们识别出来的各种业务对象就会归结到各个领域之中。然而,有时候,不同领域的业务对象会进行交互,比如,我要知道自己订单的物流情况。所以,要在不同的领域之间设计一些交互的方式。
|
||||
|
||||
而战术设计是低层设计,也就是如何具体地组织不同的业务模型。在这个层次上,DDD 给我们提供了一些标准的做法供我们参考。比如,哪种模型应该设计成实体,哪些应该设计成值对象。
|
||||
|
||||
我们还要考虑模型之间是什么样的关系,比如,哪些模型要一起使用,可以成为一个聚合。接下来,我们还需要考虑这些模型从哪来、怎样演变,DDD 同样为我们提供了一些标准的设计概念,比如仓库、服务等等。
|
||||
|
||||
通过战略设计和战术设计,我们就可以把发现出来的不同业务概念各归其位了。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们讲了领域驱动设计,这是目前在软件行业内最符合软件发展趋势的一种设计方法,因为它把软件设计的起始点从技术拉到了业务。
|
||||
|
||||
学习领域驱动设计,我们要从通用语言和模型驱动设计入手。通用语言是在业务人员和技术人员之间建立一套共有的语言,开发通用语言的一种实践是事件风暴,这是一种工作坊,通过识别领域事件找到引发事件的命令,找出与事件和命令相关的实体或聚合,帮助团队建立通用语言。
|
||||
|
||||
DDD 的模型设计可以分为战略设计和战术设计。战略设计是高层设计,将系统拆分成领域,战术设计是低层设计,考虑如何组织不同的模型。
|
||||
|
||||
好,我们已经对 DDD 有了一个初步的了解。接下来的两讲,我们就分别来看看,如何进行战略设计和战术设计。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**建立一套业务人员和开发人员共享的通用语言**。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/03/88d3dd98642fd2d2d619a435d0d85903.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你们在实际工作中是如何与业务人员沟通的?欢迎在留言区写下你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
121
极客时间专栏/软件设计之美/设计一个软件—设计方法/28 | 战略设计:如何划分系统的模块?.md
Normal file
121
极客时间专栏/软件设计之美/设计一个软件—设计方法/28 | 战略设计:如何划分系统的模块?.md
Normal file
@@ -0,0 +1,121 @@
|
||||
<audio id="audio" title="28 | 战略设计:如何划分系统的模块?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/bc/68a9b51d6801cdb5yy09db23f56d66bc.mp3"></audio>
|
||||
|
||||
你好!我是郑晔。
|
||||
|
||||
上一讲,我们已经初步认识了 DDD ,知道了支撑 DDD 最核心的就是通用语言和模型驱动设计的方法。我们在上一讲已经讲了建立通用语言的方法,接下来,就该进行模型的设计了。
|
||||
|
||||
在模型设计中,DDD 又分成了两个阶段,战略设计和战术设计。这一讲,我们先来聊聊战略设计,下一讲再来谈战术设计。
|
||||
|
||||
战略设计,这个名字听上去有点高大上。而且,战略设计包含很多的概念,比如,子域、限界上下文和上下文映射图等等。这让很多人有些望而却步。虽然概念看似很多,但只要有一条主线将它们贯穿起来,这些概念也不难理解。
|
||||
|
||||
我们可以先把这些概念做一个划分,分为**做业务的划分和落地成解决方案**两个部分,也就是说,战略设计中的概念,一部分是为了将不同的业务区分开来,也就是要将识别出来的业务概念做一个划分,另一部分则是将划分出来的业务落实到真实的解决方案中。
|
||||
|
||||
好,我们接下来就先来看看战略设计中的这些概念到底是怎么回事。
|
||||
|
||||
## 业务概念的划分
|
||||
|
||||
我们前面说过,软件开发就是在解决问题,所以,一方面,我们要知道要解决的问题是什么;另一方面,我们要知道怎么去解决问题。
|
||||
|
||||
我们要解决的问题就是领域问题,在 DDD 中,有几个概念是与领域相关的,比如,子域、核心域、支撑域、通用域等。其实,它们说的都是一件事,就是如何先把问题从大面上进行分解。
|
||||
|
||||
领域驱动设计这个名字里面,排在第一位的是**领域(Domain)**,它就对应着要解决的问题。正如我们一直说的,软件开发是解决问题,而解决问题要分而治之。所谓分而治之,就是要把问题分解了,对应到领域驱动设计中,就是要把一个大领域分解成若干的小领域,而这个分解出来的小领域就是**子域(Subdomain)**。
|
||||
|
||||
我们在上一讲中说,领域驱动设计首先要建立起一套通用语言,这样一来,我们就拥有了各种各样的词汇,它们对应着模型。接下来,我们就要给这些词汇做个分类,而分类就是要把它们划分到不同的子域中去。这里面的关键就在于,要找出不同的关注点。没错,还是分离关注点。
|
||||
|
||||
比如,我要做一个项目管理软件,就需要有用户、有项目、有团队,不同的人还要扮演不同的角色。第一步,我们至少可以先把身份管理和项目管理这两件事分开,因为它们的关注点是不同的。身份管理关注的是用户的身份信息,诸如用户名密码之类的,而项目管理关注的重点是项目和团队之类的。所以,我们这里有了两个子域:身份管理和项目管理。
|
||||
|
||||
如果直接给你看结果,你可能会觉得很好理解。但是,划分出不同的子域还是比较容易出问题的,因为有一些概念并不容易区分。比如,用户应该怎么划分呢?放在身份管理是合适的,但项目管理也要用到用户。
|
||||
|
||||
幸好,我们已经学习了单一职责原则,它给了我们一个重要的思考维度,变化从何而来。不同角色的人会关注不同的变化,所以,我们知道虽然我们用的词都是“用户”,但我们想表达的含义却是不同的,我们最好将这些不同的含义分开,也就是将不同的角色分开。
|
||||
|
||||
比如,在身份管理中,它是“用户”,而在项目管理中,它就成了“项目成员”。所以,我们划分子域实际上就是在把不同的概念区分开来,让它们各归其位。
|
||||
|
||||
对于一个真实项目而言,划分出来的子域可能会有很多,但并非每个子域都一样重要。所以,我们还要把划分出来的子域再做一下区分,分成核心域(Core Domain)、支撑域(Supporting Subdomain)和通用域(Generic Subdomain)。
|
||||
|
||||
核心域是整个系统最重要的部分,是整个业务得以成功的关键。关于核心域,Eric Evans 曾提出过几个问题,帮我们识别核心域:
|
||||
|
||||
- 为什么这个系统值得写?
|
||||
- 为什么不直接买一个?
|
||||
- 为什么不外包?
|
||||
|
||||
如果你对这几个问题的回答能够帮你找到这个系统非写不可的理由,那它就是你的核心域。
|
||||
|
||||
什么是支撑域呢?有一些子域不是你的核心竞争力,但却是系统不得不做的东西,市场上也找不到一个现成的方案,这种子域就是支撑域。比如,我们要做一个排行榜功能,可能根据各种信息做排名,这种东西没有人会按照你的需要做出一个,对你来说,又是扩展自己系统的重要一步,它就是一个支撑域。
|
||||
|
||||
还有一种子域叫通用域,就是行业里通常都是这么做,即便不自己做,也并不影响你的业务运行。比如,很多 App 要给用户发通知,这样的功能完全可以买一个服务来做,丝毫不影响你的业务运行。它就是一个通用域。
|
||||
|
||||
我们之所以要区分不同的子域,关键的原因就在于,我们可以决定不同的投资策略。核心域要全力投入,支撑域次之,通用域甚至可以花钱买服务。
|
||||
|
||||
## 业务概念的落地
|
||||
|
||||
通过划分子域,区分核心域、支撑域和通用域,我们把DDD在问题层面的概念已经说清楚了。接下来,就要进入到解决方案层面了。
|
||||
|
||||
我们现在有了切分出来的子域,怎样去落实到代码上呢?首先要解决的就是这些子域如何组织的问题,是写一个程序把所有子域都放在里面呢,还是每个子域做一个独立的应用,抑或是有一些在一起,有一些分开。
|
||||
|
||||
这就引出了领域驱动设计中的一个重要的概念,限界上下文(Bounded Context)。
|
||||
|
||||
**限界上下文,顾名思义,它形成了一个边界,一个限定了通用语言自由使用的边界,一旦出界,含义便无法保证**。比如,同样是说“订单”,如果不加限制,你很难区分它是用在哪种场景之下。而一旦定义了限界上下文,那交易上下文的“订单”和物流上下文的“订单”肯定是不同的。原因就在于,订单这个说法,在不同的边界内,含义是不一样的。
|
||||
|
||||
注意,子域和限界上下文不一定是一一对应的,可能在一个限界上下文中包含了多个子域,也可能在一个子域横跨了多个限界上下文。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/ec/4188375e5221108bc6e59ab47d9c30ec.jpg" alt="">
|
||||
|
||||
前面我们说了限界上下文是在解决方案层面的,所以,很自然地,我们就可以把限界上下文看作是一个独立的系统。很多团队做微服务的时候,最纠结的问题就是如何划分服务边界,而限界上下文的出现刚好与微服务的理念契合,每个限界上下文都可以成为一个独立的服务。
|
||||
|
||||
**限界上下文的重点在于,它是完全独立的,不会为了完成一个业务需求要跑到其他服务中去做很多事**,而这恰恰是很多微服务出问题的点,比如,一个业务功能要调用很多其他系统的功能。
|
||||
|
||||
有了对限界上下文的理解,我们就可以把整个业务分解到不同的限界上下文中,但是,尽管我们拆分了系统,它们终究还是一个系统,免不了彼此之间要有交互。
|
||||
|
||||
比如,一个用户下了订单,这是在订单上下文中完成的。那接下来,用户要去支付,这是在支付上下文中完成的。我们肯定要通过某种途径让订单上下文的一些信息发送到支付上下文里的。
|
||||
|
||||
所以,我们就要有一种描述方式,将不同限界上下文之间交互的方式描述出来,这就是上下文映射图(Context Map)。DDD 给我们提供了一些描述这种交互的方式,比如:
|
||||
|
||||
- 合作关系(Partnership);
|
||||
- 共享内核(Shared Kernel);
|
||||
- 客户-供应商(Customer-Supplier);
|
||||
- 跟随者(Conformist);
|
||||
- 防腐层(Anticorruption Layer);
|
||||
- 开放主机服务(Open Host Service);
|
||||
- 发布语言(Published Language);
|
||||
- 各行其道(Separate Ways);
|
||||
- 大泥球(Big Ball of Mud)。
|
||||
|
||||
之所以有这么多不同的交互方式,主要是为了让你在头脑中仔细辨认一下,看看限界上下文之间到底在以怎样的方式进行交互。
|
||||
|
||||
当然这么多交互方式,想一次性记住也是不现实的,有些甚至是你要规避的,比如大泥球。如果说这么多交互方式你必须要记住一个的话,那就是防腐层(Anticorruption Layer)。
|
||||
|
||||
防腐层是最具防御性的一种关系,简言之,就是指我们要在外部模型和内部模型之间建立起一个翻译层,将外部模型转化为内部模型。我在第1讲给你讲了一个因为没有建立防腐造成的问题。所以,但凡有可能,就要建立防腐层,将外部模型完全隔离开。
|
||||
|
||||
当我们知道了不同的限界上下文之间采用哪种交互方式之后,不同的交互方式就可以落地为不同的协议。现在最常用的几种协议有REST API、RPC 或是消息队列,我们可以根据实际情况进行选择。
|
||||
|
||||
在我们定义好不同的限界上下文,将它们之间的交互呈现出来之后,我们就得到了一张上下文映射图。上下文映射图是可以帮助我们理解系统的各个部分之间,是怎样进行交互的,帮我们建立一个全局性的认知,而这往往是很多团队欠缺的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/60/f4/606f7d3f445117475652409a837caff4.jpg" alt="">
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们主要讲了DDD 中的战略设计。战略设计中的概念主要是为了**做业务的划分和落地成解决方案**。
|
||||
|
||||
首先业务的划分,我们要把识别出来的模型做一个分类,把它们放置到不同的子域中。划分子域的出发点就是不同的关注点,也就是不同的变化来源。
|
||||
|
||||
划分出来的子域有着不同的重要程度,我们将它们再分为核心域、支撑域和通用域。做出这种区分,主要是为了针对它们各自的特点,决定不同的投入。
|
||||
|
||||
有了不同的领域划分,我们还要把这些领域映射到解决方案上,这就引出了限界上下文。限界上下文限定了模型的使用边界,它可以成为一个独立的系统。如果对应到微服务中,每一个限界上下文可以对应成一个微服务。
|
||||
|
||||
上下文映射图定义了不同上下文之间的交互方式,如果你只能记住一种交互方式的话,就应该记住防腐层。
|
||||
|
||||
按照我们之前介绍的了解软件设计的思路,建立起通用语言之后,我们就找到了主要的模型,通过战略设计,我们可以把识别出来的模型放到不同的限界上下文中,就相当于把模型做了分组。然后,我们需要定义出一些接口,让不同的模型之间可以交互,我们也就有了一张上下文映射图。
|
||||
|
||||
这样一来,我们就把之前学习的知识和新的知识建立起了连接。
|
||||
|
||||
我们有了模型,有了接口,接下来就该深入到实现中。下一讲,我们就要进一步了解 DDD 的实现:战术设计。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**战略设计,就是将不同的模型进行分组**。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/51/2e0c3075edbe4f9817751abe041e8d51.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你的项目在模型的分组上哪些地方做得好,哪些地方做得不够好呢?欢迎在留言区写下你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
130
极客时间专栏/软件设计之美/设计一个软件—设计方法/29 | 战术设计:如何像写故事一样找出模型?.md
Normal file
130
极客时间专栏/软件设计之美/设计一个软件—设计方法/29 | 战术设计:如何像写故事一样找出模型?.md
Normal file
@@ -0,0 +1,130 @@
|
||||
<audio id="audio" title="29 | 战术设计:如何像写故事一样找出模型?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/fb/03282dc929677028001913a242ebd3fb.mp3"></audio>
|
||||
|
||||
你好,我是郑晔!
|
||||
|
||||
在上一讲中,我们讲了 DDD 中的战略设计,学习如何将识别出来的不同模型放到不同的限界上下文中。那么,接下来,我们就该做更具体的工作了,也就是如何设计模型。在 DDD 中,把具体的模型找出来的做法有一个更响亮的名字:战术设计。
|
||||
|
||||
战术设计同样也包含了很多概念,比如,实体、值对象、聚合、领域服务、应用服务等等。有这么多概念,我们该如何区分和理解他们呢?我们同样需要一根主线。
|
||||
|
||||
其实,我们可以把战术设计理解成写一个故事。你知道怎样去写个故事吗?写故事通常都是有一定套路的。我们要先构建好故事的背景,然后,要设定不同的角色,接下来,创建角色之间的关系,最后,我们要安排人物之间互动起来,形成故事。
|
||||
|
||||
对于战术设计而言,故事的背景就是我们面对的领域问题,剩下的就是我们在这个故事背景下,要找出不同的角色,找出角色之间的关系,让它们互动起来,这样,我们就有了故事,也完成了战术设计。
|
||||
|
||||
接下来,我们就来看看,战术设计这个故事模板,我们应该怎么填?
|
||||
|
||||
## 角色:实体、值对象
|
||||
|
||||
我们的首要任务就是设计角色,在战术设计中,我们的角色就是各种名词。我们在初学面向对象的时候,课本上的内容就告诉我们要识别出一个一个的模型,其实,就是让我们识别名词。
|
||||
|
||||
识别名词也是很多人对于面向对象的直觉反应。有一些设计方法会先建立数据库表,这种做法本质上也是从识别名词入手的。**我们在战术设计中,要识别的名词包括了实体和值对象。**
|
||||
|
||||
什么是实体呢?**实体(Entity)指的是能够通过唯一标识符标识出来的对象。**
|
||||
|
||||
我们都知道,在业务处理中,有一类对象会有一定的生命周期。我以电商平台上的订单为例,它会在一次交易的过程中存在,而在它的生命周期中,它的一些属性可能会有变化,比如说,订单的状态刚开始是下单完成,然后在支付之后,变成了已支付,在发货之后就变成了已发货。
|
||||
|
||||
但是这个订单始终都是这个订单,因为这个订单有唯一的标识符,也就是订单号,订单号作为它的标识符能将它标识出来。你可以通过订单号查询它的状态,可以修改订单的一些信息,比如,配送的地址。像这种通过唯一标识符标识出来的对象,就是实体。
|
||||
|
||||
其实,大多数程序员对于实体并不陌生,因为在各种设计方法中,都有相应的方法识别实体。你甚至可以简单粗暴地将它理解成数据库里存储的对象,虽然这种理解并不完全正确。
|
||||
|
||||
**还有一类对象称为值对象,它就表示一个值。**比如,订单地址,它是由省、市、区和具体住址组成。它同实体的差别在于,它没有标识符。之所以它叫值对象,是因为它表现得像一个值。值对象可能会有很多属性,而要想判断值对象是否相等,我们就要判断这些属性是否相等。对于两个订单地址来说,只有省、市、区和具体住址等多个属性都相同,我们才认为它们是同一个地址。
|
||||
|
||||
实体的属性是可以变的,只要标识符不变,它就还是那个实体。但是,值对象的属性却不能变,一旦变了,它就不再是那个对象,所以,我们会把值对象设置成一个不变的对象。在前面讲函数式编程的不变性时,我给你介绍了不变性的诸多好处,这里也完全适用于值对象。
|
||||
|
||||
那你现在应该懂了,**我们为什么要将对象分为实体和值对象?其实主要是为了分出值对象**,也就是把变的对象和不变的对象区分开。在传统的做法中,找出实体是你一定会做的一件事,而在不同的模型中,区分出值对象是我们通常欠缺的考虑。
|
||||
|
||||
一方面,我们会把一些值对象当作实体,但其实这种对象并不需要一个标识符;另一方面,也是更重要的,就是很多值对象我们并没有识别出来,比如,很多人会用一个字符串表示电话号码,会用一个 double 类型表示价格,而这些东西其实都应该是一个值对象。
|
||||
|
||||
之所以说这里缺少了对象,原因就在于,这里用基本类型是没有行为的。在 DDD 的对象设计中,对象应该是有行为的。比如,价格其实要有精度的限制,计算时要有自己的计算规则。如果不用一个类将它封装起来,这种行为就将散落在代码的各处,变得难以维护。
|
||||
|
||||
其实,我们在讨论面向对象的封装时就已经说过了,只有数据的对象是封装没做好的结果,一个好的封装应该是基于行为的。在 DDD 的相关讨论中,经常有人批评所谓的“贫血模型”,说的其实就是这种没有行为的对象。你可以回头复习一下[第15讲](https://time.geekbang.org/column/article/252605),我就不在这里赘述了。
|
||||
|
||||
## 关系:聚合和聚合根
|
||||
|
||||
选定了角色之后,接下来,我们就该考虑它们的关系了。
|
||||
|
||||
在传统的开发中,我们经常会遇到一个难题。比如,如果我有一个订单,它有自己对应的订单项。问题来了,我取订单的时候,该不该把订单项一起取出来呢?取吧,怕一次取出来东西太多;不取吧?要是我用到了,再去一条一条地取,太浪费时间了。
|
||||
|
||||
这就是典型的一对多问题,只不过,在其他场景中,主角就变成了各种其他的对象。
|
||||
|
||||
不过,这也是一种用技术解决业务问题的典型思路。我们之所以这么纠结,主要就是因为我们考虑问题的出发点是技术,如果我们把考虑问题的出发点放到业务上呢?
|
||||
|
||||
战术设计就给了我们这样一个思考的维度:聚合。**聚合(Aggregate)就是多个实体或值对象的组合,这些对象是什么关系呢?你可以理解为它们要同生共死。**比如,一个订单里有很多个订单项,如果这个订单作废了,这些订单项也就没用了。所以,我们基本上可以把订单和订单项看成一个单元,订单和订单项就是一个聚合。
|
||||
|
||||
学习 DDD 时,有人会告诉你,聚合要保证事务(Transaction)一致性。简言之,就是要更新就一起更新,要删除就一起删除。只要你理解了它们是一个整体,你就不难理解为什么这些对象要一起操作了。
|
||||
|
||||
不过,一个聚合里可以包含很多个对象,每个对象里还可以继续包含其它的对象,就像一棵大树一层层展开。但重点是,这是一棵树,所以,它只能有一个树根,这个根就是聚合根。
|
||||
|
||||
聚合根(Aggregate Root),就是从外部访问这个聚合的起点。我还以上面的订单和订单项为例,在订单和订单项组成的这个聚合里,订单就是聚合根。因为你想访问它们,就要从订单入手,你要通过订单号找到订单,然后,把相关的订单项也一并拿出来。
|
||||
|
||||
其实,我们可以把所有的对象都看成是一种聚合。只不过,有一些聚合根下还有其他的对象,有一些没有而已。这样一来,你就有了一个统一的视角看待所有的对象了。所以,我们也可以用统一的标准要求聚合,比如,聚合不能设计得太大。你有没有发现,这其实就是单一职责原则在聚合上的应用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/f7/7fca1371e0b3443b0bceaa412cd6d2f7.jpg" alt="">
|
||||
|
||||
那如果不同的聚合之间有关系怎么办?比如,我要在订单项里知道到底买了哪个产品,这个时候,我在订单项里保存的不是完整的产品信息,而是产品ID。还记得吗?我们在前面说过,实体是有唯一标识符的。如果需要,我们就可以根据产品 ID 找出产品信息。
|
||||
|
||||
有了对于聚合的理解,做设计的时候,我们就要识别出哪些对象可以组成聚合。所以,我们的一对多问题也就不再是问题了:是聚合的,我们可以一次都拿出来;不是聚合的,我们就靠标识符按需提取。**当你纠结于技术时,先想想自己是不是解错了问题。**
|
||||
|
||||
## 互动:工厂、仓库、领域服务、应用服务
|
||||
|
||||
我们现在有角色了,也确定关系了。接下来,我们就要安排互动了,也就是说,我们要把故事的来龙去脉讲清楚了。
|
||||
|
||||
还记得[第27讲](https://time.geekbang.org/column/article/266819)的事件风暴吗?我们在其中识别出了事件和动作,而故事的来龙去脉其实就是这些事件和动作。因为有了各种动作,各种角色才能够生动地活跃起来,整个故事才得以展开。
|
||||
|
||||
动作的结果会产生出各种事件,也就是**领域事件**,领域事件相当于记录了业务过程中最重要的事情。相对于 DDD 中的其他概念,领域事件加入 DDD 大家庭是比较晚的,但因为其价值所在,它迅速地就成了DDD 中不可或缺的一个重要概念。
|
||||
|
||||
因为领域事件是一条很好的主线,可以帮我们梳理出业务上的变化。同时,在如今这个分布式系统此起彼伏的时代,领域事件可以帮助我们让系统达成最终一致的状态。
|
||||
|
||||
那各种动作又是什么呢?那就是我们在写作中常用到的动词。在战术设计中,**领域服务(Domain Service)**就是动词。只不过,它操作的目标是领域对象,更准确地说,它操作的是聚合根。
|
||||
|
||||
动词,是我们在学习面向对象中最为缺少的一个环节,很多教材都会教你如何识别名词。在实际编码中,我们会大量地使用像 Handler、Service之类的名字,它们其实就是动词。
|
||||
|
||||
你可能会问,按照前面的说法,动作不应该在实体或值对象上吗?确实是这样的,能放到这些对象上的动作固然可以,但是,总会有一些动作不适合放在这些对象上面,比如,要在两个账户之间转账,这个操作牵扯到两个账户,肯定不能放到一个实体类中。这样的动作就可以放到领域服务中。
|
||||
|
||||
还有一类动作也比较特殊,就是创建对象的动作。显然,这个时候还没有对象,所以,这一类的动作也要放在领域服务上。这种动作对应的就是**工厂(Factory)**。这个工厂其实就是设计模式中常提到的工厂,有了设计模式的基础之后,你理解起来就容易多了。
|
||||
|
||||
需要注意的是,由于聚合的存在,聚合里的各种子对象都要从聚合根创建出来,以便保证二者之间的关联。比如,订单项的产生应该从订单上的订单项工厂方法创建出来。而聚合根本身的产生,就可以由领域服务来扮演工厂的角色。
|
||||
|
||||
对于这些领域对象,无论是创建,还是修改,我们都需要有一个地方把变更的结果保存下来,而承担这个职责的就是**仓库(Repository)**。你可以简单地把它理解成持久化操作(当然,在不同的项目中,具体的处理还是会有些差别的)。
|
||||
|
||||
其实,很多人熟悉的 CRUD,可以对应成一个一个的领域服务。如果我们用战术设计的做法来表示,应该是这样:
|
||||
|
||||
- 创建(Create),从工厂中创建出一个对象,然后,保存到仓库中;
|
||||
- 查询(Read),通过仓库进行查询;
|
||||
- 修改(Update),通过仓库找到要修改的对象,修改之后,存回到仓库中;
|
||||
- 删除(Delete),通过仓库找到要删除的对象,然后,在仓库中删除。
|
||||
|
||||
当然,这种简单的映射并不好,没有体现出业务含义,这里只是为了帮助你把已有知识和新知识之间架设起桥梁。
|
||||
|
||||
当我们把领域服务构建起来之后,核心的业务逻辑基本就成型了。但要做一个系统,肯定还会有一些杂七杂八的东西,比如,用户要修改一个订单,但首先要保证这个订单是他的。在 DDD中,承载这些内容的就是**应用服务**。
|
||||
|
||||
应用服务可以扮演协调者的角色,协调不同的领域对象、领域服务等完成客户端所要求的各种业务动作,所以,也有人把它称之为“工作流服务”。一般来说,一些与业务逻辑无关的内容都会放到应用服务中完成,比如,监控、身份认证等等。说到这里,我们已经说出了应用服务和领域服务之间的区别。
|
||||
|
||||
**应用服务和领域服务之间最大的区别就在于,领域服务包含业务逻辑,而应用服务不包含。**至于哪些东西算是业务逻辑,就要结合具体的项目来看了。
|
||||
|
||||
至此,我已经把战术设计这个故事模板给你讲了一遍,DDD 也算完整地讲了一遍了。你现在应该对DDD的各种基础概念之间是个什么关系、如果要做领域驱动设计,要有怎样一个步骤等有一个基本的认识了。
|
||||
|
||||
当然,仅凭三讲的篇幅,我们想要完整地理解领域驱动设计几乎是不可能的。但是你现在至少有了一个框架,当你再去学习 DDD 中那些让人眼花缭乱的知识时,你就不会轻易地迷失了。
|
||||
|
||||
Vaughn Vernon 写过两本关于 DDD 的书,是现在市面上比较好的 DDD 学习材料。建议你先阅读《领域驱动设计精粹》,这本书可以帮你快速入门;然后你再看《实现领域驱动设计》,这本书很厚,但讲得要更细致一些。当然,想要真正想学会 DDD,还是需要你在实际项目中进行练习。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们讲了DDD 中的战术设计,我们把战术设计当作了一个故事模板。让你先去识别角色,也就是找到**实体和值对象**。一个简单的区分就是,能通过唯一标识符识别出来的就是实体,只能通过字段组合才能识别出来的是值对象。
|
||||
|
||||
然后我们应该找到角色之间的关系,也就是**聚合**。操作聚合关键点在于找到**聚合根**。当聚合根不存在时,聚合中的对象也就不再有价值了。
|
||||
|
||||
有了角色及其关系,接下来就是找到各种动词,让故事生动起来。这里,我们讲到了动作,也就是**领域服务**,以及动作的结果,也就是**领域事件**,还有创建对象的**工厂**和保存对象的**仓库**。这些内容构成了我们最核心的业务逻辑。一些额外的工作,我们可以放到外围来做,这就是**应用服务**。
|
||||
|
||||
通过这几讲关于DDD的学习,你知道了如何识别出各种对象。通过前面设计原则、设计模式的讲解,你知道了如何组织这些对象。至此,我已经把设计相关的主要知识给你讲过一遍了,你现在应该知道如何做设计了。
|
||||
|
||||
那现在我们已经有了这样的基础,我们就可以做自己的设计了。从下一讲开始,我们就来体验一下,如何在真实的项目中做设计。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**战术设计,就是按照模板寻找相应的模型。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/5a/1f4e21934c11a3b984aafe2238745c5a.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你可以按照战术设计的模板,简要地描述一下你的项目吗?欢迎在留言区写下你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
Reference in New Issue
Block a user