想象一家法国餐厅的后厨:主厨和菜谱只关心把菜做好。顾客在包间还是大厅、是用现金还是手机支付,后厨统统不关心。顾客的需求通过“点单口”递进来,变成一张标准菜单;食材则通过“领料口”去仓库领取。假如今天餐厅换成扫码点餐,明天供应商换成另一家农场,后厨的菜谱一行都不用改。六边形架构就是给软件后厨装上的这套点单口和领料口。 它把业务逻辑放在最中间,周围只留出约定好的“端口”,再由“适配器”去对接不同的外部技术。于是,UI、数据库、消息队列都变成了可以随时更换的桌椅和供应商,真正重要的菜谱永远只依赖那几扇门。你不需要先掌握架构理论,只要记住:核心管业务,端口管约定,适配器管翻译。
对 AI 从业者而言,六边形架构能让你交付的模型服务、Agent 编排系统不再被某个数据库或模型厂商绑架,测试时还能用替身快速验证核心逻辑,这是架构层应对 AI 技术飞速迭代的护身符。
目录
可视化演示
动态演示六边形架构端口与适配器模式的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
六边形架构端口与适配器模式:六边形架 六边形架构核心理念与依赖反转 端口接口定义与适配器实现 核心机制可视化 六边形架构端口与适配器模式整体架构与关键组件
1. 架构起源:后厨为什么站在中间
一家法餐厅最值钱的是后厨的菜谱,而不是某个牌子的燃气灶或冰箱。顾客在哪里吃饭不重要,重要的是后厨能稳定出餐。软件也一样:业务规则是菜谱,框架和外部系统是炉灶和冰箱。把菜谱和炉灶焊死,换一个牌子的炉灶就要重写菜谱,这是传统分层架构最让人头疼的地方。
六边形架构正是为了解决这个痛点而生。Alistair Cockburn 在大量企业级系统开发中发现,UI 和数据库并不像传统分层图里画的那样“高高在上”或“垫在底下”,它们只是围绕业务核心的普通访客。于是他用六边形表示核心,把外部系统画在六边形外面,避免视觉上产生“上层/下层”的等级暗示。
每条边都代表一个对外交互的通道。核心既不认识 HTTP,也不认识 SQL,它只通过通道上的“端口”对外表达“我需要什么”和“我能提供什么”。端口是一组约定,真正把外部世界和约定对接起来的,是后续要讲的适配器。
回到餐厅比喻:六边形不是让你真的修六个门,而是提醒你——门可以多也可以少,但所有门都必须对着外面的世界,而不是对着后厨的炉灶。门里门外互不干扰,才能保证后厨的稳定性。
- 核心符号
\text{核心}代表后厨里的菜谱、切配、烹饪规则,是软件中最不容易变的部分。 - 端口并集
\bigcup \text{端口}表示把很多“门”拼在一起;“并集”强调每个端口彼此独立,又共同组成完整边界。 - 组合箭头
\longrightarrow表示核心与端口组合后,形成完整的“六边形”运行时。
2. 端口:给业务核心留下的门
后厨必须有两个口:一个是“点单口”,顾客的菜名从这里进来;一个是“领料口”,后厨从这里领取食材。没有这两个口,服务员只能闯进后厨大声喊,供应链只能直接往锅里倒货,后厨会乱成一团。端口就是软件后厨的门洞。
在技术层面,端口是接口或抽象类。它不写实现,只写“能力”和“输入输出”。点单口就是“入端口”,也叫驱动端口,把外部用户的诉求翻译成核心能执行的用例;领料口就是“出端口”,也叫被驱动端口,把核心想调用的数据、消息、文件等能力定义成服务需求。
定义端口时,核心决定的是“菜谱需要什么”,而不是“供应商怎么送货”。例如订单服务会定义一个 OrderRepository 接口,方法可能叫“保存订单”,但不会写“连接 MySQL 的哪个表”。这样,数据库替换时,核心的“菜谱”完全不用动。
点单口和领料口一旦定下来,服务员和供应商就能各自准备。核心只认菜单和领料单,不关心写菜单的人是手写还是 iPad 点餐。这就是端口带来的“契约感”。
- 入端口标识
\text{入端口}表示接收外部请求的那扇门,例如用户下单的用例入口。 - 出端口标识
\text{出端口}表示核心需要外部配合完成的那扇门,例如保存订单、发送通知。 - 箭头语义
\to在这里表示“调用/使用”的流向;箭头指向的一方是核心依赖或暴露的目标。
3. 适配器:门外世界的翻译官
点单口写的是“宫保鸡丁”,但门口的服务员可能需要把英文菜单、扫码枪、顾客语音全部翻译成这几个字。领料口写的是“冷藏鸡肉两公斤”,进来的可能是冷库、冰柜或农场直送。适配器就是这些负责翻译和搬运的角色。
驱动端适配器在外部系统这一侧,它把 HTTP 请求、CLI 命令、消息队列事件等翻译成入端口的方法调用;被驱动端适配器在资源这一侧,它把核心定义好的出端口实现成 MySQL、Redis、第三方 API 的具体操作。两者都实现了端口,却不包含业务规则。
适配器还可以充当“替身”。测试时,用一个内存版 Adapter 替代真实数据库,核心代码完全不知道也不在意。这就像后厨测试新菜时,先用一张打印好的菜单而不必真的让服务员跑一趟,只要能验证出餐流程即可。
在餐厅里,服务员和采购员可以随时换人,只要他们看得懂点单口和领料口的规定。技术框架迭代时,你只需要换掉一个适配器,而不是掀翻整个后厨。
- 适配器主体
\text{适配器}是外部的翻译官,负责和真实世界打交道。 - 外部协议
\text{外部协议}指 HTTP、gRPC、SQL、消息队列等具体技术方言。 - 端口调用
\text{端口调用}是把方言转成核心端口上的方法调用,让核心无需理解方言。
4. 依赖方向:让箭头指向核心
传统架构经常出现“核心依赖数据库”的情况:代码里直接 new 一个数据库连接,业务方法里写 SQL。这就像菜谱上写着“必须使用某品牌冰箱的冷藏格”,一旦换冰箱,菜谱也要改。六边形架构要做的是把依赖翻过来:核心只依赖端口,数据库适配器反过来依赖端口。
这里的关键点是“源码依赖方向”和“调用执行方向”是相反的。数据库适配器确实会被核心调用,但从源码依赖看,适配器必须实现端口接口,所以它了解核心定义的端口;而核心完全不 import 任何数据库驱动。箭头指向哪一方,哪一方就拥有稳定性。
这种反转也叫依赖倒置。借助依赖注入,在程序启动的组合根处把具体适配器交给核心,核心甚至不知道当前用的是 MySQL 还是 PostgreSQL。替换数据库时,你只需要重新组装一次,核心用例一次都不改。
如果有一天你发现某个核心类里 import 了具体的数据库驱动或消息中间件 SDK,就说明有人把供应商直接塞进了后厨。停下来,把它挪到适配器里去。
- 核心依赖端口
\text{核心} \to \text{端口}表示核心源码中引用端口接口,而不是具体实现。 - 适配器依赖端口
\text{端口} \leftarrow \text{适配器}表示适配器实现该接口,因此它认识端口。 - 双向箭头汇聚两端箭头汇聚在端口上,说明核心与适配器之间没有直接依赖,连接点只有端口。
5. 同类架构:洋葱、整洁与六边形是一家人
后厨会演化出不同布置:有的餐厅给后厨画成同心圆,有的画成一层层套间,但核心思想都是“重要的东西放在最里面”。六边形架构、洋葱架构、整洁架构说的其实是同一件事,只是画法和强调重点不同。
洋葱架构由 Jeffrey Palermo 提出,把核心领域放最内层,外层包裹应用层和基础设施层;整洁架构由 Robert C. Martin 提出,明确用 Use Case 和 Entity 把依赖规则分层。六边形架构则更强调端口和适配器的对称性,不规定一定要有多少个圈、多少层。
它们的共同铁律是:依赖只能由外向内,内层不依赖外层。区别主要在于术语和图形:六边形把多个端口放在六条边上,洋葱用一圈一圈的同心圆强调层次,整洁架构把接口适配器放在最外层。选择哪一种并不重要,重要的是依赖方向别画反。
无论是六边形还是洋葱,判断架构好不好,只需问一句:如果把 MySQL 换成 PostgreSQL,核心代码会变红吗?如果变红,说明依赖方向出了问题。
- 外部层
\text{外部层}代表 UI、数据库、第三方服务等不稳定易变的部分。 - 接口层
\text{接口层}代表端口、DTO、入出力转换等边界层。 - 依赖箭头
\rightarrow表示“依赖”而不是“数据流动”;数据可以流向核心,源码依赖必须指向内核。
6. 工程实践:Go 与 Java 的落地方法
讲完思想,后厨要真正开张了。Go 和 Java 的落地风格略有不同,但都是围绕“接口做端口、结构体/类做适配器”展开。
在 Go 中,接口是隐式满足的。你可以在核心包定义 OrderStore 端口,而数据库适配器包只需要提供带有相同方法签名的结构体,不需要显式声明 implements。这让适配器可以保持轻量,也让核心包不被基础库污染。Java 则更强调显式接口和包结构:把端口 interface 放在领域层或应用层,把数据库实现放在独立的 infra 模块,通过依赖注入框架在启动时组装。
测试时也可以利用端口造“木偶”:用内存 Map 实现仓库端口,用日志打印实现通知端口。这样单元测试不需要连数据库、不需要发邮件,几十毫秒就能跑完。在 DDD 项目中,这种端口与适配器的划分往往直接对应领域服务与基础设施的边界。
给餐厅装上这两扇门之后,换供应商就像换一家农场送货:菜谱还是那张菜谱,厨房还是那个厨房,唯一的改动是采购员手里的送货单模板。这就是端口与适配器最朴素的价值。
- 接口同构端口
\text{interface} \cong \text{端口}说明 Go/Java 中的接口在架构中承担端口角色。 - 实现同构适配器
\text{实现} \cong \text{适配器}说明实现类只不过是对外部技术的翻译。 - 映射关系把接口和实现的对应关系代入六边形,就得到了“核心定义接口、外部实现接口”的落地结构。
AI 应用场景
LLM 模型服务网关
阶段:模型服务集成与上线
解决的问题:很多 LLM 应用把 OpenAI SDK、向量数据库、Redis 的配置直接写在 Agent 业务代码里。换模型供应商或换向量库时,业务逻辑被迫修改,线上事故频发。六边形架构把对话编排放在核心,只依赖 ChatModel 端口和 MemoryStore 端口,由适配器对接 OpenAI、Claude、本地模型以及不同的向量数据库。
具体案例:一个客服机器人团队需要从闭源模型切换到大模型私有化部署。由于业务核心只依赖端口,团队只需新增一个本地模型适配器,并在组合根处切换注入;Agent 编排逻辑、提示词模板、会话管理全部复用。
推荐系统特征工程隔离
阶段:特征工程与在线离线一致性
解决的问题:推荐模型既要使用离线 Hive 特征,又要使用在线 Redis 特征,如果特征读取逻辑散落在各处,离线与在线一致性很难保证。通过定义 FeatureStore 出端口,核心的排序逻辑只关心“取到特征”,不同数据源由适配器负责映射。
具体案例:某推荐服务把特征拼接逻辑从数据源代码中抽离,离线组件和在线服务共用同一个核心模块,只在启动时注入 Hive 适配器或 Redis 适配器,两侧特征结果差异从小时级下降到分钟级。
Python 演示
from abc import ABC, abstractmethod # 端口: 仓库接口, 对应六边形的出端口class OrderRepository(ABC): @abstractmethod def save_order(self, order_id: int, amount: float) -> None: pass # 端口: 通知接口, 另一个出端口class Notifier(ABC): @abstractmethod def send(self, message: str) -> None: pass # 核心业务逻辑: 只依赖端口, 不知道数据库和邮件细节class OrderService: def __init__(self, repo: OrderRepository, notifier: Notifier): self.repo = repo self.notifier = notifier def place_order(self, order_id: int, amount: float) -> str: self.repo.save_order(order_id, amount) self.notifier.send("order placed") return "ok" # 适配器: 用内存字典实现仓库端口class MemoryOrderRepository(OrderRepository): def __init__(self): self.store = {} def save_order(self, order_id: int, amount: float) -> None: self.store[order_id] = amount # 适配器: 用控制台打印实现通知端口class ConsoleNotifier(Notifier): def send(self, message: str) -> None: print("[notify]", message) # 组合根: 在程序入口把适配器注入核心if __name__ == "__main__": repo = MemoryOrderRepository() notifier = ConsoleNotifier() service = OrderService(repo, notifier) print(service.place_order(42, 199.0))
OrderService 只依赖两个端口接口,替换仓库或通知实现时不需要改动核心。class OrderRepository(ABC):定义出端口,核心不关心数据存到哪。class Notifier(ABC):定义另一个出端口,核心只负责发出通知意图。class OrderService:核心业务逻辑,只持有端口类型的依赖。def __init__(self, repo: OrderRepository, notifier: Notifier):通过构造函数注入端口,外部适配器在启动时被组装进来。def place_order(self, order_id: int, amount: float) -> str:用例方法,只调用端口,不出现任何具体库名。class MemoryOrderRepository(OrderRepository):内存适配器,测试时可以直接替换数据库实现。service = OrderService(repo, notifier)组合根,把适配器注入核心,这是依赖反转发生的时刻。
常见误区
误区我一直以为六边形六条边就要有六个端口,少一个就不标准。
正解六边形只是 Cockburn 画图时顺手选的多边形,为了对称好看,绝不是规定。你的系统可能只有一个入端口和两个出端口,甚至画成八边形也不影响架构思想。真正重要的是核心被端口包围,外部系统只能从这些端口进入。
为何常见:看到“六边形”这个词,很容易把图形字面化,误以为六条边是强制要求。
误区我已把 Controller 层叫做端口了,这应该就是六边形架构。
正解端口是核心内部定义的接口契约,属于核心的“门框”;HTTP Controller 只是门外的一个适配器,负责把 HTTP 报文翻译成端口调用。API 路径、状态码、请求体全是外部协议,沾满这些细节的代码不该出现在核心包里。
为何常见:日常开发中最常接触的就是对外 API,容易把“接口”这个词和端口混淆。
误区我在 Service 层写了 interface,再写个 impl,应该已经应用了六边形架构。
正解为了分层而随手拆接口,并不等于架构隔离。六边形要求核心不依赖任何外部框架和基础设施,接口必须由核心定义、适配器反向实现,并且依赖方向始终指向核心。如果接口定义在基础设施包里,核心反而去依赖基础设施,那就是挂羊头卖狗肉。
为何常见:接口是六边形的必要元素,但缺少“依赖反转”和“边界控制”的接口只是普通抽象。
误区既然它们都画圆圈、都强调依赖向内,那学一个就够了。
正解目标一致但侧重点不同。六边形架构强调端口和适配器的对称性,洋葱架构强调分层包裹,整洁架构强调用例和实体为核心。实际项目中它们可以互相借鉴,但生硬地认为“一样”会忽略不同语境下的术语差异。
为何常见:三者都在解决同一类问题,图形相似导致很多初学者不再深究概念边界。
误区为了保持纯净,核心最好什么都不 import。
正解核心不是“零依赖”,而是“不依赖外部变化”。它可以依赖语言标准库、自己的领域模型,以及自己定义的端口接口。如果连端口都不能依赖,核心就无法表达“我需要保存订单”这样的意图。
为何常见:把“不依赖框架”听成了“不依赖任何代码”,忽略了对领域内生概念的正常使用。
面试题
Q1. 六边形架构中,端口的主要作用是?
- A.定义核心与外部交互的契约← 正确答案
- B.直接操作数据库与缓存
- C.负责渲染前端页面样式
- D.统一管理所有环境变量
答案:A。端口是核心与外部世界之间的契约。就像点单口只负责传递标准菜单,不负责做菜,也不负责送餐;数据库和页面渲染只是外部世界的具体事务。
Q2. 六边形架构中,适配器的主要职责是?
- A.把外部技术翻译成端口调用← 正确答案
- B.让核心可以直接操作数据库
- C.替外部系统实现全部业务规则
- D.在核心内部生成接口定义
答案:A。适配器是翻译官。外部技术说方言,核心说普通话,适配器负责把 HTTP、SQL、消息等方言转成核心端口能理解的方法调用,而不是替核心思考业务。
Q3. 关于六边形架构的依赖方向,正确的是?
- A.核心依赖数据库和框架
- B.核心与适配器互相依赖
- C.核心和适配器都依赖端口← 正确答案
- D.端口依赖数据库实现
答案:C。依赖箭头要汇聚到端口上。核心调用端口,适配器实现端口,两者都不直接认识对方。这就像点单口连接服务员和后厨,双方只认口,不认脸。
Q4. 关于驱动端和被驱动端,正确的是?
- A.左侧适配器属于被驱动端
- B.被驱动端由核心主动调用← 正确答案
- C.驱动端必须使用 HTTP 协议
- D.每个端口只能有一个适配器
答案:B。被驱动端通常对应出端口,例如仓库接口,核心会主动调用它。左侧不一定就是被驱动端,协议也不是判断依据;左边的适配器通常是“驱动端”,主动把请求递进核心。
Q5. 替换数据库时,最常见需要修改的是?
- A.核心业务用例
- B.端口接口定义
- C.数据库适配器← 正确答案
- D.六边形本身形状
答案:C。核心只依赖端口,端口不关心实现。换数据库只需重写适配器,并在组合根重新注入,就像换供应商只会换采购员,绝不会改菜谱。
Q6. 洋葱架构和六边形架构的共同点是?
- A.都必须有六个端口
- B.依赖方向都指向核心← 正确答案
- C.都由 Go 语言实现
- D.都不允许使用数据库
答案:B。洋葱、整洁、六边形本质都是让外层依赖内层,内层不依赖外层。至于端口数量、语言、是否用数据库,都不是判定标准。
Q7. Go 语言中更符合六边形风格的做法是?
- A.在基础设施包定义接口
- B.核心包定义接口外部实现← 正确答案
- C.适配器直接依赖具体库
- D.用继承绑定核心和数据库
答案:B。端口应该由核心定义,这样核心才能只依赖端口。Go 接口是隐式满足的,外部实现类不需要显式声明 implements,但接口归属仍然影响依赖方向。
Q8. 下列哪些属于适配器该做的事?
- A.解析 HTTP 请求为方法调用← 正确答案
- B.将订单存储到 MySQL 数据库← 正确答案
- C.决定订单金额的优惠规则
- D.把查询结果渲染成 JSON← 正确答案
答案:ABD。解析 HTTP、访问 MySQL、渲染 JSON 都是外部技术细节,属于适配器。决定优惠规则是业务规则,应放在核心内部,不能交给适配器。
Q9. 为了保持核心可替换性,需要做到?
- A.核心只依赖端口接口← 正确答案
- B.适配器在组合根注入← 正确答案
- C.端口定义放在基础设施层
- D.测试时替换为假适配器← 正确答案
答案:ABD。核心依赖端口、组合根注入、测试用替身,这三者共同保证核心不依赖具体实现。端口如果定义在基础设施层,核心就会反向依赖基础设施,破坏依赖方向。
Q10. 六边形架构带来的主要收益是?
- A.业务核心不再依赖框架← 正确答案
- B.数据库层可以更好替换← 正确答案
- C.代码数量一定大幅减少
- D.单元测试可以使用替身← 正确答案
答案:ABD。可测试、可替换、核心纯净是六边形架构的标志性收益。代码量并不必然减少,有时反而会多出接口和适配器的结构成本,这是值得的。