Skip to content

设计模式:结构型 ​

S4-004建议学高级约 90 分钟JDK 21企业开发进阶 · 软件工程与设计模式

1. 本节定位 ​

《设计模式:创建型》讲"对象怎么造";本节讲**"对象怎么组合"**:

text
组合优于继承 —— 这句话不是审美,而是有硬数字的  ✓✓

★ 本节的实测会算出一道题:5 个可选功能,继承要多少类、装饰器要多少类 ✓

2. 学习目标 ​

  • 能算出"继承实现功能组合"的类数量爆炸(2^n) ★★★;
  • 知道装饰器的包装顺序与它的唯一心智负担 ★★;
  • 知道代理与装饰器的区别(控制访问 vs 增加功能) ★★★;
  • 知道动态代理的代价,以及为什么它对"横切关注点"仍然划算 ★★★;
  • 知道适配器解决什么问题 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看本节的实测数字 ✓:

text
  功能数    继承实现(2^n 个子类)    装饰器实现(n 个类)
  1         2 个                      1 个
  3         8 个                      3 个
  5         32 个                     5 个
  6         64 个                     6 个

★ 一道真实的题目 ✓:

text
一个下单流程有 5 个"可选步骤":风控 → 优惠券 → 积分 → 日志 → 埋点
问:用继承实现"这些步骤的任意组合",要写多少个 OrderService 子类?
答:2^5 = 32 个 ✗✗
用装饰器:5 个装饰器类,运行时按需组装 ✓✓

5. 核心概念 ​

5.1 组合优于继承:2^n vs n ★★★ ​

★ 为什么继承会爆炸 ✓:

继承是"编译期确定"的 ——要么有这个功能,要么没有 ✓

于是"所有组合"都必须写成独立的子类 ✗✗

★ 装饰器为什么是线性的 ✓✓:

装饰器是"运行时组装"的 ——每个功能一个装饰器,需要就套一层 ✓

new Logging(new Encryption(new Compression(base))) ✓

★ 本节的实测(装饰器演示) ✓:

text
  只有基础功能:        ORDER-DATA
  + 加密:              enc(ATAD-RE DRO)
  + 压缩 + 加密:       enc(zip(10B:ORDER-DATA))
  + 日志 + 压缩 + 加密: log[...us](enc(zip(10B:ORDER-DATA)))

★ 注意包装顺序 ✓✓:

最后包的在最外层,最先执行 ✓

new Logging(new Encryption(base)) → 先 log,再 enc ——与包装顺序相反 ✓

这是装饰器唯一的心智负担 ⚠️

5.2 代理 vs 装饰器:长得像,目的不同 ★★★ ​

维度装饰器代理
目的增加功能 ✓控制访问 ✓
谁决定包装调用方(运行时组装)✓框架(调用方无感)✓✓
层数可叠加多层 ✓通常一层
典型例子IO 流、Servlet FilterSpring AOP、MyBatis Mapper

★ 一句记忆法 ✓✓:

装饰器是"我自己要加"(给流加缓冲);代理是"别人偷偷加"(框架给你加事务) ✓

★ 动态代理的三个代价 ✓(本节实测):

代价说明
① 每次调用走 InvocationHandler.invoke一次额外的方法派发
② method.invoke 内部有反射调用JDK 已大量优化,但仍比直接调用慢
③ 参数要装箱成 Object[]基本类型会有装箱开销

★★ 但要看绝对值 ✓✓:

多出的开销是"纳秒级",而一次数据库查询是"毫秒级" ✓

所以代理用于"横切关注点"(日志、事务、鉴权)非常划算 ✓✓

★ 这也解释了 Spring AOP 的两种代理 ✓:

情况用的代理
目标类实现了接口 ✓JDK 动态代理(本节这种)
没有接口CGLIB(生成子类)

★ 所以 Spring 官方建议"面向接口编程" ✓✓

★ 与本项目已有内容的呼应 ✓: 本模块在 《Spring:AOP 与事务》 实测过"代理失效"的场景 —— 类内部方法直接互调(this.method())不走代理 ✗ 因为那时调用的是"原始对象自己",而不是代理对象 ✓✓

5.3 适配器:把不兼容的接口接起来 ★★ ​

问题:新代码依赖一个接口,但已有的第三方类签名不同 ✗

java
interface PaymentGateway {           // 我们定的接口
    PaymentResult pay(PaymentRequest request);
}

class LegacyBankApi {                // 第三方老接口,签名不兼容
    String doTransfer(String account, long cents, String currency);
}

✓ 适配器:

java
class LegacyBankAdapter implements PaymentGateway {
    private final LegacyBankApi api;

    @Override
    public PaymentResult pay(PaymentRequest request) {
        String raw = api.doTransfer(request.account(),
                request.amount().movePointRight(2).longValue(),  // ★ 元转分
                "CNY");
        return new PaymentResult("SUCCESS".equals(raw));          // ★ 结果格式转换
    }
}

★ 适配器的价值 ✓✓:

它把"第三方的不兼容"隔离在一个类里 —— 换供应商时只改这一个类,业务代码不动 ✓✓

★ 与外观模式(Facade)的区别 ✓:

模式作用
适配器 ★★转换接口(一个变另一个)
外观 ★简化接口(多个合成一个)

5.4 结构型模式速览 ★ ​

模式一句话典型
装饰器 ★★★运行时叠加功能IO 流、BufferedReader
代理 ★★★控制访问 + 横切增强Spring AOP
适配器 ★★转换接口三方 SDK 封装
外观 ★简化子系统调用一个 Service 包住多个组件
桥接 ★抽象与实现分离(两个维度独立变化)JDBC 的 Driver
组合 ★树形结构统一处理文件系统、组织架构
享元共享细粒度对象字符串常量池、连接池

★ 其中最常用的是前三个 ✓ ——其余在特定场景才出现 ✓

6. 递进示例 ​

6.1 最小示例:装饰器骨架 ​

java
interface Component { String operation(); }

class Decorator implements Component {
    protected final Component wrapped;
    Decorator(Component wrapped) { this.wrapped = wrapped; }
    @Override public String operation() { return wrapped.operation(); }
}

6.2 业务示例:用代理统一埋点 ​

java
OrderService proxy = (OrderService) Proxy.newProxyInstance(
        clazz.getClassLoader(), new Class[]{OrderService.class},
        (p, method, args) -> {
            long start = System.nanoTime();
            try { return method.invoke(target, args); }
            finally { metrics.record(method.getName(), System.nanoTime() - start); }
        });

★ 业务代码零侵入,就能给所有方法加指标 ✓✓

6.3 反例:用继承做功能组合 ​

见 5.1 的 32 个类 ✗

7. 完整可运行代码与实测 ​

代码:labs/design-patterns/src/main/java/cn/kb/learn/dp/DecoratorProxyLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl design-patterns compile
java "-Dstdout.encoding=UTF-8" -cp design-patterns/target/classes cn.kb.learn.dp.DecoratorProxyLesson

实测结论 ✓:

#结论
①继承实现功能组合是 2^n 增长,装饰器是线性 ✓✓
②装饰器可任意叠加,但要注意包装顺序(最后包的最先执行) ✓
③动态代理有纳秒级开销,相对毫秒级的 IO 可忽略 ✓

8. Debug 观察点 ​

  1. 交换两个装饰器的包装顺序:观察输出里 log 与 enc 的先后变化 ✓;
  2. 把代理换成直接调用:对比耗时(差异很大说明你的方法本身太轻,代理开销才显眼)★;
  3. 在代理里打印 method.getName():观察它拦住了哪些调用 ✓;
  4. 让目标类内部方法互调:观察代理是否被绕过(《Spring:AOP 与事务》)★★。

9. 常见错误 ​

#错误说明
1用继承做"功能组合" ✗✗类数量 2^n 爆炸 ✓
2搞混装饰器与代理 ✗一个加功能、一个控访问 ✓
3以为动态代理没有开销 ✗有,但相对 IO 可忽略 ✓
4在代理里捕获所有异常并吞掉 ✗✗会掩盖问题 ✓
5适配器里写了业务逻辑 ✗它只该做"接口转换" ✓
6忘记装饰器的顺序会影响语义 ✗见 5.1 ✓

10. 动手实验 ​

目标:亲手体会"2^n vs n"。

步骤:

  1. 跑 DecoratorProxyLesson,确认功能数与类数的表 ✓;
  2. 给某个接口加第 4 个、第 5 个可选功能,用继承写一遍(感受工作量)⚠️;
  3. 换成装饰器,对比代码量 ★★;
  4. 用动态代理给一个 Service 加"耗时统计",业务代码零改动 ★★★;
  5. 写一个适配器包住某个三方类,让业务代码只依赖自己的接口 ✓。

验收方式:能说出"你的项目里哪一处该用装饰器而不是继承"。

扩展任务:给"下单流程"实现可插拔的步骤链:

  • 用装饰器或责任链(《设计模式:行为型》 会讲责任链)✓;
  • 支持"按配置开启/关闭某一步" ✓;
  • 对比"用 if 判断要不要执行这一步"的写法 ★★

11. 练习题 ​

  1. 判断:装饰器和代理是一回事。(对 / 错)
  2. 填空:5 个可选功能用继承要 ______ 个类,用装饰器要 ______ 个类。
  3. 简答:为什么 Spring 建议"面向接口编程"?
  4. (代码阅读)
java
InputStream in = new BufferedInputStream(
                     new GZIPInputStream(
                         new FileInputStream("data.gz")));

这段代码用了几层装饰?执行顺序是什么? 5. (设计)给"支付"设计适配层:业务只依赖 PaymentGateway 接口,底下接三家不同的三方 SDK,换供应商时零改动。

参考答案(要点)
  1. 错。装饰器增加功能(调用方组装);代理控制访问(框架介入) ✓★
  2. 32 个;5 个 ✓
  3. 因为 JDK 动态代理只能代理接口 ✗ ——没有接口就要用 CGLIB 生成子类, 而 final 类/方法不能被代理 ⚠️★
  4. 三层装饰(Buffered / GZIP / File)✓ ——执行顺序由外到内:读文件 → 解压 → 缓冲 ✓
  5. 要点:每个三方 SDK 一个 Adapter 实现 PaymentGateway,业务只依赖接口,切换只改装配处 ✓

12. 掌握标准 ​

  • [ ] 能算出"继承实现功能组合"的 2^n 爆炸 ★★★;
  • [ ] 知道装饰器的包装顺序与它的唯一心智负担 ★★;
  • [ ] 能区分装饰器与代理 ★★★;
  • [ ] 知道动态代理有开销,但对横切关注点仍划算 ★★★;
  • [ ] 知道适配器与外观的区别 ★★;
  • [ ] 知道 Spring 用 JDK 代理还是 CGLIB 取决于有没有接口 ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《设计模式:行为型》:策略、观察者与模板方法 —— 把"会变的行为"摘出来。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引