领域驱动设计系列 -01. 序章
前言
领域驱动设计(Domain-Driven Design,DDD)这一项“古老而神秘”的架构理论如今在互联网的催化下焕然新生,又重新走进了公众的视野。多年前,我一直在从事偏底层的基础架构相关工作,其重心主要集中在系统性能层面,对高层业务领域并不关心,自然也不会花太多心思在业务架构上,而如今又重新转行做起业务,all in 在兼备严谨性和高度业务复杂性的专业金融领域,如何设计出具备维护性、扩展性、伸缩性,以及易测性的业务系统就成为我当下的主要矛盾,因此我选择拥抱 DDD,并逐步挖掘其宝藏。
DDD 诞生的初衷与核心是为了解决业务问题,尤其是复杂业务问题,我们可以将它理解为复杂业务系统的解决与应对之道,这一点大家一定要有清晰的认知。之所以会选择开设系列专栏专门讲解和剖析 DDD,主要是处于 2 点原因,首先是目前市面上现售的书籍,哪怕是 Eric 的经典之作《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书,对从未接触过 DDD 的开发人员都显得不是那么友好,繁琐的知识点、复杂的概念,非常容易导致大部分 DDD 新手半途而废,或者理解出现偏差;其次国内社区很多人过度神话和重度渲染 DDD,只讲高大上的概念,不谈落地,这 2 种原因加起来直接导致 DDD 的实际使用成本变得非常高。因此我决定开个系列专栏,从战略设计和战术设计等 2 个层面来为大家分享如何基于 DDD 实现复杂的业务系统设计,相信大家在仔细阅读后,一定能够有所裨益。
为了让 DDD 新手快速尝到甜头并从中受益,本系列的开篇,我选择以战术层面的 Domain Primitive 着手,等大家逐步找到感觉后,我再深入讲解如何基于 DDD 去解决我们实际的业务痛点,以及如何通过 DDD 来设计复杂的业务系统,毕竟能够落地才是最重要的。
初识 Domain Primitive
之所以会选择以 Domain Primitive(下述简称 DP)为开篇,是因为 DP 在 DDD 中绝对是最为基础和强大的一项概念,当你对 DDD 逐渐熟悉和深入后,你会发现在代码层面 DP 是构成领域模型的核心之一。那么究竟什么是 DP?从本质上来说,DP 实际上就是一种特殊的 VO 对象,不过它并非是基于贫血模型的设计,而是充血模型。关于贫血模型,后续章节我会着重讨论,而现在你仅需知道,所谓的贫血模型就是一堆只定义有属性而没有行为(方法)的对象,比较典型的就是 Transaction Script 模式下的 DTO、Entity,以及 DO 等对象。
实际上很多文章或者书籍在进行举例的时候,都是以特定的业务领域进行展开,所带来的直接问题就是会给不熟悉这块业务领域的开发人员带来较高的阅读和理解成本。而本文为了能够让大家更好的理解 DP 本身的概念和特性,因此特意以较为简单的注册业务为例,示例 1:
上述程序示例非常简单,就是一个基于邀请码作为准入条件的注册逻辑,register()方法中主要由下述 3 部分构成:
参数校验逻辑;
创建邀请码逻辑;
新用户注册逻辑;
其代码结构是典型的 MCV 架构,由 Controller 负责任务编排,并把数据传输对象 DTO 交付给具体的 RegisterService 处理注册逻辑,最后由 RegisterService 负责调动持久层执行数据写入操作。代码整体看似一气呵成,但实则问题较多。因为这样的代码书写形式不具备任何的维护性、扩展性、复用性,以及可测试性。
我们先从业务视角去审视上述代码。register 逻辑中是否应该和校验逻辑强耦合?或者说是否应该有强依赖关系?答案是否定的,校验逻辑自然跟注册逻辑无关。再回到技术层面,如果所有使用到目标入参的地方,都需要使用相同的校验规则来执行数据校验,这明显是违反了 CRY 原则,并且随着入参的变化,我们还需要及时调整核心业务逻辑代码,这显然不合理。那我们是否可以单独封装一个校验工具类来简化这一操作?当然可以,但如果应用中所有的校验逻辑都集中在同一个类里,那这个工具类的维护成本仍然很高。那使用 JSR303 呢?当然也可以,只是复杂的校验逻辑,比如带语义规则的校验逻辑我们则需要自定义校验器来进行实现,如果应用中存在着大量的自定义校验器,这对我们的维护成本来讲同样也是一种极大的挑战。
再看邀请码逻辑。创建邀请码逻辑是否应该被包含在 regiter 代码块中?答案同样也是否定的,注册就是注册,不应该有其它任何逻辑来干扰它的可读性和维护性。那我们是否应该单独封装一个服务类来实现邀请码的创建(这里不讨论实际开发过程中复杂的邀请码创建过程)?回答这个问题之前,我们回顾下 DDD 的核心是什么?DDD 的核心是解决业务问题,那究竟怎么解决业务问题,首先就是要精准定位我们的核心业务逻辑,那上述业务的核心业务逻辑是什么?既然注册的准入条件是邀请码,那么邀请码就是核心业务逻辑,如果把核心业务逻辑分散到多个服务类中去实现和维护,同样也会导致维护成本的激增,不聚焦。既然我们时时刻刻喜欢把高内聚、低耦合挂在嘴边,那我们就应该冷静下来思考,究竟哪些逻辑需要进行拆分,哪些逻辑需要进行内聚才能带来更为合理的维护成本。
刚才提及过,DP 实际上就是一种特殊的 VO 对象,不仅包含属性,同样还包含有特定的行为,这里的特定行为,所指的就是相应逻辑合理的内聚性。基于 DP 的思想,我们把邀请码单独封装成一个 VO 对象,并在构造函数中执行校验逻辑,然后单独新增个计算方法来实现创建邀请码逻辑,经过这一系列的步骤,是否就能够准确的表达和突出邀请码这个概念,并同时降低 register()方法的复杂度和提升可读性?答案是肯定的,示例 2:
如果我们把所有的 register 逻辑中所有的参数校验涉都封装到对应的 DP 中完成,那我们的整个 register 逻辑就会回归原本的纯粹,清晰可读,示例 3:
在此大家需要注意,只有当属性存在特定的业务语义,或者存在某些限制的时候我们才需要将其封装为对应的 DP 对象,而不是所有的 Entity 属性都需要刻板的统一封装成 DP 对象,因为这么做是无意义的,除了会带来不必要的维护成本外,一点实际实惠没有。
初识 ACL 防腐层
上一小节我们已经对 DP 的基本概念有了一个大致的了解,那么接下来,我们再来继续看看之前的 register 逻辑还遗留有什么问题。传统的 MVC 是三层架构,一般情况下,Service 都会直接负责调用持久层 DAO,那所面临的问题就是,如果下游存储发生变更,比如:由于 Mysql 不满足于当前的业务现状,需要将其更换为其它的存储引擎时,那上游所有引用了 UserDao 的核心业务逻辑都需要面临代码的调整,这样的代码无疑是巨大的,如果真的发生在线上业务中,开发人员一定会面临着不敢改,改不动的局面。所以从严格意义上来讲,一个优秀的业务架构,不应该和任何依赖存在紧耦合关系(强依赖),或者说,依赖的变更,不应该直接影响其核心业务逻辑,这一点非常重要。
防腐层实际上不是什么高深的概念,通俗来讲,ACL 就是一个屏障,用于隔绝外部依赖变化对核心业务逻辑的影响,无论外部依赖产生任何变更,都不会直接影响到其核心逻辑。那么假设我们在 Service 层和 DAO 层之间增加一个防腐层 Repository,由 Repository 来负责实现对 DAO 层的调用,Repository 和 Entity 关联,DAO 和 DO 对象关联,那么 Repository 将会屏蔽掉下游所有的变更差异,也就是说,下游存储发生的任何变更,我们仅需在 Repository 进行相应的代码调整即可,从而避免牵一发而动全身的风险,示例 4:
关于 DP 和 ACL 的一些基础知识,我觉得大家暂时先了解这么多即可(虽然 DP 和 ACL 所涉及的内容远不止如此)。当大家能够理解本文的内容后,你就会发现 DDD 远没有你想象的那么复杂,至于后续,我打算切回到 DDD 的战略设计层面,用 2-3 章的内容,结合具体的业务场景去分析如何使用有界上下文的思想实现微服务的拆分。
至此,本文内容全部结束。如果在阅读过程中有任何疑问,欢迎在评论区留言参与讨论。
推荐文章:
版权声明: 本文为 InfoQ 作者【九叔】的原创文章。
原文链接:【http://xie.infoq.cn/article/5b1cfaaf6c110949ca919b9dc】。文章转载请联系作者。
评论