我们回顾了经典三层架构与领域驱动设计四层架构,然后又对分层架构模式的产生与设计原则做了一次历史回顾。我们先后参考了 Robert Martin 的整洁架构、Cockburn 的六边形架构以及 Toby Clemson 给出的微服务架构模型。现在,是时候为领域驱动设计的架构模型做一次总结陈词了。然而事情并未结束,因为任何技术结论都并非句点,而仅仅代表了满足当时技术背景的一种判断,技术总是在演进,领域驱动架构亦是如此。与其关心结果,不如将眼睛投往这个演进的过程,或许风景会更加动人。
根据“依赖倒置原则”与 Robert Martin 提出的“整洁架构”思想,我们推翻了 Eric Evans 在《领域驱动设计》书中提出的分层架构。Vaughn Vernon 在《实现领域驱动设计》一书中给出了改良版的分层架构,他将基础设施层奇怪地放在了整个架构的最上面:
整个架构模型清晰地表达了领域层别无依赖的特质,但整个架构却容易给人以一种错乱感。单以这个分层模型来看,虽则没有让高层依赖低层,却又反过来让低层依赖了高层,这仍然是不合理的。当然你可以说此时的基础设施层已经变成了高层,然而从之前分析的南向网关与北向网关来说,基础设施层存在被“肢解”的可能。坦白讲,这个架构模型仍然没有解决人们对分层架构的认知错误,例如它并没有很好地表达依赖倒置原则与依赖注入。还需要注意的是,这个架构模型将基础设施层放在了整个分层架构的最顶端,导致它依赖了用户展现层,这似乎并不能自圆其说。我们需要重新梳理领域驱动架构,展示它的演进过程。
该怎么演进领域驱动架构?可以从两个方向着手:
- 避免领域模型出现贫血模型
- 保证领域模型的纯粹性
避免贫血的领域模型
我们需要回顾经典的 Java 三层架构对领域模型的设计。在这个三层架构中,领域逻辑被定义在业务逻辑层的 Service 对象中,至于反映了领域概念的领域对象则被定义为 Java Bean,这些 Java Bean 并没有包含任何领域逻辑,因此被放在了数据访问层。注意,这是经典三层架构的关键,即代表领域概念的 Java Bean 被放在了数据访问层,而非业务逻辑层。经典三层架构采用了 J2EE 开发的 DAO 模式,即将访问数据库的逻辑封装到数据访问对象(Data Access Object)中。这些 DAO 对象仅负责与数据库的交互,并实现领域对象到数据表的 CRUD(增删改查)操作,因而也被放到了数据访问层中,如下图所示:
如果以面向对象设计范式进行领域建模,我们需要遵循面向对象的设计原则,其中最重要的设计原则就是“数据与行为应该封装在一起”,这也是 GRASP 模式中“信息专家模式”的体现。前面提及的 Java Bean 由于仅包含了访问私有字段的 get 和 set 方法,可以说是对面向对象设计原则的“背叛”,Martin Fowler 则将这种没有任何业务行为的对象称之为“贫血对象”。基于这样的贫血对象进行领域建模,得到的模型则被称之为“贫血模型”。这种贫血模型被认为是简单的,却不具备对象的丰富表达能力,当业务逻辑变得复杂时,在表达领域模型方面就会变得“力不从心”,无法有效应对重用与变化,且可能导致臃肿的“上帝类”。贫血模型的种种问题会在战术设计中再做深入探讨,这里我们姑且给出一个结论,即:在面向对象设计背景下,当我们面对相对复杂的业务逻辑时,应避免设计出贫血模型。
要避免贫血模型,就需要合理地将操作数据的行为分配给这些领域模型对象(Domain Model),即战术设计中的 Entity 与 Value Object,而不是前面提及的 Service 对象。由于领域模型对象包含了领域逻辑,就需要从数据访问层转移到业务逻辑层。至于那些不属于任何领域模型对象的领域逻辑,仍然放到 Service 对象中。由于 DAOs 对象需要操作这些领域模型对象,使得处于数据访问层的 DAOs 对象必须依赖领域层的领域模型对象,也就是说,要避免贫血的领域模型,就不可能避免底层的数据访问层对业务逻辑层的依赖。
从分层的职责和意义讲,一个系统的基础不仅仅限于对数据库的访问,还包括访问诸如网络、文件、消息队列或者其他硬件设施,因此 Eric Evans 将其更名为“基础设施层”是非常合理的。至于将业务逻辑层更名为领域层也是题中应有之义。遵循整洁架构思想,基础设施层属于架构的外层,它依赖于处于内部的领域层亦是正确的做法。在领域层,封装了领域逻辑的 Services 对象则可能需要持久化领域对象,甚至可能依赖基础设施层的其他组件。于是,之前的分层架构就演进为:
保证领域模型的纯粹性
若将整个层次看做一个整体,在刚才给出的分层架构图中,加粗的两条依赖线可以清晰地看到领域层与基础设施层之间产生了“双向依赖”。在实际开发中,若这两层又被定义为两个模块,双向依赖就成为了设计坏味,它导致了两个层次的紧耦合。此时,领域模型变得不再纯粹,根由则是高层直接依赖了低层,而不是因为低层依赖了高层。故而我们需要去掉右侧 Services 指向 DAOs 的依赖。
DAOs 负责访问数据库,其实现逻辑是容易变化的。基于“稳定依赖原则”,我们需要让领域层建立在一个更加稳定的基础上。抽象总是比具体更稳定,因此,改进设计的方式是对 DAOs 进行抽象,然后利用依赖注入对数据访问的实现逻辑进行注入,如下图所示:
DAOs 的抽象到底该放在哪里?莫非需要为基础设施层建立一个单独的抽象层吗?这牵涉到我们对数据库访问的认知。任何一个软件系统的领域对象都存在其生命周期,代表领域逻辑的业务方法其实就是在创造它,发现它,更新它的状态,最后通常也会销毁它。倘若部署软件系统的计算机足够强劲与稳定,就不再需要任何外部资源了;这时,对领域对象的生命周期管理就变成了对普通对象的内存管理。因此,从业务角度看,管理对象的生命周期是必须的,访问外部资源却并非必须。只是因为计算机资源不足以满足这种稳定性,才不得已引入外部资源罢了。也就是说,访问这些领域对象属于业务要素,而如何访问这些领域对象(如通过外部资源),则属于具体实现的技术要素。
从编码角度看,领域对象实例的容身之处不过就是一种数据结构而已,区别仅在于存储的位置。领域驱动设计将管理这些对象的数据结构抽象为资源库(Repository)。通过这个抽象的资源库访问领域对象,自然就应该看作是一种领域行为。倘若资源库的实现为数据库,并通过数据库持久化的机制来实现领域对象的生命周期管理,则这个持久化行为就是技术因素。
结合前面对整洁架构的探讨,抽象的资源库接口代表了领域行为,应该放在领域层;实现资源库接口的数据库持久化,需要调用诸如 MyBatis 这样的第三方框架,属于技术实现,应该放在基础设施层。于是,分层架构就演进为:
由于抽象的 Repositories 被搬迁至领域层,图中的领域层就不再依赖任何其他层次的组件或类,成为一个纯粹的领域模型。我们的演进正逐步迈向整洁架构!
用户展现层的变迁
现代软件系统变得日趋复杂,对于一个偏向业务领域的分层架构,领域层的调用者决不仅限于用户展现层的 UI 组件,比如说可以是第三方服务发起对领域逻辑的调用。即使是用户展现层,也可能需要不同的用户交互方式与呈现界面,例如 Web、Windows 或者多种多样的移动客户端。因此在分层架构中,无法再用“用户展现层”来涵盖整个业务系统的客户端概念。通常,我们需要采用前后端分离的架构思想,将用户展现层彻底分离出去,形成一个完全松耦合的前端层。
不管前端的展现方式如何,它的设计思想是面向调用者,而非面向领域。因此,我们在讨论领域驱动设计时,通常不会将前端设计纳入到领域驱动设计的范围。有人尝试将领域驱动设计引入到前端设计中,那是将前端自身当做一种领域。在设计后端 API 时,我们确乎需要从调用者的角度考虑 API 的定义,并确定从 Domain Model(或者 Service Model,又或者是 Resource Model)到 View Model 的转换,又或者考虑引入所谓“DTO(Data Transfer Object,数据传输对象)”,但这些都只限于后端 API 协议的设计。
准确地讲,前端可以视为是与基础设施层组件进行交互的外部资源,如前面整洁架构中的 Web 组件与 UI 组件。为了简化前端与后端的通信集成,我们通常会为系统引入一个开放主机服务(OHS),为前端提供统一而标准的服务接口。该接口实际上就是之前整洁架构中提及的 Controllers 组件,也即我提出的基础设施层的北向网关。于是,分层架构就演变为: