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 层的边界也不是特别清晰。这些问题在菱形对称架构都能得到解决,后面也会介绍到这个模型。

相关推荐
人机与认知实验室37 分钟前
从“九三架构”看人机耦合频率、相变与态势感知谱系
架构
Maiko Star1 小时前
* SpringBoot整合LangChain4j
java·spring boot·后端·langchain4j
明月_清风1 小时前
Go语言空接口与类型断言完全指南:从"万能容器"到"类型还原"
后端·go
闵孚龙1 小时前
Qwen3.7-Max深度解析:智能体Agent、AI编程、MCP工作流、跨框架泛化与百炼API,一次讲透国产大模型新前沿
人工智能·算法·架构·ai编程
敖正炀2 小时前
DDD 战略设计:限界上下文、实体与值对象
架构
每天进步一点_JL2 小时前
Spring Boot 缓存体系
后端
百珏2 小时前
[灰度发布]:全链路透传组件:APM、自研方案与 Java Agent 的实现取舍
后端·设计模式·架构
正在走向自律2 小时前
DISTINCT 去重查询为什么这么慢?聊聊我能理解的几种优化思路
后端
OpsEye2 小时前
数据库连接池爆了,这3个命令能救你一次
运维·数据库·后端
绝知此事2 小时前
【产品更名】通义灵码升级为 Qoder CN:AI 编码助手新时代,附大模型收费与 Spring Boot 支持全对比
人工智能·spring boot·后端·idea·ai编程