3. 整洁架构的问题分析

整洁架构 (Clean Architecture)是由Bob大叔在2012年提出的一个架构模型,顾名思义,是为了使架构更简洁。 但实际上这个架构也并不简洁,已经分了4层,前面讲端口和适配器架构版本2 的时候实际上也有对每一层进行了介绍。

  • 蓝色层简称 驱动层
  • 绿色层简称 适配器层
  • 红色层简称 应用层
  • 黄色层简称 领域层

整洁架构优点在于分层很清晰(相比端口和适配器架构),依赖关系很明确,且右下角有小例子。但我认为整洁架构存在 2 问题。

问题1: 不管主动或者被动适配器都提取了接口

接口适配器层就是适配器层,前面提到只有被动适配器需要依赖倒置,主动适配器其实并不需要。但右下角的案例还是把主动适配器和被动适配器都提取了接口。我们再分析一下被动和主动适配器提取接口的目的。

被动(出口)适配器提取接口

在适配器层,Presenter 是一种被动适配器,是 UI 渲染器,负责把业务模型转成出口 UI,用例层处理完后会调用端口,由 UI 渲染器把 出口 UI 展示出来。而Controller 是一种主动适配器,是 UI 控制器,负责把入口 UI 的模型转成应用层的模型。

举个登录的例子

下面调用方向

sequenceDiagram box rgb(167, 215, 253) 驱动层 participant UI Drive end box rgb(162, 252, 185) 适配器层 participant LoginController end box rgb(255, 161, 156) 应用层 participant LoginAppService end box rgb(252, 253, 183) 领域层 participant LoginDomainService end box rgb(162, 252, 185) 适配器层 participant LoginPresenter end box rgb(167, 215, 253) 驱动层 participant UI Drive end UI Drive ->> LoginController: 触发登录事件 LoginController->>LoginAppService: 执行登录用例 LoginAppService->>LoginDomainService: 执行登录逻辑 LoginAppService-->>LoginPresenter: 业务模型转 UI 模型 LoginPresenter->>UI Drive : 渲染 UI

下面是正确的依赖方案

classDiagram UI_Driver ..> LoginController LoginController ..> LoginApplicationService LoginApplicationService ..> LoginDomainService LoginPresenter ..> LoginApplicationService UI_Driver ..> LoginPresenter

LoginApplicationService 其实会调用 LoginPresenter,导致依赖了 LoginPresenter,因此需要依赖倒置,LoginPresenter 需要提取接口。因为接口是要面向应用层设计的,因此整洁架构对于端口的命名为 User Case Output Port (主动适配器由可称为入口适配器,被动适配器可称为出口适配器)。这是合理的。

主动(入口)适配器提取接口

主动适配器提取接口,并不是为了依赖倒置。如果是依赖倒置,就变成了应用层依赖适配器层了。因此主动适配器提取接口也是面向应用层的,其实更合适的说法是应用层提取接口,所以称为 User Case Output Port 会比 Adapter Port 更合适。

其实我看了很多系统都喜欢对 Service 提取一个接口,美约后续可拓展不同的功能实现。但实际上大部分实现都只有一个。这是因为应用层本来就是面向用例,倾向于个性化的,不追求通用的,所以与其用接口去方便切换实现,还不如定义另一个应用服务。

那提取接口不是为了依赖倒置,而是为了其他目的。

  1. 接口隔离原则。前面提到了应用层其实是业务能力的体现,是系统的门面,这意味着应用层包含了很多系统能力。如果把整个应用层暴露给其他系统或者模块使用,不符合接口隔离原则。因此需要提取接口。
  2. 无缝切换实现。这里的实现不是功能实现,而是调用方式的实现。如果原来是在一个进程里面,可以直接本地调用应用层,但如果后面变成前后端分离,则不能直接本地调用,而是要改成远程调用,这时如果适配器层是依赖应用层的接口,则把实现改成远程调用即可。

不同场景可能有不同目的,目前就想到这2个常见的目的。

问题2: 驱动层依赖适配器层

如果驱动层没有理解错的话这个依赖关系是有问题的,参考端口和适配器架构方式2的解释。适配器应该是驱动层和应用层的粘合剂,恶心自己,让其他两个层都能独立演进。意味着适配器层应该依赖驱动层和应用层。

<架构整洁之道>所说的,"软件的接口适配器层中通常是一组数据转换器,它们负责将数据从对用例和业务实体而言最方便操作的格式,转化成外部系统(譬如数据库以及Web)最方便操作的格式。" 其实也可以看出适配器是面向外部系统设计的,理应依赖外部系统。

下面的图出自 <架构整洁之道>,是整洁架构的一个例子,了解完上面的内容应该对这幅图很清楚了。

但图中我认为有2个问题

  1. View Model 应该和 View 一样在驱动层。因为 View Model 是基于 View 设计的,和 View 是强绑定的,而且不同的 View 框架 View Model 可能也是不一样的。因此都应该是驱动层,由 Presenter 对驱动层的Model和用例层的Model 进行转化。
  2. Data Access 属于被动适配器,Database 属于驱动层。Data Access Intercace 其实端口,和 Output Boundary 是一样的。而 Data Access 是通过 sql (驱动层) 去实现应用层的端口,其实就是一种适配器。

这个有其他观点的可以讨论一下,观点不重要,分析过程才最有价值。

总结

整洁架构分层依赖清晰, 而且有例子<整洁架构之道>, 能落地。应该要向这种架构风格学习。但它自身也有一些问题, 导致完全按照整洁架构落地的时候存在无用接口的问题。另外在整洁架构里面对应用层和领域层的区别也是比较模糊(参考Bob大叔的微博), 导致落地的时候对这 2 层的边界也不是特别清晰。这些问题在菱形对称架构都能得到解决,后面也会介绍到这个模型。

相关推荐
向前看-3 小时前
验证码机制
前端·后端
工业甲酰苯胺4 小时前
分布式系统架构:服务容错
数据库·架构
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
Java程序之猿6 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构
AskHarries7 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion8 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp8 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder8 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
小蜗牛慢慢爬行9 小时前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate