端口与适配器把业务规则同外部技术细节隔开

说明入站端口、出站端口和适配器如何控制依赖方向,并处理事务、契约测试与边界粒度。

端口与适配器把业务规则同外部技术细节隔开

软件通常同时连接 HTTP、数据库、消息队列、文件系统和第三方服务。若业务逻辑直接依赖每一种客户端和框架类型,技术替换、测试和故障隔离都会牵动核心代码。端口与适配器架构把应用内部视为规则与用例,把外部世界通过明确接口接入。

这里的边界不是“控制器、服务、数据访问”三个目录的另一种命名。关键问题是依赖方向:核心定义它需要什么能力以及向外提供什么用例,具体技术在边界外实现这些契约。

端口表达应用边界上的意图

入站端口描述外部调用者能要求应用完成的动作,例如创建订单、计算路线或发布文章。它以业务输入、输出和错误语义表达,不暴露 Web 框架请求对象。HTTP 控制器、命令行或定时任务都可以成为调用同一端口的入站适配器。

出站端口描述核心为完成用例所需要的能力,例如保存聚合、查询汇率、发送通知。接口由使用方定义,返回值应符合业务所需,而不是完整复制数据库表或供应商响应。这样内部规则不需要知道 PostgreSQL、SMTP 或某个云 SDK 的细节。

Alistair Cockburn 对 Hexagonal Architecture 的原始思想强调应用能够由不同外部驱动,并通过可替换适配器连接外部资源:https://www.hexagonalarchitecture.org/ 。六边形只是边界示意,不规定必须有六个组件,也不等于某种固定包结构。

适配器负责协议与模型转换

入站适配器解析协议、认证上下文和外部数据,再调用端口。它可以做格式校验和权限入口检查,但业务不变量应由应用或领域层维护。否则同一用例从消息消费者调用时,可能绕过只写在控制器里的规则。

出站适配器实现端口,把业务对象转换成 SQL、HTTP 或事件协议。映射层要处理时区、金额精度、分页、错误码和重试语义。直接把 ORM 实体贯穿到核心,会让延迟加载、字段注解和持久化生命周期成为业务模型的一部分。

适配器不是无逻辑的薄壳。协议兼容、序列化、幂等键、供应商限流和错误分类都属于边界责任。需要避免的是把关键业务决策埋在某个适配器里,使换入口或存储后行为改变。

事务和一致性跨越边界时要显式设计

单个数据库事务可以围绕用例协调多个仓储,但远程调用和消息发布通常不在同一原子事务中。端口抽象不会消除分布式一致性问题。应用需要决定事务边界、重试、补偿、发件箱或异步工作流,并把可恢复错误与永久错误区分。

如果出站端口把“保存订单并发送消息”伪装成一个方法,具体适配器可能无法提供承诺的原子性。更清楚的设计是表达状态变化和待发送事件,由可靠发布机制处理。契约必须反映真实保证,不能用接口名称制造不存在的一致性。

读模型也不必强行通过领域聚合。报表或搜索查询可以有专用查询端口与投影,只要其所有权和数据新鲜度明确。架构目标是控制耦合,不是让每个字节都经过同一模型。

测试从核心行为扩展到边界契约

核心用例可以使用内存适配器或测试替身验证业务规则,但替身必须遵守真实端口语义。一个永不失败、立即一致的假仓储,无法暴露生产数据库中的唯一约束、并发和事务问题。核心测试之外仍需要适配器集成测试。

契约测试用于确认多个适配器对同一端口的行为一致。例如查不到对象时返回空值还是错误、分页顺序是否稳定、重复写入如何处理。对第三方 API,要用录制或沙箱验证字段、状态码和超时,同时避免把供应商短暂行为写成永久业务规则。

端到端测试保留少量关键路径,证明协议入口、依赖组装、持久化和外部交互能共同工作。测试金字塔的重点不是只做单元测试,而是让每种风险在最合适边界被发现。

边界粒度需要随业务复杂度调整

为每个简单函数创建端口会造成接口碎片和映射样板。端口应围绕稳定用例或外部能力,而不是围绕每个类。小型应用可先建立少数清楚边界,等技术变化、团队所有权或测试需求出现后再细分。

反过来,一个包含几十个不相关方法的通用仓储端口会成为耦合中心。接口隔离应让调用方只依赖所需操作。领域之间若拥有不同模型和变更节奏,应通过明确服务或事件契约交互,而不是共享内部存储适配器。

SEI 对软件架构的资料强调结构、关系和关键属性之间的系统性权衡:https://www.sei.cmu.edu/software-architecture/ 。采用端口与适配器要用可修改性、可测试性、性能和团队认知成本评价。对极低延迟路径,额外映射可能需要测量;对简单只读服务,完整领域层可能没有收益。

端口与适配器提供的是依赖治理方法。核心规则保持稳定,外部协议可以演进,故障与测试在边界上变得可见;如果只有目录名称改变而模型、异常和事务仍泄漏,架构并未真正形成。

资料依据