近距离接触 DDD

DDD 中文名:领域驱动设计,做业务开发必不可少的指导方法论。

如果你苦于:

  1. 不知道如何从产品需求文档中提取关键业务信息;
  2. 不知道如何根据业务建立领域模型和数据模型;
  3. 明明知道 MVC 框架解决不了复杂业务,又不了解其他业务框架;
  4. 只听说过 DDD 高大上的方法论,却不知道如何落地。

那么你都可以来看看这场 Chat。

本场 Chat 结合大家实际开发中场景,以举例子的方式,对比 MVC 和 DDD 战略设计和战术设计的不同,让你对 DDD 有个整体的认识,最后给大家展示一种可以快速落地实践的业务框架。

DDD,中文名为领域驱动设计,为业务开发中必不可少的指导方法论,本文以业务开发中战略设计和战术设计为例,将普通开发模式和 DDD 模式进行对比,让大家对 DDD 有个整体的认识。

我们在工作中,可能常常听到过下面两种声音:

  • 产品说:这个做出来的东西,不是我想要的,体现不了业务价值。
  • 开发说:你要做的东西和目前系统设计冲突了,如果要做的话,工作量比较大,改动点很多!(感觉在乱提需求……)

产生这两种声音的原因有很多,有的人说是沟通问题,有的人说系统设计不够扩展,而我觉得这些问题都只是表象,不是问题的根源。

DDD 完全可以解决以上两个问题,在战略设计阶段可以解决业务价值的问题,在战术阶段可以解决系统和业务需求不一致的问题。接下来我们按照常见开发流程,把普通开发模式和 DDD 开发模式对比一下,看看两者有啥区别,大家也看看这两个问题具体是怎么被解决的。

业务开发的几个大阶段如下:

4adf1f10-8b03-11e9-9f77-038caa738dcc

接着我们按照几个大阶段分别来描述下 DDD 模式有什么不同。

需求评审阶段

我们先描述一下需求评审的场景:

产品经理把开发、测试等相关的人预约到一个会议室,产品把写好的需求文档拿出来讲,大家听,不懂的就问……

不知道其他的需求评审会是不是这样的,反正我经历的基本是这样。不是说这种不对,只是感觉这样似乎缺少一些重点?

  • 需求的来源?
  • 需求评审上如何提问?
  • 需求评审的结果是什么?

需求的来源

常规评审会的做法上,需求的来源都在需求文档上,需求文档写了什么需求,我就设计什么样的系统,需求文档中需求基本都是为了完成特定的场景,不是很全面完整,可能会导致系统设计的不够扩展。

抛开技术不谈,系统设计难以扩展的很大原因就在于需求的不完整。设想一个很大的需求场景,你拿到的只是部分,你设计出来的系统能够满足后来的扩展么?

DDD 强调的有:

  • 需求不要仅仅局限于需求文档,要多多联想生活,个人工作经验,多想想竞品的实现。
  • 不确定的细节多和领域专家讨论讨论,注意这里领域专家不一定是产品经理,领域专家指的是对业务的广度和深度理解比较强的人。

这两点非常重要,如果你真的实施下来了,最后的结果可能是你得到了 10 个场景,但需求文章只做其中的 2 个场景,但剩下的 8 个场景没有浪费,可以为你的系统设计做好扩展,因为你清楚,以后这里可能会改。

至于如何扩展,核心在于抽象,你可以把共用的事情抽象,可以把行为抽象,可以把流程抽象等。

需求评审上如何提问?

需求评审会的主要目的是为了弄懂业务和需求,但真正的弄懂业务和需求是什么,太难了,有可能你专注于这个业务几年后,才能真正知道该如何定义这个业务,所以我们该把这个目标缩小一下。

于是一般的评审会,大家就去听听流程,听听我们要做成什么样子。

DDD 更加强调业务的本质,最后达到能用一两句话直白地把业务描述出来的效果,为了达到这种效果,我们常常会有一些方法,比如:

  • 抓住动词,联想名词
  • WR 原则
抓住动词,联想名词

我们小时候学习语文的句子,经常会说到主谓宾,然后为了锻炼我们的语感和对句子的理解,记得那时候还有组句和完形填空的题目。同理,业务需求其实也是一个句子,为了理解这个句子,我们完全可以用小时候做题的方式,但为了方便,我们简化成:抓住动词,联想名词

我们举个常见场景:

小美在某宝 App 买东西。

这句话比较简单地描述了买东西的场景,我们用抓住动词联想名词来描述一下这个场景,可以简单描述成这样:

enter image description here

通过这种方式,我们可以把业务描述抽象成概念,并且把这些概念丰富。比如说买的东西,我们抽象成商品,并把商品丰富成实物商品、虚拟商品、海淘商品等。

WR 原则

九十年代的时候,有个外国人发表了 5W1H 分析法,后人把这种方法称为思考问题的基本方法。我们在 DDD 落地时尝试过这种方法,发现很用有。5W1H 分析法有一张图很清楚的,图片来自于百度百科:

enter image description here

这个图表达了对问题的一种思考套路。

图纵轴就是分析法中的 5W1H:What、Why、Where、When、Who、How,具体含义图解中已经说的很明白了。

图横轴,是对 5W1H 的每个阶段的深入解析,帮助我们对当前阶段有更深的理解。分别是四个问题:现状如何、为什么现状如此、能否改善、该怎么改善。

图纵轴和横轴乘一下,是 24 个方格,是对问题的思考结果,整个思考下来,我们对一件事情应该会有一定的认识了。

在分析业务时,5W1H 分析法会帮助我们理清业务的流程,但我们得不到一个结果,在业务开发过程中,结果往往是非常重要的。我们还是举买东西的例子:

买东西的例子我们抽象成了下单场景。在领域建模的时候,我们往往会用订单这个名词,来承接下单的所有因果,这个订单就是本次下单的结果,记录着下单的所有信息和后续履约过程。如此的例子有很多,比如下单之后,商家去发货,会有发货单,买家来退款,会有退款单等,我们建模的时候,都会用一个结果来承接所有的动作。

所以我们在 5W1H 的基础上加了一个名词,叫做结果,英文为 Result,总体简称 WR 原则。

5W1H 强调的是分析的过程,但我们领域建模使用更多的却是分析的结果。

所以在需求评审的时候,针对每个需求,你可以从抓住动词联想名词 + WR 原则出发,来进行提问,比如问:我们为什么需要做这个业务?价值在哪儿?

需求评审的结果是什么?

一般的需求评审会的结尾都是,产品经理开始问:

大家有木有不懂的,不懂的提问。大概几秒的沉默,产品经理接着说,那好,需求评审结束了,说白了,本次需求评审的产出,其实就是那么一篇需求文档。

我们实践 DDD 的时候略有不同,我们要求需要评审会结束后,最好有个最初版的通用语言出来。

我们对通用语言的定义:一定上下文内,对业务概念的一致通用表达,是理清业务是什么,能干什么,以及和其他业务边界的过程。

但在实际的工作中,需求评审会上产出初版通用语言太难了,你很难要求产品经理在需求文档中写出重要的通用语言,不太现实,那么只能开发自己来了,我们会要求懂 DDD 的同学在需求评审会结束时,发出来通用语言的 wiki 地址,要求主设计人员,在技术评审前,通过各种手段,完善好通用语言,并记录在文档中。最近我个人在落地一个项目,总结出来的通用语言,给大家展示下:

enter image description here

技术评审

在需求评审后和技术评审前这个时间范围内,主导人员主要是开发人员了,开发人员需要在技术文章中,产出三个重要部分:

  • 通用语言
  • 领域模型
  • 数据模型

通用语言我们上图也展示了一个,表达的形式没有标准,表格,图形都行,一般好的通用语言有如下几个特征:

  • 描述比较直白简单,基本都是一句话,最好让小白都能看懂。不会用一个很难理解的名词去解释另外一个名词,也不会很复杂啰嗦。
  • 重点通用语言会有英文描述。
  • 通用语言有顺序,会循序渐进。
领域模型

这里其实是 DDD 落地最复杂的地方,也是耗时比较久的地方,我们最好能在业务评审的时候,就要开始去关注领域模型。

很多人对领域模型不是很了解,不知道领域模型到底是啥,通俗的来说:领域模型就是用来定义领域元素,和管理领域元素的上下文的。

那么又会有人问,领域元素又那些,领域元素的上下文又是啥?

  • 我们常见的领域元素有:实体、值对象、聚合、领域服务、应用服务、领域能力等。
  • 领域元素之间的上下文指:元素间包含关系和逻辑关系。

本文主要让大家对 DDD 有个整体的认识,具体的就不细说了,我们有个详细的文章列表,感兴趣的同学可以研究研究:

http://wenhe.online/?p=2043

我们贴出来一张完整的领域模型图(不包含和外域的上下文),感兴趣的同学可以自己研究研究:

enter image description here

数据模型

数据模型,直白来说,就是在领域模型的基础上进行建表,我们需要表达出一种存储结构,这里特别推荐一本书,叫做《彩色 UML 建模》 ,这本书目前已经不印刷了,网上只有高价买原本的才有,这本书是我看过建模书籍中说的最好的一本了。

还有一些另外的建模小技巧,都是很常见的建模手段:

  • 表的二级结构,很多场景下表都可以设计成二级结构,如总账和明细,订单和商品等。
  • 不要害怕字段冗余,很多时候冗余是件好事,可以帮忙我们减少表的关联,增加查询的速度,有时候完全按照三范式建表可能会增加很多成本。
  • 大宽表,像 ES、Hive 这种非关系型数据库,我们常常会建大宽表,来方便搜索。

虽然说我们在实践 DDD 的时候,尽量不要让数据模型来影响了业务,但有些时候,还是要结合一下公司的规模和成本作一些让步,争取尽快落地。

当通用语言、领域模型、数据模型都准备好了之后,当然我们这里准备的只是初版,我们在技术评审会上,需要把团队成员都叫上,由开发主导,向各个成员展示我们的初版成果,大家可以一起讨论,有争议的地方,讨论出结果后,再修改掉。

技术评审算是全员参与的第二个会议了。

测试评审

测试评审会的主导者是测试工程师,开发和产品需要注意的有:

  • 测试工程师的表达是否符合通用语言,不符合请纠正。
  • 测试工程师的理解是否和你理解一直,不一致请讨论。

以上阶段完成,DDD 战略设计也基本完成了,其实和普通的设计流程差别一致的,但思想侧重点不一样,DDD 更加侧重于如何想清楚业务是什么、能干什么、边界在那里,战略设计的所有产出都是围绕着这三个问题展开的。

战术设计

战术设计直白来说,就是写代码了,我们在使用 DDD 模式来写代码时,我们对代码有着严格的约束,大的来说模块、package、类的分层约束、调用约束等,小的来说模块,package,领域元素都是有命名约束,通过这样约束,让代码展示出来,就像展示出业务一样,即代码即设计、即业务。

战术方面内容太多,没有几十篇说不完,我们就说一点:如何写出高内聚低耦合的代码

我们通过 MVC 和 DDD 两种架构,选取下面两个方面简单阐述下:

  • 分层
  • 领域层富血模型
分层

大家搭建项目的时候,都会通过模块来进行分层,大家讨论最多的是分三层好,还是分四层好呢?很少见有人去思考分层的好处所在。

分层只是达到系统高内聚低耦合的一种手段。

  • 高内聚:指的是对领域层的内聚。
  • 低耦合:指的是领域层对上下游的耦合少。

所以我们以下内容,并不是比较分三层还是四层还是五层的好坏,我们主要是来看看那种分层结构比较容易让业务达到高内聚低耦合。

我们来看下传统的 MVC 三层结构是否可以实现高内聚低耦合,画一张图描述下 MVC 三层结构:

f71d8650-8b07-11e9-ae6c-75b709235ff6

MVC 三层指的是 Controller 控制层、Service 业务逻辑层、 Model 数据层。

从图中的三层结构来看,其实是可以做到高内聚低耦合的,但很难。

接下来我们从两个角度来分析一下:

  • domain 层和上下游的关系
  • domain 层自己内部的关系
domain 和上层间的高内聚低耦合

Service 上游是 Controller 层,Service 层定义接口,Controller 层进行调用,这点是做到了依赖抽象。但在实践中,2 个很大细节被忽略,导致了违背了高内聚低耦合的原则。

细节一:DTO 流入了 Service 层

大家应该是知道,DTO 是对外的数据载体,是当前业务场景的输入和输出,即是应用服务的输入和输出。在实际对接的过程中,场景的输入和输出是很难制定标准的,很多时候需要去适配别人,所以我们希望 DTO 的身影只出现在 Controller 层,但实际工作中,DTO 也会经过 Controller 层流转到 Service 层,这就导致上游的业务已经侵入到本域了,违背了低耦合。

细节二:Controller 层写业务

Controller 层有个原则,我们只做流程编排、参数转化、事务等事情,绝不写业务,但实际的工作中,这个原则也经常被忽略,我们常常看见 Controller 层被大量的业务逻辑充斥着,这就导致了业务逻辑从 Service 层转移到了 Controller 层,导致业务逻辑分散在两个模块上,违背了业务内聚的原则。

那么这种情况在 DDD 中是如何改善的呢,我看过网上一些同学写的 DDD 代码,即使使用 DDD,只是把两层换了名字,换成了 app 层和 domain 层,但这种问题仍然没有解决,特别是在业务特别复杂的时候,问题还是存在,于是我一直在想,究竟是那里做的不对?

这个问题的根源在于 Controller 层和 Service 层的边界没有划分清楚(这里说的边界是技术边界,业务边界很多同学都划分的清楚,但写代码的时候,代码完全体现不出来业务边界),那么可能同学会说,哈哈,其实我也知道 Controller 层很薄,没有业务逻辑,业务逻辑都在 Service 层,但团队越来越大,业务越来越复杂,这个边界也随着划分不清楚了,慢慢的失控了?

是的,会失控的,但也有办法可控,我们在实践中通过以下两种办法可控:

  • 代码自动生成,生成的业务代码已经规定好了业务约束,开发人员不会乱写。
  • app 层和 domain 层的边界定义了约束,app 层只能够调用 domain 层的实体行为、聚合行为和领域服务三种领域元素,其他领域元素都不允许出现在 app 层。

自动生成业务代码保证了项目的整体技术框架,可以按照设定的走,不会随着团队规模的扩大而乱掉。

app 层只能调用 domain 层三种接口,也是通过代码自动生成来控制的。

通过这样子的技术约束来固定了代码架构,从而体现业务边界。

有的同学可能会问,这样子做,技术会不会去影响业务?肯定是不会的,我们是先设计领域模型,然后用对应的代码去实现领域模型,这样的技术只会让业务在代码中得到最大的体现。

只要你知道领域模型和代码是如何一一对应的,你会发现,看代码就像看业务文档一样。

DDD 能做到这一点,主要是因为 DDD 将领域层进行了细分,比如说领域对象有实体、聚合,动作和操作叫做领域服务,能力叫做领域能力等,而 MVC 架构并没有对业务元素进行细分,所有的业务都是 Service,从而导致 Controller 层和 Service 层很难定义出技术约束,因为都是 Service,你不会知道这个 Service 是用来描述对象的?还是来描述一个业务操作的?

DDD 将领域层进行了细分,是我个人觉得 DDD 比较 MVC 框架的最大亮点。

我认为能做到以上 2 点的 DDD 业务框架才会比 MVC 好些,否则的话,在 app 层和 domain 层之间,两者并没有差别!

domain 和下层间的高内聚低耦合

在 MVC 中,Service 层的下层是 Model 层,Model 层是数据层,我们可以简单理解成 Mysql、es、Redis 等等,通常由 Model 层定义一个接口,Service 层去调用,那么这时候问题来了,Model 层的改动会不会影响 Service 层?

会的,肯定会的,Model 层的改动肯定会影响 Service 层!但这还仅仅是技术上耦合的地方,并不是致命点,致命点是这种依赖会导致业务的耦合!

假设现在 Model 层由 A 来维护,Service 层由 B 来维护,那么 Model 层的接口将由 A 来定义,A 定义出来的接口,应该是按照 Model 层的标准来的,然后 Service 层会去调用 A 的接口,那么问题来了:A 的接口是 Service 层想要的么?符合 Service 层的业务发展需要么?

一个两个可能是,但如果是一百个呢?答案肯定是否定的,这时候 Service 层只能去适配 Model 层的接口,是不是很奇怪,核心业务居然要去适配底层数据的储存结构?这样做的系统,就是大家都说的数据模型驱动的业务系统,是以数据模型出发生产出来的业务,而不是以实际业务出发生产出来的业务,这句话有点点拗口。

为了解决这个问题,DDD 提出了非常棒的解决方案:依赖调用

MVC 是 Service 层依赖 Model 层,DDD 却完全相反,domain 层的下游都需要去依赖 domain 层!

直白地说,就是 domain 层如果需要什么,就自己去定义接口,然后下游去实现,因为接口是自己定义的,所以业务是内聚在 domain 层,然后 domain 层也不会去耦合下游的业务。

有的人会说,这不就是一种依赖抽象么?我理解绝不是这么的简单,我理解这是前辈在 MVC 的痛点基础上,想出来的通过一种技术手段来解决了模块之间的边界问题,是架构间的慢慢演化,发现目前架构的痛点,并通过演化,提出一种新的架构思想,这是很了不起的,绝不是什么依赖抽象这样的大话。

这里面其实体现了一种思路:我们是可以通过一些技术约束来划清出领域的业务边界的。

前面所说的 app 层和 domain 层的两种解决方案,我也是借鉴了这种思路,技术不能去影响业务,但能够去反哺业务的。

代码就不上了,都是很简单的实现,大家可以去星球自动生成代码看看。

领域层的富血模型

分层说的是领域层和其他域之间的关系,用国家来形容的话,说的就是外交,接着我们要来说下自制,领域层内部各个领域元素之间有什么关系,又该如何管理呢?

在此之前,说下 DDD 中三个概念,贫血模型、富血模型、充血模型,三个概念都是用来描述对象状态的。比如说这个实体很贫血,这个聚合充血,而我们的目标是富血,直白来说,贫血就是该做的事情没做,没有尽职;充血就是做的事情太多,把不该做的事情也做了,管的太多;富血就是做的事情刚刚好,我只做了我该做的事情。

为了达到富血模型,我们认为两点是特别重要的:

  • 领域元素的定义要清楚
  • 领域元素的边界要清楚

DDD 中的领域元素其实还满多的,有实体、聚合、领域能力、值对象、领域服务、应用服务、工厂、仓储等。

要想搞清楚,可以先看看 DDD 经典书籍:《领域驱动设计:软件核心复杂性应对之道》,其他理论派的 DDD 书籍个人都不推荐,最最关键的是需要实战,我基本上也是在实战了公司 2~3 个项目之后,才慢慢对这些领域元素有较深的理解了。

领域元素的边界要清楚

边界主要有两个思考的出发点,包含关系和逻辑关系。

其实如果你真的对领域元素的定义理解透了,其实领域元素的边界也出来了。

  • 比如说聚合是来管理实体间一种固定的业务关系的,聚合和实体是一对多的关系,聚合会包含多个实体。
  • 比如说值对象是用来描述对象的,那么值对象只可能用来描述实体,而不是聚合。
  • 比如说领域服务是一种操作或动作,你会发现领域服务在边界解耦,组装领域能力方面也有很大的作用。

领域元素的定义才是非常关键的,吃透了,领域元素的边界自然就清楚了。

篇幅有限,就到这里了,本文其实一点点细的东西都没有说,基本说的都是一种思路,让大家对 DDD 有个简单的认识,对文中有描述感兴趣的同学,可以加我微信:luanqiu0,或者关注知识星球:DMVP,知识星球手把手带你实操 DDD。

最后放一段无声视频,作为一个彩蛋(这段视频才是本文的重中之重),给大家展示下,我们目前快速落地 DDD 的一些工具(编辑如果把视频剪掉了,发文前请提前通知,感谢)。

视频 6 分钟,较大,请勿使用流量观看。

视频点击此处


本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

阅读全文: http://gitbook.cn/gitchat/activity/5cef33ceec085b26a117f2d3

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值