18.4 分离接口

把接口放在与实现不同的包中,使高层策略可以依赖抽象而不依赖实现部署。

学习目标

  • 能解释接口包如何让高层策略依赖抽象,并让实现包在部署和技术变化时保持可替换
  • 能编写 TypeScript 接口、实现与依赖注入装配代码,验证接口包不反向依赖实现
  • 能根据变化轴、包边界和测试隔离收益,判断何时采用或拒绝分离接口

为什么 18.4 分离接口 值得单独学习

把接口放在与实现不同的包中,使高层策略可以依赖抽象而不依赖实现部署。 18.4 分离接口 的核心不是套用某个框架 API,而是回答:如何隔离变化、身份或特殊行为,让上层模式依赖稳定语义而非具体基础设施。在 订单系统基础设施替换 中,如果无法说清责任、状态和失败由谁承担,即使正常请求能够返回,架构决定也没有完成。

的重点不是把每个函数都抽象化,而是让变化方向停在一个可发布、可测试的接口边界。本页以 2024 年中文版公开目录限定 18.4 分离接口 的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写。它不复现原书正文、插图或代码;目录只决定“要讲什么”,这里的案例、实验、判断题和答案均为本课程原创。

先建立直觉:问题、机制与代价

18.4 分离接口 面对的具体压力是“调用方数量、实现变化率、替换范围与测试隔离成本”。它采用的机制可以概括为:把接口放在与实现不同的包中,使高层策略可以依赖抽象而不依赖实现部署。 机制带来的收益必须与新增间接层、同步责任或迁移成本同时记录,否则学习者只会得到一个没有拒绝条件的模式名称。

对 18.4 分离接口,优先比较的替代路线是:仅在确有变化轴或语义约束时引入网关、映射器、接口、值对象等构件。本页的通过条件是“能从高层模块替换一个实现,检查依赖方向,并证明接口包不反向引用实现”;若实验只能显示结果而不能指出 从何处越界,就不能据此选择 18.4 分离接口。

目录单元到教学证据

18.4 分离接口

18.4 分离接口 的学习边界里,18.4 分离接口 不是待背诵的目录词,而是用来检查“调用者”是否把责任交给正确对象。对 订单系统基础设施替换,学习者要记录 依赖方向 的可观察变化,并说明它何时支持或否定 18.4 分离接口。

专属设计案例:订单系统基础设施替换

把 订单系统基础设施替换 切成“调用者 → 抽象边界 → 适配机制 → 协作者 → 结果”五个观察点。18.4 分离接口 的设计草案必须写出谁拥有状态、谁作出业务决定、失败怎样传播,以及 依赖方向、对象语义、配置、测试隔离、表示转换 中哪个指标最先提示当前方案不再适用。

设计记录采用五个可换行字段:单元键为 poeaa24-pattern-44-separated-interface;模式族为 base;裁决是“能从高层模块替换一个实现,检查依赖方向,并证明接口包不反向引用实现”;观测项包括 依赖方向、对象语义、配置、测试隔离、表示转换;拒绝条件是“依赖方向 超过团队为 18.4 分离接口 设定的边界”。

配置不是生产框架语法,而是一张评审卡。对 18.4 分离接口 的任何实现都要能把运行证据重新映射到这张卡;如果更换 ORM、Web 框架或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。

三步交互实验

Separated Interface:接口独立成包,依赖指向抽象高层模块只 import 接口包接口包interface Payment charge(amount)实现包StripePayment依赖实现依赖方向规则:• 高层 → 接口包 ← 实现包:依赖始终指向抽象• 接口包不反向引用实现 • 可替换实现而不改高层代码(DI 注入)接口独立成包,高层与实现都依赖抽象,依赖方向不反转
分离接口将接口定义在独立包中,高层模块和实现包都依赖接口包, 依赖方向始终指向抽象,可替换实现而不改高层代码。

选择与拒绝矩阵

评审问题选择 18.4 分离接口 的证据应拒绝或改用其他方案的信号
责任调用者 到 结果 的所有者清晰抽象边界 可以绕过边界直接改写状态
变化依赖方向 的变化被局部吸收一次小改动同时触及 对象语义、配置、测试隔离
失败故障能在 结果 前被识别并回退只能看到最终错误,无法定位 依赖方向 的首个异常
替代已与同族候选比较并保留撤回路径因框架内置或团队习惯而跳过问题分析

代码实践:把端口放在高层可拥有的包

接口包只描述稳定业务语义,实现包可以使用 Stripe、银行 SDK 或测试替身。让部署选择留在边界。

export type PaymentPort = {
  charge(input: {
    orderId: string;
    cents: number;
  }): Promise<
    | { kind: "charged"; reference: string }
    | { kind: "retryable"; reason: "timeout" | "rate-limit" }
    | { kind: "rejected"; reason: "declined" }
  >;
};
 
export async function settleOrder(
  payment: PaymentPort,
  orderId: string,
  cents: number,
) {
  return payment.charge({ orderId, cents });
}
 
// 装配根决定使用哪一个实现;领域代码不 import Stripe SDK。
const payment: PaymentPort = new StripePayment(client);

接口包的验收条件是:高层只引用 PaymentPort,实现包实现它但不把实现类型反向暴露给高层,Fake 可以在测试中返回确定结果。若高层开始检查 StripePayment 的异常类,就说明分离接口已经被绕过。

常见误区

可验证练习

练习

问题 1:检查包依赖。 高层模块 import PaymentPort,接口包却 import StripePayment。这违反了什么边界?如何改?

问题 2:补一个 Fake。 请为 PaymentPort 写一个 Fake,使测试能模拟一次 retryable,而不启动真实支付服务。

问题 3:决定是否拆包。 一个稳定函数只有一个调用者,也没有独立部署或替换计划。请说明为什么应暂缓分离接口。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

分离接口

把接口放进独立包,使高层策略和具体实现都依赖抽象的组织方式。

依赖方向

模块之间允许引用的方向;本章要求高层和实现都指向接口包。

依赖注入

在装配根提供具体实现,让高层策略只依赖接口契约的方式。

接口契约

调用者能够依赖的输入、结果和错误语义,不包含供应商实现细节。

本章小结

掌握 18.4 分离接口 的标志不是记住定义,而是能在 订单系统基础设施替换 中解释“调用者 → 抽象边界 → 适配机制 → 协作者 → 结果”的责任链,利用 依赖方向、对象语义、配置、测试隔离、表示转换 作出可证伪的选择,并在出现“没有变化轴也预先增加全局入口和间接层”时明确拒绝当前实现。

前后导航

来源与改写范围

资料与写作方式声明

本章以Martin Fowler《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…