我理解的 Smart Domain 与 DDD
(本文首发于个人博客:https://brightliao.com/2022/07/27/smart-domain-and-ddd/)
前段时间,咱们 CTO 八叉在极客时间做了一次关于用 Smart Domain 实现 DDD 的分享(点击这里回看)。一个新词 Smart Domain 进入大家的视野。
Smart Domain 解析
Smart Domain 是啥?为什么可以用 Smart Domain 实现 DDD?本文尝试结合以往对 DDD 的学习和实践的经验,跟大家分享一下个人的理解。
八叉在分享中提到 Smart Domain 这个名字来源于 Smart UI。我们都知道 Smart UI 是 DDD 中提到的一种反模式,只能用于解决简单问题。这里的命名略带反讽戏谑的意味。
下面咱们结合示例看看 Smart Domain 究竟是什么。
打开 Smart Domain 的示例工程:https://github.com/Re-engineering-Domain-Driven-Design/Accounting。可以看到,项目在结构上分为了四个子模块:
main
: Web 应用入口,负责配置即启动应用api
: 定义 Restful APIdomain
: 核心领域层persistent
: 数据持久化
模块划分与依赖关系
深究起来,这四个模块和现在的分层架构有一些相似之处,但却并没有显示的严格的进行分层。同时,八叉在分享中明确提到了分层架构对领域建模是有伤害的,容易导致抽象不足。
值得注意的是,**domain
模块没有任何依赖**,其他模块则依赖spring
及相应的包。通过在domain
层定义抽象的接口(但不提供实现,由其他模块提供实现)的方式,将domain
层的核心逻辑隔离起来,使得domain
层可以非常容易根据领域需要进行灵活的设计及独立的测试。大家如果熟悉依赖倒置的设计原则,应该可以很容易领会这一做法的好处。(domain
层本应该依赖数据持久化进行数据的查询与保存,这里通过抽象的接口设计让持久化层反过来依赖domain
层的接口。)
关联对象
Smart Domain 的一个关键设计在于在模型之间引入了一个中间关联对象。关联对象由一系列接口来定义(见代码中的HasMany
HasOne
Many
接口),各类跨模型的操作均通过关联对象实现。这一设计避免了直接进行模型引用的诸多问题,比如引用的模型数量太多无法直接放入内存、引用的模型的查询修改通过Repository
实现进而引入抽象能力很弱的Service
去协调等。
示例项目中通过关联对象建模出的结果如下:
无关联对象的实现方式的本质问题在于希望完全用内存模型来抽象数据库访问,而内存模型事实上无法直接建模数据库的复杂性,因而引起了一系列连锁反应。通过引入关联对象,将数据库访问显示的建模出来,这些连锁反应就不复存在了,领域层也将更清晰、纯粹和丰满。
关联对象和传统 DDD 中的Repository
的抽象有一定的相似点,在我看来其最重要的区别在于关联对象接口定义在了引用方代码中。这一做法的隐含建议是从使用的角度来定义接口,从而使得接口定义不多不少,刚好满足系统需求。
其他
由于在 Smart Domain 的设计里面,对象图以统一的关联的形式被创建出来,所以可以提供一个统一的访问关联对象的方法。这一点很好的符合了 Restful API 的设计思想,API 模块利用这个特点,以很少的代码完成了 Restful API 的导出。
纵观 Smart Domain 的设计,可以发现结构上非常简洁,没有引入传统 DDD 实践中常用却很难用好的Repository
Service
Aggregate
等模式。然而这恰恰是让开发人员可以集中精力在对领域的挖掘和思考上。
总结
总结起来,Smart Domain 主要的创新及价值有以下几点:
打破了大家习惯用的分层架构,这些分层架构大都是从技术角度进行的抽象,而非领域角度
摒弃了职责模糊不清的 service 模式,使得原来 service 层的逻辑下沉到领域模型中,从而期望得到更丰满的领域模型
弱化了 DDD 中引入的聚合、限界上下文等容易引起争议的模式
尽可能的摒弃了在代码中使用过于技术化的术语,如 Controller、DTO 等,从而使得我们更专注于领域设计,通过引入更多的抽象来解决问题
将数据库查询显示的建模出来(通过关联对象),而不是直接在模型中引用关联模型的实例,有利于避免查询性能问题
为什么说 Smart Domain 实现了 DDD?
经过上面对 Smart Domain 的分析之后,可能有人会问:这跟 DDD 有啥关系?为什么 DDD 中的很多设计模式都没用却说实现了 DDD?
要回答这个问题,需要搞清楚什么是 DDD,即 DDD 的定义是什么。
什么是 DDD?
要追寻 DDD 的定义,可以回到这本早期的也是业界公认最权威的 Eric 的书《领域驱动设计-软件核心复杂性应对之道》中。
Eric 并未直接在书中对 DDD 进行定义,但是在第一章中,结合示例,总结了有效建模的几个要素:
模型和实现的绑定
获得了一种基于模型的语言
开发一个蕴含丰富知识的模型
持续进行模型提炼
头脑风暴进行创新,进行大量实验
在知识消化章节提到:
分析员和程序员将自己的知识输入到了模型中,因此模型的组织更严密,抽象也更为整洁
模型反映业务的深层次知识,模型真正是对业务原理的抽象反映
在统一语言章节提到:
如果不把“讲话”与各种沟通方式配合起来使用,那么将是巨大的浪费,因为人类本身就有 讲话的天赋。遗憾的是,当人们讲话时,一般并不使用领域模型的语言。
当人们谈话时,自然会发现词语解释和意义上的差别,而且自然而然会解决这些差别。他们会发现这种语言中的晦涩之处并消除它们,从而使语言变得顺畅。
使用模型的元素以及模型中各元素之间的交互来大声描述场景,并且按照模型允许的方式将各种概念结合到一起。找到更简单的表达方式来讲出你要讲的话,然后将这些新的思想应用到图和代码中。
从这些描述可以看出 DDD 的指导思想是:深入研究领域,消化知识,充分沟通,然后用软件模型对问题进行深刻的抽象,最终得到一个富含知识的领域模型。
Smart Domain 实现 DDD
从上面的分析来看,DDD 的实现不在于用何种方法,而是看最后是否得到了良好的深刻的领域模型。Smart Domain 可以促进我们把关注点从研究技术转向研究领域,从而推动开发人员去深入分析理解问题,创新的大胆的进行抽象和建模,最终得到好的领域模型。所以,可以说 Smart Domain 提供了一种很好的方式来实现 DDD,这显然是合理的。
Smart Domain 的扩展思考
Smart Domain 给我们提供了一个新的 DDD 实现思路。我们对它有没有什么疑问呢?
领域层抽象
习惯分层架构的同学,看到 Smart Domain 的思想,可能在直觉上会感觉事情不太对:以往可能有的项目上的代码超过 10w 行,但领域模型却只有 10 多个,难道要把这些代码都放入这 10 多个类?
事实上,这正好是抽象不足的表现,也正是 Smart Domain 或者 DDD 希望解决的问题。正是由于我们不能把太多代码放入同一个类(过大的类是一种明显的反模式)这个原因,才促使我们想办法通过引入更多的抽象来解决问题。
如何引入新的抽象?一个典型的示例是 DDD 原书中提到的关于策略模式抽象的例子。
在航运领域建模的过程中,在处理货物超订时,如果没有抽象,可以直接在代码中加入一些条件判断代码来实现。这样的处理方式的结果就是某个模型或类中的代码越来越多,直至难以维护。
而仔细思考领域之后,可以发现:
超订规则是一个策略。策略其实是一种设计模式,也就是我们所说的 STRATEGY 模式。
可以看到,用策略模式来抽象可以很好的解决这个问题。这就是深入思考的结果。
除了可以用策略模式进行抽象,DDD 书中提到的大多数常见模式都是可以使用的,比如Factory
、Repository
等。这些模式还包括设计模式中的组合模式、门面模式、解释器模式、观察者模式等等。
或许是大家在学习或者讲解 DDD 时过于关注了 DDD 引入的几个新的模式(如实体、值对象、聚合等),很多人都忽略了 DDD 中指出的要深入理解领域这个重点中的重点。
下面是 Eric 在书中语重心长的想要提醒大家的文字:
通过像 PCB 示例这样的模型获得的知识远远不只是“发现名词”,业务活动和规则如同所涉 及的实体一样,都是领域的中心......当我们的建模不再局限于寻找实体和值对象时我们才能充分吸取知识......领域专家往往不会意识到他们的思考过程有多么复杂,协作消化知识的过程使得规则得以澄清和充实,并消除规则矛盾以及删除无用规则。
弱化分层,强化领域划分
有人可能会问,上面例子中的策略模式相关的代码应该放在什么地方?当然是领域层!事实上 Smart Domain 的设计思想是根本不区分这些分层。这样一来,所有代码都是可以看作领域层代码(尽管有一些代码看作基础设施层可能更为合理),从而把尽量多的代码放在领域层,尽最大可能丰富领域层。
分层被弱化了,领域中的代码变多了,这正是 DDD 和 Smart Domain 所希望的结果。然而,随之而来的问题是如何管理这些代码。
很容易想到的答案是进行模块划分。事实上,DDD 中有单独提到“模块”这一模式。进行模块划分时,应参考高内聚低耦合的原则,使得模块内是高内聚,模块间是低耦合的。
常见的不符合高内聚低耦合模块划分原则的一个反例是按照技术名称进行模块划分。比如,可以回顾一下我们维护过的代码库,是不是还记得里面有一些模块的名称是controller
requests
responses
dtos
services
等等?当我们打开这些模块时,会发现里面的类其实没什么关系,而模块间的相互引用却非常多。
事实上,DDD 中讲到了更多的关于广义的模块划分的内容。从整个源代码库的角度来看,源代码可以按照从小到大不同粒度划分为函数、类、模块、领域、服务等级别。DDD 中讲到了如何在这些不同的层级进行划分。
在类级别,DDD 原书中提到了 Standalone Class 模式。Standalone Class 即独立的与其他类无关的类。这一模式其实是在说类级别的高内聚低耦合。
在类级别之上,DDD 中提到了 Aggregate 模式。Aggretate 即一组强相关的类形成的聚合。这一模式其实是在说一组类的高内聚低耦合。
在领域级别,DDD 提到了领域划分。可以按照领域的职责,将领域划分为核心域、通用域、支撑域等领域。这一模式是在说领域的高内聚低耦合。
事实上,从完整的代码库的角度来看,可以很容易建立以下认知:
代码块构成一个范围,函数体构成一个范围,类构成一个范围,类所在的包构成了一个范围,包所在的库构成了一个范围。将范围当做领域来理解,可以认为这些领域按照不同的细节程度和抽象程度构成了一个类似森林的结构。而森林的每一层都应当是高内聚低耦合的。
关于以上内容的更多描述,欢迎参考我的另一篇博客https://brightliao.com/2019/08/08/domain-concept-in-your-code/。
与传统的聚合相结合
在 Smart Domain 的示例代码中,基于内存的抽象也通过关联实现,略显麻烦。比如:
客户端需要调用好几个 api 才能把一个页面需要的数据拿到,不太方便
基于内存访问数据比基于关联对象访问更方便
传统的 DDD 实现通过聚合(聚合根直接引用其他实体)来解决此问题。如果可以确定聚合中关联的其他实体数量不多,则也许还是可以考虑通过聚合的方式来实现。此时,直接通过一个 Restful API 一次性返回此聚合中的所有模型即可。
对于聚合根之间的引用,则仍然可以采用 Smart Domain 中的关联对象实现方式。
如此一来,可以结合两者的优势。这可能是实践过程中值得考虑的选择。
不过,混合两种模式对架构师可能不太友好。与其引入更多的选择,他们可能更希望推进项目中的架构一致性。实际项目中,可能要结合团队成员的能力来综合考虑如何选择。
总结
本文分析了 Smart Domain 的设计,尝试回答了为什么 Smart Domain 可以用于实现 DDD。结合以往对 DDD 的学习和实践经验,分享了一些扩展的问题。希望对大家了解 DDD 和 Smart Domain 有一定帮助。
虽然 Smart Domain 作为一种设计范式,可以辅助我们实现 DDD。但是具体到真实项目中,建模这个过程还得结合实际的领域问题。有哪些值得参考的案例呢?下一篇文章将做一些分享。
版权声明: 本文为 InfoQ 作者【Bright】的原创文章。
原文链接:【http://xie.infoq.cn/article/78e6ced6fa69bee0eca2d9154】。
本文遵守【CC BY-NC】协议,转载请保留原文出处及本版权声明。
评论