分离关注点构建领域核心——领域驱动设计中的分层架构

2,301 阅读8分钟

1.领域驱动设计中的架构设计

不论我们是否讨论架构设计,软件实现都要遵循某种架构设计的,不论是好的坏的,设计不足还是过度设计,也就是说工程师们都会按照某种方式组织代码。领域驱动设计也是如此,为了降低失败风险,引入了一些架构。

Evans著书时是分层架构横行的年代,但是实践者们发现抛开分层架构DDD依然对分离关注点构建领域核心有很好的适应性。我们还是从最小闭环出发,使用经典的分层架构来看DDD的架构设计。

DDD的先驱们在设计DDD时是允许存在多种架构的,随着时间推移,分层架构(Layers Architecture)脱颖而出,依赖倒置原则(Dependency Inversion Principle,DIP)改进了分层架构,六边形架构(Hexagonal Architecture)成为首选,下面就来聊聊这几个架构风格与架构模式。

1.1 分离关注点

创建一个不依赖于用户界面和数据库即可运行的应用(application),这样,就可以对其进行自动化回归测试;在没有数据库时也可进行开发;同时,无需用户参与就可以将多个应用连接起来。

Evans对架构设计的出发点突出了领域层的核心地位,意在剥离领域模型无关的一切繁枝末节,包括集成成本高昂的用户界面与数据存储,保证领域模型可以独立开发测试,成为快速迭代的业务单元。如何剥离就是分离关注点,是架构设计重点考量的地方。

这些隐患是可以通过分离关注点来提前规避的,比如应该内聚的逻辑分散在各处,而要解耦的又混杂在一起,服务难以划分与理解;技术代码与业务代码交错叠加,互相影响,更新业务、更换技术选型就会带着历史负担;重复大量出现,需要统一修改的却要多处修改,导致代码可读性越来越糟。

在接下来的架构设计与代码实现我们都会予以讨论,充分理解分离关注点的妙处,完成最小闭环。

1.2 分层架构

分层架构被认为是所有架构的鼻祖,并被广泛应用于Web、桌面应用以及企业级应用,比如广为人知的Http网络协议。一个程序或系统被分成不同的层次,每层保持内聚,并且只能依赖比其自身更低的层,也就是依赖方向是确定的。

具体地说,分层架构中每层只能与位于其下方的层发生耦合,我们常说的分层架构是松散分层架构(Relaxed Layers Architecture),允许跨层耦合,只要保证依赖方向是从高到低的即可。

Evans也重点关注了分层架构,他对领域驱动设计中的分层架构的设计思路是将领域模型与业务逻辑分离开,减少对基础设施、用户界面以及应用逻辑这些不属于业务逻辑的依赖。 也就是分离关注点,将领域作为一等公民列为架构的核心,软件实现的不稳定部分依赖稳定部分。这在我们前面领域建模的过程中已经有所体现,并没有讨论任何用户交互、存储计算等内容依然可以顺利建模。

DDD采用传统分层架构的样子:

  • 用户接口层(User Interface)处理用户服务包括页面显示、请求应答这些,不包含业务逻辑
  • 应用层(Application Layer)主要包含应用服务包括持久化事务、安全认证、外部系统消息等,协调领域对象的操作,很薄
  • 领域层(Domain Layer)包含领域对象,提供无状态的领域服务供应用层使用以及发布领域事件,他们都是可独立测试的业务单元
  • 基础设施层(Infrastructure Layer)提供持久化机制、消息机制等基础能力

传统分层架构.png

但是,这会碰到领域层或多或少依赖基础设施层的挑战,一些接口实现依赖基础设层比如持久化机制,这就破坏了依赖方向。

解决办法是引入DIP。

1.3 依赖倒置原则

Robert C.Martin(Bob大叔)的这两句话堪称经典:

高层模块不应该依赖于低层模块,两者都应该依赖于抽象。

抽象不应该依赖于细节,细节应该依赖于抽象。

将基础设施层作为上层,用户接口层、应用层和领域层视为同一层,在其下层,这样解决了依赖方向问题。

DIP改进的分层架构.png

这样看上去,无所谓高层与低层了,所有层都可以依赖于抽象,整个分层好像被推平了一样。

加入对称性,迎来了DDD的首选架构——六边形架构。

1.4 六边形架构

六边形架构由Cockburn提出,也被称作端口与适配器模式(Ports and Adapters Pattern)。就像引入DI后所有分层被推平一样(它就会自然具有六边形架构风格),六边形架构的设计思想就是要平等看待系统的输入输出。我们所熟知的Http、MQ、文件系统、数据库都是可以适配的端口。

六边形架构将系统分成了三个子域,用户端(User-Side),业务逻辑(Business Logic)和服务端(Server-Side),业务逻辑就包含领域核心。

用户端类似于用户接口层,是驱动(Driving)业务逻辑的,表示在左边。

业务逻辑类似于应用层和领域层,是所有业务定义与实现的部分,不受端口变化而变化。

服务端类似于基础设施层,是被(Driven)业务逻辑所驱动的,表示在右边。

当然也会碰到依赖方向被挑战的情况,解决办法依然是把DIP拿来,各端通过依赖抽象解耦。

六边形架构视图1.png

当然,被很多人熟知的是这种表达形式:

六边形架构视图2.png

其实,正如六边形架构的作者Cockburn所说,六边形的“六”并不重要,叫做四边形、七边形都可以。只是在图上摆放端口时,六条边基本够用又不会显得拥挤,而且相对端口-适配器,这种命名方式更好记忆,也就这么沿用下来了。

业界讨论DDD的架构设计,默认通过这种结构来表达了。弱化六边形的边角、进一步分离业务逻辑,就会变成洋葱架构,但我们谈论的依然是六边形架构,而一般地表达还是会拿架构鼻祖分层架构来命名。

2.如何设计分层架构

我们虚拟项目的例子跌跌撞撞也活到了这一集,终于要开始编码了。

我们用SpringBoot作为开发框架先来搭建工程顶层结构。当然,这里的结构与语言是无关的。

首先是domain层,重要的模块建立起来,用来封装领域数据和逻辑,是整个系统的核心。

然后是application层,作为domain的门面,封装领域服务,处理横切关注点。

接着是adapter层,作为端口适配器屏蔽输入输出技术差异,进一步分为driven被动适配器层和driving主动适配器层。

driving层我们目前涉及到持久化persistence层,driven层支持适配http、消息和rest(http侧重控制转发是MVC架构中的C,rest是状态转移侧重资源传递)。

还有一些可能对整个工程都生效的部分如框架framework层和工具util层就放置在common层。framework会调用我们自己写的代码,而util被我们写的代码所调用。

注意,这里的driven和driving是相对领域层而言的,和上面说的分别从用户端和服务端讲的主体不同。

 |-- adapter
     |-- driven
         |-- mq
         |-- restful
         |-- web
     |-- driving
         |-- persistence
 |-- application
 |-- domain
 |-- common
     |-- framework
     |-- util
 |-- SafetymonitorApplication.java

一个初步的分层架构就有了,引用下极客时间的图示看得更清晰。

分层架构举例

按照各个分层的领域模块耦合进一步深化分层。

 |-- adapter
     |-- driven
         |-- mq
         |-- restful
             |-- alarmmng
             |-- cameramng
             |-- controlmng
             |-- usermng
         |-- web
     |-- driving
         |-- persistence
             |-- alarmmng
             |-- cameramng
             |-- controlmng
             |-- usermng
 |-- application
     |-- alarmmng
     |-- cameramng
     |-- controlmng
     |-- usermng
 |-- domain
     |-- alarmmng
     |-- cameramng
     |-- controlmng
     |-- usermng
 |-- common
     |-- framework
     |-- util
 |-- SafetymonitorApplication.java

到这里,DDD的分层架构的雏形就形成了。

分层架构与六边形架构结合的最大魅力就是domain层的独立性。

至于测试,现代开发框架很容易实现对于driven层的触发,对于driving层可以通过Mock来驱动测试,这些SpringBoot都有良好的支持。也就是说domain层与driven层集成测试不需要依赖driving层,与driving层集成测试也不需要driven层。

接下来终于到了最可爱的撸代码环节,会碰到并解决很多代码坏味道,并会发掘新的业务知识修补领域模型不完善的地方,实现DDD的最后一公里。


参考