想象一家法国餐厅的后厨:主厨和菜谱只关心把菜做好。顾客在包间还是大厅、是用现金还是手机支付,后厨统统不关心。顾客的需求通过“点单口”递进来,变成一张标准菜单;食材则通过“领料口”去仓库领取。假如今天餐厅换成扫码点餐,明天供应商换成另一家农场,后厨的菜谱一行都不用改。六边形架构就是给软件后厨装上的这套点单口和领料口。 它把业务逻辑放在最中间,周围只留出约定好的“端口”,再由“适配器”去对接不同的外部技术。于是,UI、数据库、消息队列都变成了可以随时更换的桌椅和供应商,真正重要的菜谱永远只依赖那几扇门。你不需要先掌握架构理论,只要记住:核心管业务,端口管约定,适配器管翻译。

岗位关联

对 AI 从业者而言,六边形架构能让你交付的模型服务、Agent 编排系统不再被某个数据库或模型厂商绑架,测试时还能用替身快速验证核心逻辑,这是架构层应对 AI 技术飞速迭代的护身符。

目录
  1. 可视化演示
  2. 1. 架构起源:后厨为什么站在中间
  3. 2. 端口:给业务核心留下的门
  4. 3. 适配器:门外世界的翻译官
  5. 4. 依赖方向:让箭头指向核心
  6. 5. 同类架构:洋葱、整洁与六边形是一家人
  7. 6. 工程实践:Go 与 Java 的落地方法
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示六边形架构端口与适配器模式的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

六边形架构端口与适配器模式:六边形架 六边形架构核心理念与依赖反转 端口接口定义与适配器实现 核心机制可视化 六边形架构端口与适配器模式整体架构与关键组件

1. 架构起源:后厨为什么站在中间

一家法餐厅最值钱的是后厨的菜谱,而不是某个牌子的燃气灶或冰箱。顾客在哪里吃饭不重要,重要的是后厨能稳定出餐。软件也一样:业务规则是菜谱,框架和外部系统是炉灶和冰箱。把菜谱和炉灶焊死,换一个牌子的炉灶就要重写菜谱,这是传统分层架构最让人头疼的地方。

六边形架构正是为了解决这个痛点而生。Alistair Cockburn 在大量企业级系统开发中发现,UI 和数据库并不像传统分层图里画的那样“高高在上”或“垫在底下”,它们只是围绕业务核心的普通访客。于是他用六边形表示核心,把外部系统画在六边形外面,避免视觉上产生“上层/下层”的等级暗示。

每条边都代表一个对外交互的通道。核心既不认识 HTTP,也不认识 SQL,它只通过通道上的“端口”对外表达“我需要什么”和“我能提供什么”。端口是一组约定,真正把外部世界和约定对接起来的,是后续要讲的适配器。

回到餐厅比喻:六边形不是让你真的修六个门,而是提醒你——门可以多也可以少,但所有门都必须对着外面的世界,而不是对着后厨的炉灶。门里门外互不干扰,才能保证后厨的稳定性。

核心+⋃端口⟶六边形\text{核心} + \bigcup \text{端口} \longrightarrow \text{六边形}
这里的“加法”不是数值相加,而是把业务核心与一组端口装配起来,得到一个可以独立运行的架构边界。
  1. 核心符号\text{核心} 代表后厨里的菜谱、切配、烹饪规则,是软件中最不容易变的部分。
    核心\text{核心}
  2. 端口并集\bigcup \text{端口} 表示把很多“门”拼在一起;“并集”强调每个端口彼此独立,又共同组成完整边界。
    ⋃iPorti\bigcup_i \text{Port}_i
  3. 组合箭头\longrightarrow 表示核心与端口组合后,形成完整的“六边形”运行时。
    核心+⋃iPorti⟶六边形\text{核心} + \bigcup_i \text{Port}_i \longrightarrow \text{六边形}

2. 端口:给业务核心留下的门

后厨必须有两个口:一个是“点单口”,顾客的菜名从这里进来;一个是“领料口”,后厨从这里领取食材。没有这两个口,服务员只能闯进后厨大声喊,供应链只能直接往锅里倒货,后厨会乱成一团。端口就是软件后厨的门洞。

在技术层面,端口是接口或抽象类。它不写实现,只写“能力”和“输入输出”。点单口就是“入端口”,也叫驱动端口,把外部用户的诉求翻译成核心能执行的用例;领料口就是“出端口”,也叫被驱动端口,把核心想调用的数据、消息、文件等能力定义成服务需求。

定义端口时,核心决定的是“菜谱需要什么”,而不是“供应商怎么送货”。例如订单服务会定义一个 OrderRepository 接口,方法可能叫“保存订单”,但不会写“连接 MySQL 的哪个表”。这样,数据库替换时,核心的“菜谱”完全不用动。

点单口和领料口一旦定下来,服务员和供应商就能各自准备。核心只认菜单和领料单,不关心写菜单的人是手写还是 iPad 点餐。这就是端口带来的“契约感”。

入端口:调用者→用例,出端口:核心→服务需求\text{入端口}: \text{调用者} \to \text{用例},\quad \text{出端口}: \text{核心} \to \text{服务需求}
入端口是别人调用我们,出端口是我们调用别人;两种门都是核心对外立下的规矩。
  1. 入端口标识\text{入端口} 表示接收外部请求的那扇门,例如用户下单的用例入口。
    调用者→用例\text{调用者} \to \text{用例}
  2. 出端口标识\text{出端口} 表示核心需要外部配合完成的那扇门,例如保存订单、发送通知。
    核心→服务需求\text{核心} \to \text{服务需求}
  3. 箭头语义\to 在这里表示“调用/使用”的流向;箭头指向的一方是核心依赖或暴露的目标。
    调用者→用例, 核心→服务需求\text{调用者} \to \text{用例},\ \text{核心} \to \text{服务需求}

3. 适配器:门外世界的翻译官

点单口写的是“宫保鸡丁”,但门口的服务员可能需要把英文菜单、扫码枪、顾客语音全部翻译成这几个字。领料口写的是“冷藏鸡肉两公斤”,进来的可能是冷库、冰柜或农场直送。适配器就是这些负责翻译和搬运的角色。

驱动端适配器在外部系统这一侧,它把 HTTP 请求、CLI 命令、消息队列事件等翻译成入端口的方法调用;被驱动端适配器在资源这一侧,它把核心定义好的出端口实现成 MySQL、Redis、第三方 API 的具体操作。两者都实现了端口,却不包含业务规则。

适配器还可以充当“替身”。测试时,用一个内存版 Adapter 替代真实数据库,核心代码完全不知道也不在意。这就像后厨测试新菜时,先用一张打印好的菜单而不必真的让服务员跑一趟,只要能验证出餐流程即可。

在餐厅里,服务员和采购员可以随时换人,只要他们看得懂点单口和领料口的规定。技术框架迭代时,你只需要换掉一个适配器,而不是掀翻整个后厨。

适配器:外部协议→端口调用(HTTP/SQL/消息)\text{适配器}: \text{外部协议} \to \text{端口调用} \quad(\text{HTTP/SQL/消息})
适配器是一座桥:桥的一头是外部世界的方言,桥的另一头是核心听得懂的普通话。
  1. 适配器主体\text{适配器} 是外部的翻译官,负责和真实世界打交道。
    Adapter\text{Adapter}
  2. 外部协议\text{外部协议} 指 HTTP、gRPC、SQL、消息队列等具体技术方言。
    HTTP, gRPC, SQL, Kafka\text{HTTP},\ \text{gRPC},\ \text{SQL},\ \text{Kafka}
  3. 端口调用\text{端口调用} 是把方言转成核心端口上的方法调用,让核心无需理解方言。
    调用者→入端口, 核心→出端口\text{调用者}\to\text{入端口},\ \text{核心}\to\text{出端口}

4. 依赖方向:让箭头指向核心

传统架构经常出现“核心依赖数据库”的情况:代码里直接 new 一个数据库连接,业务方法里写 SQL。这就像菜谱上写着“必须使用某品牌冰箱的冷藏格”,一旦换冰箱,菜谱也要改。六边形架构要做的是把依赖翻过来:核心只依赖端口,数据库适配器反过来依赖端口。

这里的关键点是“源码依赖方向”和“调用执行方向”是相反的。数据库适配器确实会被核心调用,但从源码依赖看,适配器必须实现端口接口,所以它了解核心定义的端口;而核心完全不 import 任何数据库驱动。箭头指向哪一方,哪一方就拥有稳定性。

这种反转也叫依赖倒置。借助依赖注入,在程序启动的组合根处把具体适配器交给核心,核心甚至不知道当前用的是 MySQL 还是 PostgreSQL。替换数据库时,你只需要重新组装一次,核心用例一次都不改。

如果有一天你发现某个核心类里 import 了具体的数据库驱动或消息中间件 SDK,就说明有人把供应商直接塞进了后厨。停下来,把它挪到适配器里去。

核心→端口←适配器\text{核心} \to \text{端口} \leftarrow \text{适配器}
端口像一块插座:核心把插头插向插座,适配器从另一个方向把插头插向同一插座;它们没有直接接触,却都遵守同一个形状约定。
  1. 核心依赖端口\text{核心} \to \text{端口} 表示核心源码中引用端口接口,而不是具体实现。
    Core Component→Port Interface\text{Core Component} \to \text{Port Interface}
  2. 适配器依赖端口\text{端口} \leftarrow \text{适配器} 表示适配器实现该接口,因此它认识端口。
    Adapter Implements→Port Interface\text{Adapter Implements} \to \text{Port Interface}
  3. 双向箭头汇聚两端箭头汇聚在端口上,说明核心与适配器之间没有直接依赖,连接点只有端口。
    Core→Port←Adapter\text{Core} \to \text{Port} \leftarrow \text{Adapter}

5. 同类架构:洋葱、整洁与六边形是一家人

后厨会演化出不同布置:有的餐厅给后厨画成同心圆,有的画成一层层套间,但核心思想都是“重要的东西放在最里面”。六边形架构、洋葱架构、整洁架构说的其实是同一件事,只是画法和强调重点不同。

洋葱架构由 Jeffrey Palermo 提出,把核心领域放最内层,外层包裹应用层和基础设施层;整洁架构由 Robert C. Martin 提出,明确用 Use Case 和 Entity 把依赖规则分层。六边形架构则更强调端口和适配器的对称性,不规定一定要有多少个圈、多少层。

它们的共同铁律是:依赖只能由外向内,内层不依赖外层。区别主要在于术语和图形:六边形把多个端口放在六条边上,洋葱用一圈一圈的同心圆强调层次,整洁架构把接口适配器放在最外层。选择哪一种并不重要,重要的是依赖方向别画反。

无论是六边形还是洋葱,判断架构好不好,只需问一句:如果把 MySQL 换成 PostgreSQL,核心代码会变红吗?如果变红,说明依赖方向出了问题。

外部层→接口层→内核\text{外部层} \rightarrow \text{接口层} \rightarrow \text{内核}
这个公式中箭头代表源码依赖方向:外层依赖接口层,接口层依赖内核;内核永远不会回头依赖外层。
  1. 外部层\text{外部层} 代表 UI、数据库、第三方服务等不稳定易变的部分。
    External World\text{External World}
  2. 接口层\text{接口层} 代表端口、DTO、入出力转换等边界层。
    Ports and Adapters\text{Ports and Adapters}
  3. 依赖箭头\rightarrow 表示“依赖”而不是“数据流动”;数据可以流向核心,源码依赖必须指向内核。
    外层→接口层→内核\text{外层} \to \text{接口层} \to \text{内核}

6. 工程实践:Go 与 Java 的落地方法

讲完思想,后厨要真正开张了。Go 和 Java 的落地风格略有不同,但都是围绕“接口做端口、结构体/类做适配器”展开。

在 Go 中,接口是隐式满足的。你可以在核心包定义 OrderStore 端口,而数据库适配器包只需要提供带有相同方法签名的结构体,不需要显式声明 implements。这让适配器可以保持轻量,也让核心包不被基础库污染。Java 则更强调显式接口和包结构:把端口 interface 放在领域层或应用层,把数据库实现放在独立的 infra 模块,通过依赖注入框架在启动时组装。

测试时也可以利用端口造“木偶”:用内存 Map 实现仓库端口,用日志打印实现通知端口。这样单元测试不需要连数据库、不需要发邮件,几十毫秒就能跑完。在 DDD 项目中,这种端口与适配器的划分往往直接对应领域服务与基础设施的边界。

给餐厅装上这两扇门之后,换供应商就像换一家农场送货:菜谱还是那张菜谱,厨房还是那个厨房,唯一的改动是采购员手里的送货单模板。这就是端口与适配器最朴素的价值。

interface≅端口,实现≅适配器\text{interface} \cong \text{端口},\quad \text{实现} \cong \text{适配器}
“≅”在这里表示结构同构:接口就是那扇门,实现类就是那个守门人。
  1. 接口同构端口\text{interface} \cong \text{端口} 说明 Go/Java 中的接口在架构中承担端口角色。
    Interface→Port\text{Interface} \to \text{Port}
  2. 实现同构适配器\text{实现} \cong \text{适配器} 说明实现类只不过是对外部技术的翻译。
    Impl→Adapter\text{Impl} \to \text{Adapter}
  3. 映射关系把接口和实现的对应关系代入六边形,就得到了“核心定义接口、外部实现接口”的落地结构。
    Core→Interface←Impl\text{Core} \to \text{Interface} \leftarrow \text{Impl}

AI 应用场景

LLM 模型服务网关

阶段:模型服务集成与上线

解决的问题:很多 LLM 应用把 OpenAI SDK、向量数据库、Redis 的配置直接写在 Agent 业务代码里。换模型供应商或换向量库时,业务逻辑被迫修改,线上事故频发。六边形架构把对话编排放在核心,只依赖 ChatModel 端口和 MemoryStore 端口,由适配器对接 OpenAI、Claude、本地模型以及不同的向量数据库。

具体案例:一个客服机器人团队需要从闭源模型切换到大模型私有化部署。由于业务核心只依赖端口,团队只需新增一个本地模型适配器,并在组合根处切换注入;Agent 编排逻辑、提示词模板、会话管理全部复用。

推荐系统特征工程隔离

阶段:特征工程与在线离线一致性

解决的问题:推荐模型既要使用离线 Hive 特征,又要使用在线 Redis 特征,如果特征读取逻辑散落在各处,离线与在线一致性很难保证。通过定义 FeatureStore 出端口,核心的排序逻辑只关心“取到特征”,不同数据源由适配器负责映射。

具体案例:某推荐服务把特征拼接逻辑从数据源代码中抽离,离线组件和在线服务共用同一个核心模块,只在启动时注入 Hive 适配器或 Redis 适配器,两侧特征结果差异从小时级下降到分钟级。

Python 演示

python44 行
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))
目的:用 Python 演示一个最小可运行的六边形架构:核心的 OrderService 只依赖两个端口接口,替换仓库或通知实现时不需要改动核心。
  1. class OrderRepository(ABC):定义出端口,核心不关心数据存到哪。
  2. class Notifier(ABC):定义另一个出端口,核心只负责发出通知意图。
  3. class OrderService:核心业务逻辑,只持有端口类型的依赖。
  4. def __init__(self, repo: OrderRepository, notifier: Notifier):通过构造函数注入端口,外部适配器在启动时被组装进来。
  5. def place_order(self, order_id: int, amount: float) -> str:用例方法,只调用端口,不出现任何具体库名。
  6. class MemoryOrderRepository(OrderRepository):内存适配器,测试时可以直接替换数据库实现。
  7. service = OrderService(repo, notifier)组合根,把适配器注入核心,这是依赖反转发生的时刻。

常见误区

误区我一直以为六边形六条边就要有六个端口,少一个就不标准。

正解六边形只是 Cockburn 画图时顺手选的多边形,为了对称好看,绝不是规定。你的系统可能只有一个入端口和两个出端口,甚至画成八边形也不影响架构思想。真正重要的是核心被端口包围,外部系统只能从这些端口进入。

为何常见:看到“六边形”这个词,很容易把图形字面化,误以为六条边是强制要求。

误区我已把 Controller 层叫做端口了,这应该就是六边形架构。

正解端口是核心内部定义的接口契约,属于核心的“门框”;HTTP Controller 只是门外的一个适配器,负责把 HTTP 报文翻译成端口调用。API 路径、状态码、请求体全是外部协议,沾满这些细节的代码不该出现在核心包里。

为何常见:日常开发中最常接触的就是对外 API,容易把“接口”这个词和端口混淆。

误区我在 Service 层写了 interface,再写个 impl,应该已经应用了六边形架构。

正解为了分层而随手拆接口,并不等于架构隔离。六边形要求核心不依赖任何外部框架和基础设施,接口必须由核心定义、适配器反向实现,并且依赖方向始终指向核心。如果接口定义在基础设施包里,核心反而去依赖基础设施,那就是挂羊头卖狗肉。

为何常见:接口是六边形的必要元素,但缺少“依赖反转”和“边界控制”的接口只是普通抽象。

误区既然它们都画圆圈、都强调依赖向内,那学一个就够了。

正解目标一致但侧重点不同。六边形架构强调端口和适配器的对称性,洋葱架构强调分层包裹,整洁架构强调用例和实体为核心。实际项目中它们可以互相借鉴,但生硬地认为“一样”会忽略不同语境下的术语差异。

为何常见:三者都在解决同一类问题,图形相似导致很多初学者不再深究概念边界。

误区为了保持纯净,核心最好什么都不 import。

正解核心不是“零依赖”,而是“不依赖外部变化”。它可以依赖语言标准库、自己的领域模型,以及自己定义的端口接口。如果连端口都不能依赖,核心就无法表达“我需要保存订单”这样的意图。

为何常见:把“不依赖框架”听成了“不依赖任何代码”,忽略了对领域内生概念的正常使用。

面试题

Q1. 六边形架构中,端口的主要作用是?

单选题 · easy

  • A.定义核心与外部交互的契约← 正确答案
  • B.直接操作数据库与缓存
  • C.负责渲染前端页面样式
  • D.统一管理所有环境变量

答案:A。端口是核心与外部世界之间的契约。就像点单口只负责传递标准菜单,不负责做菜,也不负责送餐;数据库和页面渲染只是外部世界的具体事务。

Q2. 六边形架构中,适配器的主要职责是?

单选题 · easy

  • A.把外部技术翻译成端口调用← 正确答案
  • B.让核心可以直接操作数据库
  • C.替外部系统实现全部业务规则
  • D.在核心内部生成接口定义

答案:A。适配器是翻译官。外部技术说方言,核心说普通话,适配器负责把 HTTP、SQL、消息等方言转成核心端口能理解的方法调用,而不是替核心思考业务。

Q3. 关于六边形架构的依赖方向,正确的是?

单选题 · mid

  • A.核心依赖数据库和框架
  • B.核心与适配器互相依赖
  • C.核心和适配器都依赖端口← 正确答案
  • D.端口依赖数据库实现

答案:C。依赖箭头要汇聚到端口上。核心调用端口,适配器实现端口,两者都不直接认识对方。这就像点单口连接服务员和后厨,双方只认口,不认脸。

Q4. 关于驱动端和被驱动端,正确的是?

单选题 · mid

  • A.左侧适配器属于被驱动端
  • B.被驱动端由核心主动调用← 正确答案
  • C.驱动端必须使用 HTTP 协议
  • D.每个端口只能有一个适配器

答案:B。被驱动端通常对应出端口,例如仓库接口,核心会主动调用它。左侧不一定就是被驱动端,协议也不是判断依据;左边的适配器通常是“驱动端”,主动把请求递进核心。

Q5. 替换数据库时,最常见需要修改的是?

单选题 · easy

  • A.核心业务用例
  • B.端口接口定义
  • C.数据库适配器← 正确答案
  • D.六边形本身形状

答案:C。核心只依赖端口,端口不关心实现。换数据库只需重写适配器,并在组合根重新注入,就像换供应商只会换采购员,绝不会改菜谱。

Q6. 洋葱架构和六边形架构的共同点是?

单选题 · hard

  • A.都必须有六个端口
  • B.依赖方向都指向核心← 正确答案
  • C.都由 Go 语言实现
  • D.都不允许使用数据库

答案:B。洋葱、整洁、六边形本质都是让外层依赖内层,内层不依赖外层。至于端口数量、语言、是否用数据库,都不是判定标准。

Q7. Go 语言中更符合六边形风格的做法是?

单选题 · hard

  • A.在基础设施包定义接口
  • B.核心包定义接口外部实现← 正确答案
  • C.适配器直接依赖具体库
  • D.用继承绑定核心和数据库

答案:B。端口应该由核心定义,这样核心才能只依赖端口。Go 接口是隐式满足的,外部实现类不需要显式声明 implements,但接口归属仍然影响依赖方向。

Q8. 下列哪些属于适配器该做的事?

多选题 · mid

  • A.解析 HTTP 请求为方法调用← 正确答案
  • B.将订单存储到 MySQL 数据库← 正确答案
  • C.决定订单金额的优惠规则
  • D.把查询结果渲染成 JSON← 正确答案

答案:ABD。解析 HTTP、访问 MySQL、渲染 JSON 都是外部技术细节,属于适配器。决定优惠规则是业务规则,应放在核心内部,不能交给适配器。

Q9. 为了保持核心可替换性,需要做到?

多选题 · extreme

  • A.核心只依赖端口接口← 正确答案
  • B.适配器在组合根注入← 正确答案
  • C.端口定义放在基础设施层
  • D.测试时替换为假适配器← 正确答案

答案:ABD。核心依赖端口、组合根注入、测试用替身,这三者共同保证核心不依赖具体实现。端口如果定义在基础设施层,核心就会反向依赖基础设施,破坏依赖方向。

Q10. 六边形架构带来的主要收益是?

多选题 · mid

  • A.业务核心不再依赖框架← 正确答案
  • B.数据库层可以更好替换← 正确答案
  • C.代码数量一定大幅减少
  • D.单元测试可以使用替身← 正确答案

答案:ABD。可测试、可替换、核心纯净是六边形架构的标志性收益。代码量并不必然减少,有时反而会多出接口和适配器的结构成本,这是值得的。