背景 #
什么是DDD,DDD全名 Domain Driven Design,是一种架构设计方法,和我们普通的设计模式有什么区别呢,我们知道设计模式有单例、工厂这些,这些东西只和代码有关,他是一种手法,可以看作是一个小手段,就是类似孔己己的茴香豆7种写法一样
设计方法是什么呢,他是“道”,就好比儒家思想,他不是单纯的一种技巧,而是一种思想,好比马克思思想,他是告诉你咋格了软件的“命”
那么为啥我们需要这种思想呢,肯定是我们遇到了问题,我们才要吗,马克思也是在资本主义剥削下成长起来的,那就从我自身出发讲讲为啥需要这个“道”来格我们软件的“命”
问题起因 #
我在公司负责中台组件种的变现模块(就是负责收钱,发发会员),这个模块分工很好(表面上),将各大组件分成独立的子模块、支付模块、订单模块、会员模块、商品模块
可是随着流水般的产品来了又走,以及上一个维护开发的人走了又走,这份祖传代码留到我手里面已经是第五手,这份代码已经是被重构过一次了,从一个古老的spring 项目(还是XML为主导,连url 路由都是字符串判断)改造而来,其实一开始这个代码的架构师是比较牛逼的,一开始就准备采用了DDD的架构,但是后面维护的人不太懂DDD是啥,表面上看起来模块非常独立,但是他的模块太独立了,独立到退化成一个大型的Dao 文件
就只有纯数据库CRUD,一点点逻辑都没有,而且拖着DDD的皮,里面不折不扣就是一个大的Dao服务,关键数据还从要从Bo 输入,然后转成 DO 去查询数据库,再从数据库取出来 DO,在转换成 Vo,关键是 Bo 和 DO 和 Vo 数据结构一毛一样(每次新增一个字段,得改三个类,真人工牛马)
子服务非常的纯粹,啥都不依赖,啥逻辑也没有,唯一的作用就是外部传入一个命令,然后从数据库取数据或者更新数据,当然岁月静好,总有人在负重前行,在所有子服务上有个可怜老爹,我们叫他网关爸爸,默默的承受住了这一切,承担了所有的业务逻辑,随便拎出来一个类文件都几千行
记得我刚接受的时候,每次遨游在这代码的海洋,我总有一种,我在哪,上个函数是啥来着,咋跳到这里了,咋这里写了是这样处理的,咋返回结果不一样,我去,最下面一行这个if将结果给改了,诸如此类,每次看都是越看越惊心,每次一个小小的改动,有可能让这一坨代码轰然倒塌
其实我是有代码洁癖的人,一开始我负责的设备服务,我就自己做了小接口的重构,但是那个时候还不懂什么是“道”什么是“术”,脑子里全是《整洁代码》,《重构》这些技巧,只知道从小地方改改,让代码更加易懂优雅,如何在这一坨上面做重构呢,其实看着这一坨代码,我知道有很多技巧可以用,但是用了之后只会让这坨代码变成一小块一小块,而且后续维护起来反而更加麻烦了
那如何重构呢,假如只是用整洁代码里面的方法,将类结构重新调整,虽然代码结构变优雅了,但是本质没有变,逻辑还是头重脚轻,网关爸爸还是承受着一切,“术”没办法改变整体,要想格了这个命,还是得“道“出场
DDD核心 #
DDD的核心简单来讲就是: 打土豪 ,分田地。将整个大系统切割成独立的一小块,分给各个子系统
有两个核心点:
- 划分领域模型
- 确认领域边界
很简单,通俗来说就是你必须将要一块大的田地,切成一小块小块渠田,然后再每个梯田中碓泥土,加固,每一块梯田只会有一个农民,他负责这个庄稼地里面的收成
但是这种分隔方式也带来了一些问题,原来我们大家长制度,父亲管理一切,老大力气大,家里的重活都交给老大,老大干的又快又好,老二比较精明,处理账务是个好手,现在分家了,老大家的账务也得自己做了,但是自己是个直脑筋,自己算太麻烦了,出来一堆错误,老二家里的体力活也得自己干,但是自己干一天还不如老大一个小时
所以在这个新的制度下面,如何做到独立又分工明确呢,DDD用了一个概念,依赖倒置
什么是依赖倒置呢,举个简单的例子,我想用手机打电话通知李四吃饭这件事,首先我是主体,我依赖手机,来完成打电话通知李四这个操作,但是我不想完全依赖手机
怎么来说呢,就是其实我的目的只是通知李四,但是你现在强制让我依赖了手机,依赖倒置就是去除这个依赖,直达目的地,在我的领地中,我只需要声明一个接口,表达我的需求
例如:
public interface NotificationService {
boolean notify(String name);
}
接下来,在我的领域中,我只需要使用spring 的依赖注入,我说我需要
@Resource
NotificationService notificationService;
然后我直接调用 notificationService.notify("李四")
就好了,我不关心到底是使用手机去通知,还是使用邮件,还是直接去找他,这就是依赖倒置,简单来说就是甩手掌柜,你提需求,别人来完成
依赖倒置之后,我们其实把代码又分成了两部分,一部分就是 领域,一部分就是实现(DDD叫他基础设施层),其中领域和实现沟通的桥梁就是 领域 会有一个 传话筒(DDD中有的人叫它gateway网关),领域会在传话筒里面声明好 需求, 实现层就会去实现它
其实很多人会认为 DDD 这种才是贴合实际的,我们在真实业务中,总有两个角色,一个是产品,一个是开发,开发只知道使用程序实现,但是他不懂为啥要这样做,产品就是知道要这样做,但是不知道怎么使用程序实现,在DDD中,我们就把产品的所有需求都使用 领域 Domain 去实现,里面没有任何的底层依赖,就算是依赖也是靠 喊话实现, 然后底层里面也没有任何业务掺杂,他只是简单的把这个需求完成,不关心到底是谁在应用
工作经验不足程序员猛的一看这个DDD就会吐槽,这东西不就是绕了一圈吗,本来一个简单的通知,让你搞得这么复杂,依赖手机就依赖呗,要是换成短信我就把调用那边改成短信,但是朋友,当你真正接触真实业务你会发现,业务的变化是你想象不到的,虽然我们前期是做了一点点无用功,但是后面要是要改的话,你会发现这一点点无用功,真的非常好用
DDD的核心真的很简单,大道至简,打土豪、分田地(切分领域),建沟渠(依赖倒置),种良田(这个不是DDD中的,但是推荐你用设计模式去优化子模块代码架构)
但是你要用好也不容易,就比如我司,出发点是好的,但是走歪了,甚至下限比之前的MVC还要低,所以理解了概念不代表你就能用好了,但是你要是能理解透这个概念,对你实践DDD非常大的帮助
要想用好DDD,最核心的就是如何去划分领域,以及规划好领域的骨架,接下来我就分别从这两个方向介绍一下我的理解,见仁见智,这个领域划分和你的业务强相关,没有最好的方法,只有最适合的方法,下面方法仅供参考
领域划分的技巧 #
DDD 四色建模 #
核心就是通过四色建模,梳理业务,理清业务边界,网上介绍这个的非常多,我觉得他们讲的都挺好的,我就不补充了,这个只是技巧,只要能帮助到你就好了,不要在乎是谁说的
可以参考 四色建模 和风暴模型,推荐使用后者建模比较合理,从用户用例出发,逐渐搭建起模型
用户故事建模法建模方法 #
核心就是从用户用例出发建模
可以参考DDD常用建模
其中 事件风暴法和 限界笔纸法 我觉得都是从这两个方法中来的吧,技巧没有好坏
领域搭建骨架的技巧 #
实体类创建技巧 #
一个领域里面到底有什么呢, DDD 一开始的模型都是大谈特谈 实体+不可变值+聚合对象,让你去理解这些概念,让你严格按照上面这三种类型,将你用到的数据库表记录放到这上面去了
我觉得本末倒置了,不是数据库的DO 对象 转换成实体类了,而是说 业务需要 实体类, 业务需要实体类能够在任何时间查到,由于我们程序无法持久化这个实体,我们只能退而求其次,让这个实体能够被 转换成 DO 被持久到数据库、或者ES或者其他NoSQL数据库了,所以 实体是根, 持久化只是一个手段
所以你去搭建这个领域骨架的时候,最重要的一点就是从实际业务出发,使用我们java面对对象思想(继承、多态、组合),实体化一个类,去间接的表达这个业务的一部分骨架
这样讲很宽泛,我来举个简单的例子,我们以电商为例,我们创建一个订单领域,一个订单领域应该有什么呢,最核心就是订单吧,订单最重要的就是(用户id,金额,货币,订单号),我们就需要在领域里面创建一个实体叫 订单实体, 随着我们业务进行,我们接入了微信支付,接入了支付包,大部分程序员第一个印象就是,在订单领域添加一个字段 type,表明 是微信还是支付宝
为啥它会第一个时间想到是这样做呢,因为啊,每个程序员思考都是从程序出发,他觉得这个订单域就是存订单到数据库的,数据库又不知道哪个是支付宝还是微信,那我用一个字段表明不就好了,这个思考方式的确对,但是我们退出程序员视角,我不考虑数据库咋存的,我就在考虑,一个支付宝订单和一个微信订单是同一个实体吗,肯定不是,但是他们之间有点关联,他们都是订单,他们继承于订单这个object
所以当业务需要支付宝订单的时候,我们不是说在改造订单这个实体,而是在订单实体上继承出一个子类,支付宝订单,这样一个支付宝订单不会去影响微信订单,使用这种方法可以满足很多设计模式的概念(里式替换、单一性原则等)
我们去做这个设计的时候千万不要抱着我一定要满足某种设计模式的方法去设计,而是去思考我这种设计是不是最能表达一个领域的部分,不要去思考我新建一个类之后就会增加多少成本、存储会多麻烦,而去思考这种设计是不是最能贴近类,我们永远无法完美表达一个业务的实体,只能说尽可能去贴近
当然我这里不是排斥说要废弃掉 实体+聚合+不可变这种分类方法,而是说我们要吸取这个分类方法的科学性,这种分类其实是定于了一种约定,我们通过约定可以更加轻松就知道这个类的类型是什么,提高我们程序易读性,所以我这边也不谈怎么去定义一个实体,这个定义实体要具体问题具体分析,我给你的建议就是说,千万不要带入数据库的设计理念去设计一个实体,就像我上面说的一样,类型就是加字段,在数据库存储的时候你可以使用这个加字段的方法,但是在设计这个实体的时候千万不要让类模仿你的数据库实现,要让实体类去模仿你的业务,让你的领域变得骨架更加清晰明朗,所以我推荐你充分使用java 中面对对象的继承、封装、组合、枚举等,让这个类更加饱满
充血模型和贫血模型技巧 #
DDD还有一个经常说的就是充血模型和贫血模型,核心就是实体类模型的丰满度,自己带了自己的类方法有自己的逻辑就是 充血模型, 没有逻辑纯字段实体类就是贫血模型
其实在我上面讲完怎么创建实体类你就知道我的看法,我认为要均衡易用性和变更复杂性,假如实体类的方法实现经常会变动,那千万不要在模型里面写死,否则后续你发新版本所有的基础层调用方都得改,朝令夕改可不是好玩的东西,千万不要学懂王
但是一些核心不变的逻辑,你可以放到里面,比如在订单类提供一个方法,来判断这笔订单是否结束,假如你不在实体类,提供的话,是不是对接的第三方都得使用一个if else 来判断你这个实体类哪个字段可以标志已经结束,这个逻辑99%的概念是不会改了,你可以提供出去,避免别人又写个if else,让你的类更加丰满
像之前阿里他们分享的一些做法就是他们用一个Share-Kernel 把不变的东西分享出去,其实也是这个意思,只不过他们用了一个高大上的单词 Share-Kernel , 就是就是一个jar包依赖
上面就是我自己对DDD的一个理解,但是这个理解,大部份可能会忘掉,最好是能有一个框架自动去约束,接下来我就提一下,如何搭建一个 框架去辅助践行,这个不是强求,只是说定一个约束,在架构层次上避免你将DDD写歪
架构辅助技巧 #
这个框架设计非常简单,主要通过Java子模块分包和强制依赖管理来实现领域独立性。下面我将介绍核心的分包思想,而不提供具体框架代码实现。
分包结构 #
将子服务划分为三个主要模块:
- api模块:包含模型定义和网关接口,完全独立
- domain模块:领域实现,仅依赖api模块
- service模块:包含前两个模块及第三方RPC包(如其他服务的Dubbo API)
依赖关系如下:
api(model + gateway) > domain (Dubbo domain service) > service (rest service + infrastructure)
架构优势 #
- 领域隔离:domain模块强制只能访问api模块的内容,确保领域独立性
- 简化依赖:其他服务只需引入api模块即可使用该领域功能
- 灵活部署:domain模块通过Dubbo服务对外暴露RPC接口
架构演进 #
初始设计存在一些不足:
- 系统内部服务间调用频繁变动
- 这些内部变动不应对外暴露
改进方案:
新增domain-api模块专供内部子服务使用,对外仍只暴露api模块。这样做的好处:
- 消除内部服务间不必要的BO/VO/DO转换
- 减少重复代码
- 保留对外封装性
模块职责说明 #
模块 | 包含内容 | 依赖关系 | 对外暴露 |
---|---|---|---|
api | 领域模型、网关接口(假如使用domain-api的话,这个部分只保留对外暴露的模型) | 无 | 是 |
domain | 领域服务实现 | 仅依赖api | Dubbo服务 |
service | REST服务、基础设施 | 依赖api和domain(可选 domain-api 依赖) | REST接口(或者Dubbo) |
domain-api(可选) | 内部专用接口(领域模型+gateway接口) | 无 | 仅内部 |
总结 #
DDD 不是一种编程技巧,而是一种思想,在这种思想下,我理解了,产品和我的思考差异,也知道了架构的好处,以前看了很多书,整洁代码,代码大全,重构,但是这些书只是在定义一种写代码的规范,让你的代码看起来不那么糟,但是假如你的架构就是不好,在一波波迭代需求下还是会逐渐散发味道
当然你是个小项目,我还是推荐你别用,MVC一把嗦,当有超过3个产品在一个需求上继续迭代,我奉劝你用DDD重新组织一下,可能你花费了几天,但是能为你未来省掉几个月的开发时间
资料: