Skip to content

Commit cd5a5db

Browse files
github-actions[bot]mameka
andauthored
[no-image] Add Chinese-translated articles (#2274)
feat: Add Chinese-translated articles Co-authored-by: mameka <mameka-bot@mamezou.com>
1 parent 5d6ae1a commit cd5a5db

File tree

5 files changed

+2134
-0
lines changed

5 files changed

+2134
-0
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
title: 对六边形架构的“违和感”彻底剖析!通过图解清晰理解3个疑问与本质
3+
author: toshio-ogiwara
4+
date: 2025-12-08T00:00:00.000Z
5+
tags:
6+
- ソフトウェア設計
7+
- advent2025
8+
image: true
9+
translate: true
10+
11+
---
12+
13+
这是[is开发者网站Advent日历2025](/events/advent-calendar/2025/)第8天的文章。
14+
15+
对于六边形架构(Ports & Adapters),虽然感觉上大概理解了,但总有一些难以释怀的地方,对吧?就我而言,主要有以下三点。
16+
17+
- 虽说“依赖应从外侧 → 内侧”,但为什么**在输入端口与实现之间依赖看起来是反向的**?这样可以吗?
18+
- **输入适配器没有实现端口接口**,却**输出适配器实现了端口接口**,这让人感觉怪怪的
19+
- 归根结底,**六边形架构和洋葱架构到底有什么区别?**
20+
21+
本文将以示例结合的方式,说明整理这些困惑时的笔记。
22+
23+
## 1. 用于说明示例的六边形架构结构
24+
25+
首先,本文以下所示的“教科书式六边形”包结构的 Spring Boot 待办(TODO)应用为示例进行说明。
26+
27+
```text
28+
com.example.todohex
29+
├─ TodoHexApplication … @SpringBootApplication
30+
31+
├─ domain … 领域模型(纯Java)
32+
│ └─ Task.java
33+
34+
├─ application
35+
│ ├─ port
36+
│ │ ├─ in … 输入端口(UseCase 接口)
37+
│ │ │ ├─ CreateTaskUseCase.java
38+
│ │ │ └─ GetTaskUseCase.java
39+
│ │ └─ out … 输出端口(Repo/Gateway 接口)
40+
│ │ ├─ SaveTaskPort.java
41+
│ │ └─ LoadTaskPort.java
42+
│ └─ service … 用例实现
43+
│ └─ TaskService.java
44+
45+
├─ adapter
46+
│ ├─ in
47+
│ │ └─ web … REST 适配器(输入端)
48+
│ │ ├─ TaskController.java
49+
│ │ ├─ TaskRequest.java
50+
│ │ └─ TaskResponse.java
51+
│ └─ out
52+
│ └─ persistence … 持久化适配器(输出端)
53+
│ ├─ TaskEntity.java
54+
│ ├─ SpringDataTaskRepository.java
55+
│ └─ TaskPersistenceAdapter.java
56+
└─ ...
57+
```
58+
59+
## 2. 有些依赖方向相反,这样可以吗?
60+
61+
那么,马上进入第一个困惑。哪里看起来是“反向的”呢?按照教科书式的方式,从 UseCase 推导到 Service 时,依赖关系[^1]会是反向的。如果根据示例画图,就像下图所示,红线的依赖关系是从右到左的。
62+
63+
![01](/img/blogs/2025/1208_hexagonal_four_questions/01_port-in.drawio.svg)
64+
65+
[^1]: 在 UML 中,“依赖关系”指的是两者之间的一种临时关系 (dependency),但在此处并不是以 UML 的意义来使用,而是用“依赖关系”一词来表示简单的使用与被使用关系。
66+
67+
另一方面,针对六边形架构的众多教程中常常出现“模块的依赖应是外→内”的说明。此时就会产生这样的疑问:“诶,`port``service` 的依赖关系不是反向的嘛,这样可以吗?”。
68+
69+
因此,我们暂且回到原典,回顾六边形架构的提出者阿里斯泰尔·柯本(Alistair Cockburn)本人是怎么说的。在他可视为原典的[原文][1]中,大致可以归纳为以下几点。
70+
71+
> * 应用通过 **Port** 与外部进行对话
72+
> ***Port** 的协议以“应用程序 API”的形式存在
73+
74+
这里所说的“API”可以是方法调用、HTTP,也可以是消息协议等任何形式,是相当抽象的层面。至少在原典中,并没有像 Java 生态中的那种“礼节级”说法:
75+
76+
* “把输入端口分为 **接口和实现类**
77+
* “模块的依赖箭头**务必指向外→内**
78+
79+
另外,在他最近的[幻灯片版][2]中,为了面向强类型语言,提到了
80+
81+
> * 声明“required interface”
82+
> * 为 Port 声明准备一个文件夹
83+
84+
之类的内容,但**并未深入依赖箭头规则本身**
85+
86+
:::column: 结论:依赖从外侧到内侧只是个都市传说
87+
阿里斯泰尔·柯本没有对依赖方向作出任何规定。恰恰相反,他说要在端口处暴露接口,因此端口与其实现之间的依赖关系逆向是很自然的。我个人认为,这个说法是因为在与明确指出“依赖只能从外层环向内层环”的洁净架构[^2]放在同一语境下讨论六边形架构时产生的都市传说。
88+
不过,如果将 `port.in``service` 看作一个“应用核心模块”,那么 `adapter.in``(port.in + service)``domain` 就形成了“外→内”的结构,因此也可以将其视为洁净架构的一种而无大碍。
89+
:::
90+
91+
[^2]: 我个人认为 Bob叔所说的洁净架构只是提供了一种“洁净架构应该如何!”之类的概念,并不存在所谓的“洁净架构”这种架构。所以,文中所说的洁净架构,是指领域与技术细节分离和隔离的“洁净架构”含义。
92+
93+
## 3. 适配器不实现端口接口吗?
94+
95+
下一个困惑是这个。
96+
97+
* in 侧的适配器(如 Controller)没有实现 port.in(红色依赖)
98+
* out 侧的适配器(DB 或外部 API)实现了 port.out(蓝色依赖)
99+
100+
仅靠文字不太容易理解,用图示如下。
101+
102+
![02](/img/blogs/2025/1208_hexagonal_four_questions/02_adapter.drawio.svg)
103+
104+
同样是适配器,却有的要实现端口、有的不用,左右也不对称,总觉得怪怪的。说实话,难道只有我觉得这真没问题吗?
105+
106+
有疑问就回到[原典][1]看看柯本是怎么说的。他把 Hexagonal 也称为 Ports & Adapters,在这里的 Port 和 Adapter 是“角色名称”。
107+
108+
* Port:
109+
* 表示“为了什么而进行对话”的**逻辑接口**
110+
* Adapter:
111+
* 将该 Port 接入到特定技术(HTTP / CLI / DB / 邮件 / 文件…)的**转换器**
112+
113+
而在他的[幻灯片][2]中,将 Port 分为
114+
115+
* Driving Ports(驱动应用程序的那一侧)
116+
* Driven Ports(被应用程序驱动的那一侧)
117+
118+
来进行说明。
119+
120+
从这个视角来看,
121+
122+
* Driving Port 侧
123+
* Adapter(UI / REST / Batch …)是按照 Port 定义进行调用的**客户端**
124+
* Driven Port 侧
125+
* Adapter(DB / 邮件 / 外部 API …)是满足 Port 定义并执行的**服务器**
126+
127+
因此,
128+
129+
* in 侧的适配器不实现端口接口
130+
* out 侧的适配器实现了端口接口
131+
132+
这种**非对称性其实很自然**
133+
134+
:::column: 结论:适配器和端口是角色名,并非语法模式
135+
Port / Adapter 这一名称并不是指“输入 = implements,输出 = implements”这样的语法模式,而仅仅是指**“表示对话目的的窗口”和“与外界的转换器”**这两个角色。这样一来,实现与否的非对称性就不那么令人在意了。
136+
:::
137+
138+
## 4. 六边形架构和洋葱架构有什么不同?
139+
140+
最后一个困惑是:
141+
> 到底六边形架构和洋葱架构有什么区别?
142+
143+
那么,为了看清它们的区别,我们来分别看看它们的整体结构。
144+
145+
### 首先是洋葱架构
146+
147+
粗略地画出洋葱架构,大致如下。
148+
149+
![03](/img/blogs/2025/1208_hexagonal_four_questions/03_onion.drawio.svg)
150+
151+
<br>
152+
153+
洋葱架构的重点是(从本图中不太容易看出……)
154+
* 以领域为中心,以同心圆状构建各个层
155+
* 依赖应当是外侧 → 内侧
156+
* **保护领域**
157+
158+
以上几点。
159+
160+
### 接下来是六边形架构的结构
161+
162+
相对而言,六边形架构(Ports & Adapters)是一种**聚焦在边界(Port)上的架构**
163+
164+
![04](/img/blogs/2025/1208_hexagonal_four_questions/04_hexagonal.drawio.svg)
165+
166+
将两者并排比较,可以看出六边形架构在结构上将洋葱架构的 application 部分及其边界细分为 `port.in` / `port.out``adapter.in` / `adapter.out`**强调了输入输出的边界(在哪里进入、又从哪里退出)**
167+
168+
换句话说:
169+
170+
- 洋葱架构:通过层(Layer)来保护内部的架构
171+
- 六边形架构:通过端口和适配器来强调边界的架构
172+
173+
而它们所追求的目标本身都非常相似:
174+
175+
* 以领域为中心
176+
* 与外界(UI/DB/外部系统)解耦
177+
* 提高可测试性
178+
179+
:::column: 结论:六边形架构可以说是洋葱架构的高级版本
180+
从结构角度来看,“六边形 = 将洋葱架构的 application + 边界部分分解为 port 与 adapter,从而‘强调输入输出边界’的版本”。然而,洋葱架构的特点是从外侧向内侧依次构建层,而六边形架构则如图中蓝线所示,形成了外 → 内 → 外的结构,以在结构层面强调“从哪里进入,又向哪里退出”。因此,其原始概念是不同的。
181+
:::
182+
183+
## 5. 结语
184+
185+
采用六边形架构确实能够实现一种干净的架构,但这也需要付出成本。柯本本人在[幻灯片][2]中也提到:
186+
187+
* 每个 Port 都会增加字段和 DI 配置
188+
* 在强类型语言中需要为 Port 准备接口和文件夹结构
189+
* 需要设计 Configurator(配置根)
190+
191+
换言之,六边形架构以整洁为代价,会增加类和接口的数量。如果个人只是想分离领域,洋葱架构往往就足够了。
192+
好的方案并不总是适合所有情况。在架构设计中,重要的是思考自己真正需要什么,并选择与之相匹配的架构。
193+
194+
[1]: https://alistair.cockburn.us/hexagonal-architecture?utm_source=chatgpt.com "hexagonal-architecture - Alistair Cockburn"
195+
[2]: https://alistaircockburn.com/Hexagonal%20Budapest%2023-05-18.pdf?utm_source=chatgpt.com "Hexagonal Architecture ( Ports & Adapters )"

0 commit comments

Comments
 (0)