4.接口而非实现编程

4.接口而非实现编程

目录介绍
  • 1.先回答上篇思考题
    • [1.1 上篇遗留三道题](#1.1 上篇遗留三道题 "#11-%E4%B8%8A%E7%AF%87%E9%81%97%E7%95%99%E4%B8%89%E9%81%93%E9%A2%98")
    • [1.2 云迁移6万行代码](#1.2 云迁移6万行代码 "#12-%E4%BA%91%E8%BF%81%E7%A7%BB6%E4%B8%87%E8%A1%8C%E4%BB%A3%E7%A0%81")
    • [1.3 五次反转补锅](#1.3 五次反转补锅 "#13-%E4%BA%94%E6%AC%A1%E5%8F%8D%E8%BD%AC%E8%A1%A5%E9%94%85")
    • [1.4 灵魂五连问](#1.4 灵魂五连问 "#14-%E7%81%B5%E9%AD%82%E4%BA%94%E8%BF%9E%E9%97%AE")
  • 2.从一个搬迁切入
    • [2.1 上云搬迁案例](#2.1 上云搬迁案例 "#21-%E4%B8%8A%E4%BA%91%E6%90%AC%E8%BF%81%E6%A1%88%E4%BE%8B")
    • [2.2 改造的代价](#2.2 改造的代价 "#22-%E6%94%B9%E9%80%A0%E7%9A%84%E4%BB%A3%E4%BB%B7")
    • [2.3 问题的根因](#2.3 问题的根因 "#23-%E9%97%AE%E9%A2%98%E7%9A%84%E6%A0%B9%E5%9B%A0")
  • 3.原则的本质
    • [3.1 一句话理解](#3.1 一句话理解 "#31-%E4%B8%80%E5%8F%A5%E8%AF%9D%E7%90%86%E8%A7%A3")
    • [3.2 接口的双重含义](#3.2 接口的双重含义 "#32-%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%8F%8C%E9%87%8D%E5%90%AB%E4%B9%89")
    • [3.3 抽象层的价值](#3.3 抽象层的价值 "#33-%E6%8A%BD%E8%B1%A1%E5%B1%82%E7%9A%84%E4%BB%B7%E5%80%BC")
  • 4.重构图片存储
    • [4.1 三步重构法](#4.1 三步重构法 "#41-%E4%B8%89%E6%AD%A5%E9%87%8D%E6%9E%84%E6%B3%95")
    • [4.2 重构后代码](#4.2 重构后代码 "#42-%E9%87%8D%E6%9E%84%E5%90%8E%E4%BB%A3%E7%A0%81")
    • [4.3 演化对比图](#4.3 演化对比图 "#43-%E6%BC%94%E5%8C%96%E5%AF%B9%E6%AF%94%E5%9B%BE")
  • 5.支付渠道实战
    • [5.1 多支付场景](#5.1 多支付场景 "#51-%E5%A4%9A%E6%94%AF%E4%BB%98%E5%9C%BA%E6%99%AF")
    • [5.2 抽象统一接口](#5.2 抽象统一接口 "#52-%E6%8A%BD%E8%B1%A1%E7%BB%9F%E4%B8%80%E6%8E%A5%E5%8F%A3")
    • [5.3 调用方零修改](#5.3 调用方零修改 "#53-%E8%B0%83%E7%94%A8%E6%96%B9%E9%9B%B6%E4%BF%AE%E6%94%B9")
  • 6.设计的边界
    • [6.1 接口的代价](#6.1 接口的代价 "#61-%E6%8E%A5%E5%8F%A3%E7%9A%84%E4%BB%A3%E4%BB%B7")
    • [6.2 何时不需要](#6.2 何时不需要 "#62-%E4%BD%95%E6%97%B6%E4%B8%8D%E9%9C%80%E8%A6%81")
    • [6.3 反模式警示](#6.3 反模式警示 "#63-%E5%8F%8D%E6%A8%A1%E5%BC%8F%E8%AD%A6%E7%A4%BA")
  • 7.原则进阶
    • [7.1 与依赖倒置](#7.1 与依赖倒置 "#71-%E4%B8%8E%E4%BE%9D%E8%B5%96%E5%80%92%E7%BD%AE")
    • [7.2 与开闭原则](#7.2 与开闭原则 "#72-%E4%B8%8E%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99")
    • [7.3 与可测试性](#7.3 与可测试性 "#73-%E4%B8%8E%E5%8F%AF%E6%B5%8B%E8%AF%95%E6%80%A7")
  • 8.总结与延伸
  • 9.综合实战案例
    • [9.1 货运推送接需求](#9.1 货运推送接需求 "#91-%E8%B4%A7%E8%BF%90%E6%8E%A8%E9%80%81%E6%8E%A5%E9%9C%80%E6%B1%82")
    • [9.2 依赖实现的崩塌](#9.2 依赖实现的崩塌 "#92-%E4%BE%9D%E8%B5%96%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%B4%A9%E5%A1%8C")
    • [9.3 接口化三步重构](#9.3 接口化三步重构 "#93-%E6%8E%A5%E5%8F%A3%E5%8C%96%E4%B8%89%E6%AD%A5%E9%87%8D%E6%9E%84")
    • [9.4 类图与可测性](#9.4 类图与可测性 "#94-%E7%B1%BB%E5%9B%BE%E4%B8%8E%E5%8F%AF%E6%B5%8B%E6%80%A7")
    • [9.5 留下三道思考题](#9.5 留下三道思考题 "#95-%E7%95%99%E4%B8%8B%E4%B8%89%E9%81%93%E6%80%9D%E8%80%83%E9%A2%98")
  • 10.认知跃迁总结

# 章节 主题 关键案例 关键图表
01 面向对象设计思想 范式之别 双 11 雪崩订单系统 网状 vs 线性流程图
02 面向对象的特性 封装/抽象/继承/多态 钱包余额对不上排查 vtable 内存布局图
03 接口vs抽象类比较 何时选哪个 日志器 47 次改动复盘 决策树、模板时序图
04 接口而非实现编程 依赖抽象不依赖具体 云迁移 6 万行连锁修改 三步重构演化图
05 多用组合和少继承 继承陷阱与组合优雅 企鹅会飞继承翻车 类爆炸 vs 正交组合
06 设计原则全景图 SOLID + 23 模式 8000 行上帝类失控 SOLID 关系图
07 SOLID 原则案例汇 SOLID 在战场上 团队三年 SOLID 之争 滥用现场扫描图
08 反模式与坏味道 30+ 嗅觉清单 PR 被驳回 17 次 坏味道地图
09 重构十二式实战 12 个最常用手法 1200 行结算函数救援 重构节奏循环
10 可测试性设计 OOP 的硬验收 95% 覆盖率上线即崩 测试金字塔
11 DDD 与战术建模 从代码到业务 需求文档 70% 失真 限界上下文图
12 综合实战图片框架 完整系统实现 图片处理系统实战 完整架构图

1.先回答上篇思考题

1.1 上篇遗留三道题

上一篇 03.接口vs抽象类比较 末尾留下了三道题:

  • 🟢 AbstractStandardGateway.payfinal 的原因?
  • 🟡 多插拔点该怎么设计?
  • 🔴 合规检查等「横切关注」该怎么加进去?
本篇答案
🟢 不加 final → 子类可以重写「骨架流程」 → 模板失效,所有"公共动作"可被跳过
🟡 多插拔点 → 多个接口 。每个可插拔点提取为接口(RateLimiter/FinChecker/Retryer),抽象类只作为「接口装配者」
🔴 横切关注 = AOP / Decorator。不走接口,也不走抽象类 ,走装饰器链,这是本篇「依赖接口而非实现」的另一面

三道题本质指向一个问题:调用方不应该看到具体实现 。它不仅是「面向接口编程」,更是「面向能力契约编程」。

1.2 云迁移6万行代码

2022 年某互联网公司「去阿里云」:从阿里云 OSS 迁到腾讯云 COS。调研阶段估计「2 人月12 天完成」。实际:

erlang 复制代码
年中启动 → 预计Q3完成
实际进度:
Q3末完成 30%、上线出了 17 个由「调用 SDK 差异」引起的事故
Q4末完成 65%、为了兼容两个 SDK,加了中间适配层
Q1今未全完:还有3 个老服务依然依赖 OSS SDK,谁也不敢动

事后复盘 git,发现代码里出现了 62873 个 OSSClient 字符串 。这意味着是迫在几万处代码背后作修改,而非某个抽象中枢。

flowchart LR subgraph 均依赖实现的世界[均依赖实现] S1[OrderService] --> O[OSSClient] S2[ProductService] --> O S3[CdnService] --> O S4[还有62个服务] --> O end subgraph 中间有3年后[迁移 3 年后] T1[OrderService] --> AD[迁移适配层] --> COS[COSClient] T2[ProductService] --> AD --> OSS_LEGACY[仍未能切走 OSS] end

如果开发人从第一天就让 OrderService 依赖于 ImageStore 接口 而不是 OSSClient 实现 ,迁移只需改 1 个 Bean 配置。但实际付出的是 3 年 + 几十个工程师 + 17 个线上事故 的代价。

这不是「代码不够优雅」的问题,这是企业生死存亡级别的设计选择。

1.3 五次反转补锅

手术现场五次反转:

反转 1 :动动手把 OSSClient 改名为 ImageStore → 调用方代码看起来凅了,实际还是同一个类

反转 2 :抽出接口 ImageStore,让 OSSImageStore 实现 → 调用方仍然写 new OSSImageStore()耦合在 new 表达式里

反转 3 :发现调用方调用了以下 8 个 OSS 独有特性方法(bucketAcl/presignUrl/expireRule...),接口里要不要加?加了就不抽象,不加调用方要重写

反转 4:抽象 PresignUrl、ExpireRule 为独立接口 → 可能会出现 5个接口,调用方发现「代码里一堆 if instanceof XxxAware」。

反转 5 :最终落定,不是「接口越庞大越好」,而是「接口要面向「调用场景」,不面向「实现能力」」。多出来的能力要么不该走接口,要么该举升为独立能力接口。

这五个反转又一次揭示:「面向接口编程」不是难在语法,难在「选哪些能力走接口」

1.4 灵魂五连问

markdown 复制代码
Q1 ── 「依赖接口」与「依赖实现」在字节码层看有什么区别?
       └─→ §02 原则的本质
Q2 ── 不是已经用了抽象类了吗,为什么还要接口?
       └─→ §02.2 接口的双重含义
Q3 ── 哪些东西不该抽象为接口?
       └─→ §05 设计的边界Q4 ── 为什么「依赖接口」几乎等价于「可测性」?
       └─→ §06.3 与可测性
Q5 ── 「依赖抽象」与「依赖倒置」到底一样不一样?
       └─→ §06.1 与依赖倒置

2.从一个搬迁切入

2.1 上云搬迁案例

承接上一篇电商系统。系统里有图片处理模块,最初图片传阿里云:

java 复制代码
public class AliyunImageStore {
    public void createBucketIfNotExisting(String bucketName) { /* ... */ }
    public String generateAccessToken() { /* 阿里云鉴权 */ }
    public String uploadToAliyun(Image img, String bucket, String token) { /* ... */ }
    public Image downloadFromAliyun(String url, String token) { /* ... */ }
}

public class ImageJob {
    public void process() {
        AliyunImageStore store = new AliyunImageStore();
        store.createBucketIfNotExisting("ai_images");
        String token = store.generateAccessToken();
        store.uploadToAliyun(image, "ai_images", token);
    }
}

代码看起来"很正常"。

2.2 改造的代价

公司决定切换到自建私有云。只是换个存储,结果改造量惊人:

flowchart LR A[切换到私有云] --> B[新写 PrivateImageStore] B --> C[方法名 uploadToAliyun 该叫啥] B --> D[私有云没有 accessToken] B --> E[所有调用点都要改] C --> F[全局重命名] D --> G[流程不一样] E --> H[一处遗漏=线上 bug]
  • uploadToAliyun() 这个名字写死了"阿里云" → 调用点全要重命名;
  • 私有云不需要 accessToken → 调用流程也要改;
  • 散落在几十个文件里的 new AliyunImageStore() 一处改漏就翻车。

2.3 问题的根因

调用方依赖了具体实现,而具体实现把"阿里云"这个易变细节渗透进了:

  • 类名(AliyunImageStore)
  • 方法名(uploadToAliyun)
  • 流程结构(必须 generateAccessToken)

调用方与一个"会变的东西"绑死了,变化的代价就以乘法形式爆炸。


3.原则的本质

3.1 一句话理解

Program to an interface, not an implementation.

依赖抽象的契约,不依赖具体的实现。

flowchart TB subgraph 错误依赖 Caller1[调用方] --> Concrete1[阿里云实现类] end subgraph 正确依赖 Caller2[调用方] --> Iface[ImageStore 接口] Iface --> Concrete2[阿里云实现] Iface --> Concrete3[私有云实现] end

3.2 接口的双重含义

这里的"接口"有两层:

  1. 语法层 :Java interface、C++ 抽象类,具体的语法机制;
  2. 设计层 :一组协议/契约,描述"做什么"而非"怎么做"。

设计层的含义比语法层更广:只要你定义了稳定的对外能力清单,哪怕用普通类承载,本质也是"面向接口编程"。

3.3 抽象层的价值

复制代码
不稳定的实现       稳定的接口          调用方
   ↓                ↓                ↓
易变细节  ─封装─→  统一契约  ←─依赖─  关心 what 不关心 how

接口是"防火墙":把不稳定的底层与稳定的上层隔开,变化被堵在防火墙之内。


4.重构图片存储

4.1 三步重构法

flowchart LR Step1[步骤1<br/>方法名去掉实现细节] --> Step2[步骤2<br/>封装内部流程] Step2 --> Step3[步骤3<br/>抽出统一接口]
步骤 反例 正例
1.命名抽象 uploadToAliyun upload
2.流程封装 调用方拼装 token+upload 接口内部一次完成
3.接口提取 直接 new AliyunImageStore new ImageStore 接口

4.2 重构后代码

java 复制代码
// 1. 接口:只描述"做什么"
public interface ImageStore {
    String upload(Image image, String bucketName);
    Image download(String url);
}

// 2. 阿里云实现:把 token、bucket 等内部流程封进 upload
public class AliyunImageStore implements ImageStore {
    @Override
    public String upload(Image image, String bucketName) {
        ensureBucket(bucketName);
        String token = generateToken();    // 内部细节
        return doUpload(image, bucketName, token);
    }
    @Override
    public Image download(String url) {
        String token = generateToken();
        return doDownload(url, token);
    }
    private void ensureBucket(String n) { /* ... */ }
    private String generateToken() { /* ... */ }
    private String doUpload(Image i, String b, String t) { /* ... */ }
    private Image  doDownload(String u, String t) { /* ... */ }
}

// 3. 私有云实现:流程不同,但接口一致
public class PrivateImageStore implements ImageStore {
    @Override
    public String upload(Image image, String bucketName) {
        ensureBucket(bucketName);
        return doUpload(image, bucketName);   // 不需要 token
    }
    @Override
    public Image download(String url) { return doDownload(url); }
    private void ensureBucket(String n) { /* ... */ }
    private String doUpload(Image i, String b) { /* ... */ }
    private Image  doDownload(String u) { /* ... */ }
}

// 4. 调用方:永远只看到 ImageStore
public class ImageJob {
    private final ImageStore store;
    public ImageJob(ImageStore store) { this.store = store; }
    public void process(Image image) {
        store.upload(image, "ai_images");
    }
}

4.3 演化对比图

flowchart LR subgraph 重构前 J1[ImageJob] -->|new + 调用| A1[AliyunImageStore] A1 -.切换.-> P1[PrivateImageStore] J1 -.随之改.-> J2[改名+改流程] end subgraph 重构后 J3[ImageJob] --> I[ImageStore 接口] I --> A2[AliyunImageStore] I --> P2[PrivateImageStore] I --> S3[S3ImageStore<br/>未来扩展] end

切换实现 → 构造时换一行,调用方零改动。


5.支付渠道实战

5.1 多支付场景

电商系统支付:支付宝、微信、信用卡、PayPal......且未来还要加。

java 复制代码
// ❌ 反例:开关变成调用方的负担
public class CheckoutService {
    public void pay(Order order, String channel) {
        if ("alipay".equals(channel)) new Alipay().pay(...);
        else if ("wechat".equals(channel)) new Wechat().pay(...);
        else if ("paypal".equals(channel)) new PayPal().pay(...);
    }
}

加一种渠道 → 改 CheckoutService → 改测试 → 改调用方......坏味道全套齐了

5.2 抽象统一接口

java 复制代码
public interface PaymentGateway {
    PayResult pay(Order order);
    Refund    refund(String txId, Money amount);
}

public class AlipayGateway implements PaymentGateway { /* ... */ }
public class WechatGateway implements PaymentGateway { /* ... */ }
public class PaypalGateway implements PaymentGateway { /* ... */ }
classDiagram class PaymentGateway { <<interface>> +pay(Order) PayResult +refund(txId, amount) Refund } PaymentGateway <|.. AlipayGateway PaymentGateway <|.. WechatGateway PaymentGateway <|.. PaypalGateway CheckoutService --> PaymentGateway

5.3 调用方零修改

java 复制代码
public class CheckoutService {
    private final Map<String, PaymentGateway> gateways;  // 注入

    public CheckoutService(Map<String, PaymentGateway> gateways) {
        this.gateways = gateways;
    }

    public void pay(Order order, String channel) {
        PaymentGateway g = gateways.get(channel);
        if (g == null) throw new UnsupportedChannelException(channel);
        g.pay(order);
    }
}

新增 PayPal → 只加一个 PaypalGateway + 注入 Map,业务层完全不动。

java 复制代码
// 第 1 步:新建实现类(20 行)
public class PaypalGateway implements PaymentGateway {
    public PayResult pay(Order order)   { /* 调 PayPal API */ }
    public Refund    refund(String txId, Money amount) { /* 调 PayPal 退款 */ }
}

// 第 2 步:在初始化处注册一行,CheckoutService 零修改
gateways.put("paypal", new PaypalGateway());

// 第 3 步:调用方 order.pay("paypal") → CheckoutService 仍然是那 12 行代码

三周后 PM 说"接 Stripe"。再来一次:加类 + 加一行注入,其余所有文件不动。


6.设计的边界

6.1 接口的代价

抽象不是免费的:

代价 说明
类爆炸 每个实现一个接口 → 类数量翻倍
间接调用 多一层调度,调试栈更深
阅读负担 看到接口要再找实现
过度设计 永远只会有一种实现的也加接口,纯负担

6.2 何时不需要

不是所有地方都要"基于接口"。判断标准:这个能力未来可能被替换吗?

情况 是否抽接口
一次性脚本
内部工具类(StringUtil)
业务实体(Order)
三方依赖(支付/消息/存储)
跨模块协议
易变策略(折扣/风控)

6.3 反模式警示

反推接口陷阱,先写实现类,再把它的方法照搬成接口:

java 复制代码
// ❌ 把 generateAccessToken 也放进接口 → 接口被实现污染
public interface ImageStore {
    String upload(...);
    String generateAccessToken();   // 这是阿里云的细节,泄漏了
}

接口要"自上而下"设计,先想清楚调用方需要什么能力 ,才决定接口长什么样,绝不能由实现反推


7.原则进阶

7.1 与依赖倒置

依赖倒置原则(DIP):高层不依赖低层,二者都依赖抽象

"接口而非实现编程"是 DIP 的具体落地手段:

flowchart LR H[高层模块<br/>OrderService] --> I[抽象<br/>PaymentGateway] L[低层模块<br/>AlipayGateway] --> I

依赖关系从"高层 → 低层"反转为"两侧 → 抽象",故得名"倒置"。

7.2 与开闭原则

开闭原则(OCP):对扩展开放,对修改关闭

复制代码
新增 PayPal → 加新类(扩展开放)
不改 CheckoutService(修改关闭)

接口编程是 OCP 的入场券,没有抽象层,扩展就只能靠改旧码。

7.3 与可测试性

java 复制代码
// 单测时注入假实现
class FakeGateway implements PaymentGateway {
    public PayResult pay(Order o) { return PayResult.success("fake_tx"); }
    public Refund refund(String t, Money m) { return Refund.ok(); }
}

@Test
void test_checkout() {
    CheckoutService svc = new CheckoutService(Map.of("fake", new FakeGateway()));
    svc.pay(order, "fake");
    // 验证业务逻辑,不需要真支付宝
}

没有接口就没有 Mock,没有 Mock 就没有可靠单测。可测试性是接口编程的副产品,也是衡量设计质量的硬指标。


8.总结与延伸

关键点 内容
一句话 依赖抽象,不依赖实现
解决的痛 实现切换的连锁修改
三步重构 命名抽象 → 流程封装 → 接口提取
边界 仅一种实现且不会变 → 不必抽
配套原则 DIP、OCP、可测性

到这里,你已经能写出"调用方依赖接口"的代码。但接口/抽象类只是组装代码的方式之一,"继承"也常被用来做扩展点。问题是:继承用多了会带来什么?

下一篇 05.多用组合和少继承 将揭示继承的暗坑,并展示组合如何成为更优解。


9.综合实战案例

主线接力,03 篇为支付网关选了抽象,本篇要让调用方发生根本性转变。

9.1 货运推送接需求

PM 给出需求:

markdown 复制代码
1. 订单发货后需发货运信息给用户
2. 支持三家快递:顺丰 / 中通 / 京东物流
3. 只主要调用他们接口查物流状态
4. 并将状态变化推送给用户(短信/公众号/站内信)
5. 后期要接亲邮、万国邮政、DHL

9.2 依赖实现的崩塌

工程师第一版:

java 复制代码
public class ShippingService {
    private final SfClient   sf   = new SfClient();    // 顺丰 SDK
    private final ZtoClient  zto  = new ZtoClient();   // 中通 SDK
    private final JdLogClient jd  = new JdLogClient(); // 京东物流 SDK

    public void notify(Order order) {
        TrackInfo info;
        switch (order.carrier()) {
            case SF:  info = sf.queryTrack(order.waybill()); break;
            case ZTO: info = zto.track(order.waybill());     break;
            case JD:  info = jd.getStatus(order.waybill());  break;
            default: throw new IllegalStateException();
        }
        // 推送逻辑同样是 switch
        switch (order.user().pushPref()) {
            case SMS: smsClient.send(...); break;
            ...
        }
    }
}

三个问题一起出现:

现象 本质
接亲邮 → 多一个 case、多一次 if OCP 违反
单测时发现 SDK 依赖真实网络 → 无法 mock 可测性崩塌
ShippingService 同时抱住 3 个 SDK、3 个推送渠道、业务三个职责 SRP 违反

9.3 接口化三步重构

第 1 步:提炼能力接口,调用方只表达「我要查物流」,不表达「我使用顺丰」:

java 复制代码
public interface CarrierTracker {
    TrackInfo track(String waybill);
}
public interface UserNotifier {
    void notify(User u, Notice n);
}

第 2 步:调用方只认识接口

java 复制代码
public class ShippingService {
    private final Map<Carrier, CarrierTracker> trackers;  // 类型 → 实现
    private final List<UserNotifier> notifiers;            // 多推送渠道同时

    public ShippingService(Map<Carrier, CarrierTracker> t, List<UserNotifier> n) {
        this.trackers = t; this.notifiers = n;
    }

    public void notifyTrackChange(Order order) {
        TrackInfo info = trackers.get(order.carrier()).track(order.waybill());
        Notice n = NoticeBuilder.from(info);
        notifiers.forEach(x -> x.notify(order.user(), n));   // 多推送渠道
    }
}

第 3 步:实现接入,每个 SDK 一个实现类、Spring 自动装配:

java 复制代码
@Component class SfTracker  implements CarrierTracker { @Override public TrackInfo track(String w) { return ... ; } }
@Component class ZtoTracker implements CarrierTracker { ... }
@Component class JdTracker  implements CarrierTracker { ... }
@Component class SmsNotifier   implements UserNotifier { ... }
@Component class WechatNotifier implements UserNotifier { ... }

接一家亲邮?@Component class YouzhengTracker 加 1 个 @Bean 注册,ShippingService 一行不改。这才是原则价值的最直接体现。

9.4 类图与可测性

classDiagram class ShippingService { -trackers Map -notifiers List +notifyTrackChange(order) } class CarrierTracker { <<interface>> +track(w) TrackInfo } class UserNotifier { <<interface>> +notify(u, n) } ShippingService --> CarrierTracker ShippingService --> UserNotifier CarrierTracker <|.. SfTracker CarrierTracker <|.. ZtoTracker CarrierTracker <|.. JdTracker UserNotifier <|.. SmsNotifier UserNotifier <|.. WechatNotifier

现在写单测变得近乎微笑:

java 复制代码
@Test void notifyShouldFanOutToAllChannels() {
    var fakeSf  = mock(CarrierTracker.class);
    when(fakeSf.track("w1")).thenReturn(new TrackInfo("已发货"));
    var fakeSms = mock(UserNotifier.class);
    var fakeWx  = mock(UserNotifier.class);

    var svc = new ShippingService(Map.of(Carrier.SF, fakeSf), List.of(fakeSms, fakeWx));
    svc.notifyTrackChange(orderWithSf("w1"));

    verify(fakeSms).notify(any(), any());
    verify(fakeWx).notify(any(), any());
}

这就是接口编程的隐藏复利:单测不需要任何真实 SDK / 真实网络 / 真实手机。它预告了第 10 篇《可测性设计》。

9.5 留下三道思考题

答案在第 05 篇开头揭晓。

🟢 易 :为什么我们把 trackers 写成 Map<Carrier, CarrierTracker> 而不是 List<CarrierTracker>?二者语义有什么本质差别?

🟡 中SfTracker 里要调用顺丰 SDK,而 SDK 包含一个 SfApiClient 该怎么设计?是「SfTracker 继承 SfApiClient」还是「SfTracker 含有一个 SfApiClient 字段」?

🔴 难 :如果「顺丰」、「中通」、「京东物流」他们 80% 共同逻辑 (例如身份鉴权、重试、限流)你会怎么复用?是抽公共抽象类还是包装一个 RestApiBase 字段?提示:这道题就是下一篇《多用组合少用继承》的核心争论。


10.认知跃迁总结

回到云迁移 6 万行代码那件事。如果从代码的第一天,调用方看到的是 ImageStore 接口而不是 OSSClient ,迁移可能只需要一个 @Bean 配置、三个工作日、一个工程师。

一句话:不是「有了接口才能切换实现」,是「所有调用都锁定在接口,实现才脱离了环境锁定」。

接口不是为了抽象而抽象,是为了「年后的你」能走。

下一篇 05.多用组合和少继承,你会发现:接口能解决「调用方锁死」,但解不了「多个能力拼装」。后者需要 OOP 最被滥用也最被误解的机制:继承 vs 组合。

相关推荐
梦梦代码精1 小时前
基于ThinkPHP6 + Vue3的家政预约系统全解析:从LBS定位到自动派单的完整实现
java·docker·开源·php·代码规范
wear工程师1 小时前
@Transactional 标了却没开事务?先看这次调用有没有经过代理
java·spring
Joey_friends1 小时前
指纹authenticate流程图
android·java·c++
2601_954526751 小时前
【硬核架构】打破 IT 与 OT 的数据孤岛!基于 Rust 异步协程的工业 IoT 网关重构,兼谈顶级自动化智能仪表厂家选型指南
物联网·架构·rust
一缕清烟在人间1 小时前
HarmonyOS应用开发实战:萌宠日记 - 生命周期在萌宠日记中的实践
后端
Conan在掘金1 小时前
鸿蒙 ArkUI 组件深水区:Span 富文本嵌套,一行 Text 塞下「红蓝斜下大」五样式 + 动态高亮
后端
卷无止境1 小时前
Python 类型注解与运行时反射:从原理到工程实践
后端·python
郑州光合科技余经理1 小时前
家政O2O平台解析:从0搭建上门预约小程序解决方案
android·java·开发语言·前端·小程序·架构·php
耀耀_很无聊2 小时前
13_Spring Boot 3.5.8 + Redisson 3.45.1 导致 Sa-Token 登录 StackOverflowError
spring boot·后端·bootstrap