1. 理解DDD
ddd全称:Domain-driven design
领域模型、驱动、设计。
它的思想是公司根据现实商业行为来划分问题域和边界。有以下好处:
- 在代码设计上,让代码腐化更慢。
- 因为有边界,所以减少了腐化传染性和依赖性。
- 让代码设计上更符合现实场景,和现实的商业行为联系更紧密。不同职责不同岗位的人交流沟通更统一(统一语言)。
The software design should be driven by the problem domain
软件设计应该要被问题域驱动
这是在B站看的一篇关于讲DDD的视频,这句话确实让我想通了很多。
下面来具体说说我理解的DDD。
2. 解决问题的代码
写代码是为了解决需求,而需求是因为要解决某个问题而诞生。但是已有前人碰到过此类的问题,并且归纳总结出了经验。最佳的方式是直接参考这类问题的经验来帮助我们设计代码,来解决这类问题以及未来可能预料到的问题(也就是所谓的扩展性)。
以某个公司为例,它的主营盈利业务是:通过C端用户交易下单,来抽取B端商家一定的佣金来盈利。它招了两个初级程序员和一个有过交易下单相关经验的高级产品来开发这个系统。
在这个场景里它就变成:我们现在有交易下单抽佣的一些领域问题需要解决,但是开发人员之前都没做过交易下单抽佣这块业务,所以他们对这块领域并没有经验,他们并不是这块的领域专家,而产品有相关的经验,所以他在这个公司属于这些领域的领域专家,但是产品并不会设计和编写代码,因此需要他们根据公司的场景,定下需求,并在讨论中使用事件风暴法来和技术一起输出技术方案,梳理出这些领域中涉及的领域模型、领域事件,来驱动程序员的代码设计。
领域专家和职位无关。就像在养育你的这个领域问题上,你妈是专家。
3. 领域问题
目的是要解决领域问题,那肯定是需要知道有哪些领域问题。可惜这是个我也没完全懂的话题。
至少每个公司都不一样,因为它是随着公司商业行为碰到的问题而引发的。为什么DDD在某个时间被过度神话,因为小公司认为,大公司已经碰到过问题,并且有了好的解决方案,那我们就不用考虑那么多,直接就用大公司已经碰到的问题作为我们的领域问题就行了。
举例:我们要做个电商App,直接挖淘宝这块的领域专家不就好了?这样确实从逻辑上没问题。但是成本会高很多,所以一般都是1+N,也就是一位相关经验丰富的专家带领一群年轻选手来做这块领域问题。
为了更好的解决业务问题,所以我们采用DDD。
而不是只有DDD才能解决业务问题。
谁来定领域问题,其实是公司。对电商来说,一开始为了解决B端、C端的问题,我们划分为
B端领域、C端领域。后面公司做大了,细化了,可能有些B端C端重合了,公司做了组织架构调整,变成交易、库存、商品三个组,分别来重点解决三个领域问题。最后又
可能因为公司的发展,国际交易量大了,支付方式需要支持各种各样的国内外支付渠道,那后面可能从交易剥离出了支付,又多了一个组,一个新的支付领域。
领域问题会随着公司的发展而变化,但是不变的是:领域知识。懂领域知识的一般都是在这个领域深耕多年的,可能是测试、产品,商务、或者开发人员。职业不重要,在于他在这个领域的经验。
当然,大公司或者垂直公司的人,这类的比例会比较高。因为他们的业务量高,问题变多,他们为了解决这类问题而有了比你更多经验。
不是996
4. 领域问题产出了领域模型与领域事件
秦统一了度量衡,方便经济交流。而建模也是,让各类人员对领域问题有相同语言去交流,所以有了统一建模语言UML。
常用工具,例如 PlantUML 工具。
工具只是工具,如果你的业务够简单直接手画画图拍照也行。只是当业务量大、涉及业务线够长、上下链路复杂,你需要跟他们对相同的事情有统一的表达称呼。
不同的公司规模,有不同的规模问题,由不同的模型解决,但是解决方案的基本架构都差不多,也就是六边形架构。
假如现在交易领域需要建模,你需要划分出相关的读写接口定义,出入参模型定义,实际业务接口,从上到下,你就自然而然的有了数据库的模型,以及各个业务接口方法它需要发出的各类交易相关的事件模型定义。
领域事件比较好理解:领域内有意义且时间有序的事件。
事件并不代表一定要异步,它的实现可以用同步或者各类消息,具体实现由各类扩展点来做。
4.1 六边形架构
核心领域模块被非领域模块包围。而非领域模块作为适配层来与其它域交互。
六边形架构又称为端口-适配器,这个名字更容易理解。六边形架构将系统分为内部(内部六边形)和外部,内部(浅褐色)代表了应用的业务逻辑,外部(粉红色)代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。一个端口可能对应多个外部系统,不同的外部系统需要使用不同的适配器,适配器负责对协议进行转换。这样就使得应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动,并且,可以在与实际运行的设备和数据库相隔离的情况下开发和测试。
例如现在我是商品的核心领域,有个领域能力叫获取商品基本信息的方法。
它需要查询自己商品的db,和库存域请求获取这个商品的实时库存。
那在商品核心领域模块不应该直接依赖库存的RPC请求接口,而是依赖自己的一个库存获取接口,由自己的非领域模块来实现这个接口,也就是所谓的适配器扩展点。
这样的好处在于:库存的获取如果换了另一种方式,从库存域获取,换成从外部三方系统接口获取库存信息,那这样,核心领域模块不用动,只需要改非领域模块的:增加一个三方系统的库存扩展点实现。
但是它的难点在于:不要过度抽象出扩展点,也不能不抽。
不能过度设计,也不能不设计。
4.2 CQRS
Command and Query Responsibility Segregation:命令和查询职责分离
命令:有副作用,类似CUD,会导致系统发生变更
查询:没副作用,类似R,不会导致系统发生变更
其实就是我们说的业务上的读写分离。为什么要 CQRS。其实你也可以不剥离,像简单的场景,教学场景的用户注册和用户查看,都是直接走DB即可,不需要分离。
但是分离的好处很明显:这样查询模块和命令模块由于分离,所以他们的底层可以不同,各自实现。可以做到用户注册走的MYSQL命令写入,同时发布用户注册事件,监听写入Redis。而读取用户列表可以走Redis或ES或HBase
CQRS 模式和 DDD 没有关系。我可以单独用 CQRS,或者只用 DDD 的思想
4.3 标准架构
我们直接看下标准的架构,涉及到事务和事件
基于刚刚的六边形架构思想,我的存储、消息,都可以作为接口,由不同的适配器来实现。
使用的是MQ的事务消息
4.4 充血模型
一个模型只有属性的get/set操作是不符合面向对象的逻辑的,应该还有行为。所以DDD其实是充血模型。
之前我也很纠结,但是看了一些文章和自己的实际开发经验,充血模型才是真正的面对对象。
4.5 聚合上下限
聚合:一组功能,我简单理解为一个类/一个包,它来解决这一个域的一块功能问题。就像商品分类,它属于商品域下面的分类功能,
因为在DDD面对对象编程,所以每个对象都是充血的,不仅有变量,还有对应的行为方法。但是一个类,有多少个行为方法,这个类的变量的数量是多少才合适,
它是有个上下限的:上限是这个类承担的功能没有过多,且没有明显的性能问题,例如用户下单和支付接口,在公司初期你可以合并在一个类,但是后面公司发展业务变
庞大,这个类的职责模糊,或者随着组织架构去拆。而下限,也就是一个类一个作用,或者明明可以一起返回的一系列变量,但是被拆了。
这个根据实际开发来判定,至少不要说用户修改手机号和获取手机号信息放两个不同的聚合里面,或者一个聚合里有成千上万的代码量或方法
4.6 事件风暴
事件: 即事实,即在业务领域中那些已经发生的事件就是事实。
风暴: 运用头脑风暴会议进行领域分析建模。
其实就是讨论需求和评审技术方案。但是会更加的严格和标准,需要有明确产出:例如领域事件、领域模型。这个跟具体公司要求不一样
其实就是讨论需求。但是里面分了具体的步骤和方法论一步一步的来。
5. 如果没有DDD
如果你只从目前的问题出发,而不从问题域出发。直接写技术方案,看起来很好,但是随着需求增加,慢慢就变成一坨屎山。
一定要落地!PPT再漂亮也是假的,但是落地才是最重要的。
可以没有DDD,毕竟能解决问题的方法才是好方法。
从个人提升的角度:
- 你会思考,你做的这一坨代码,解决了什么实际问题,带来的怎样的商业价值
- 我在解决这个问题时,如果已经有成熟的方案,拿来改改是不是比我自己从零开始更好?
- 代码的扩展和可维护性更好(从职业操守来说)
从公司的角度:
- 商业行为和代码设计一体,需求的划分更明确
- 代码的可维护、可扩展会更好
- 我的代码设计可以用在PPT分享上
6. 代码终究会腐化的
代码会随着人员变动以及公司商业目标、架构变化而变化,我们用DDD来设计好代码,只是用来减少腐化的速度,和未来方便重构。
本文只是个人实际工作总结,为了个人理解,中间某些定义没有很标准。具体还是需要从实际出发。
后面会用个简化的实际场景来做DDD的开发示例,帮助自己更好理解。