主题
界面
主题
《设计模式:创建型》讲"对象怎么造";本节讲**"对象怎么组合"**:
组合优于继承 —— 这句话不是审美,而是有硬数字的 ✓✓★ 本节的实测会算出一道题:5 个可选功能,继承要多少类、装饰器要多少类 ✓
先看本节的实测数字 ✓:
功能数 继承实现(2^n 个子类) 装饰器实现(n 个类)
1 2 个 1 个
3 8 个 3 个
5 32 个 5 个
6 64 个 6 个★ 一道真实的题目 ✓:
一个下单流程有 5 个"可选步骤":风控 → 优惠券 → 积分 → 日志 → 埋点
问:用继承实现"这些步骤的任意组合",要写多少个 OrderService 子类?
答:2^5 = 32 个 ✗✗
用装饰器:5 个装饰器类,运行时按需组装 ✓✓★ 为什么继承会爆炸 ✓:
继承是"编译期确定"的 ——要么有这个功能,要么没有 ✓
于是"所有组合"都必须写成独立的子类 ✗✗
★ 装饰器为什么是线性的 ✓✓:
装饰器是"运行时组装"的 ——每个功能一个装饰器,需要就套一层 ✓
new Logging(new Encryption(new Compression(base)))✓
★ 本节的实测(装饰器演示) ✓:
只有基础功能: 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 ——与包装顺序相反 ✓这是装饰器唯一的心智负担 ⚠️
| 维度 | 装饰器 | 代理 |
|---|---|---|
| 目的 | 增加功能 ✓ | 控制访问 ✓ |
| 谁决定包装 | 调用方(运行时组装)✓ | 框架(调用方无感)✓✓ |
| 层数 | 可叠加多层 ✓ | 通常一层 |
| 典型例子 | IO 流、Servlet Filter | Spring AOP、MyBatis Mapper |
★ 一句记忆法 ✓✓:
装饰器是"我自己要加"(给流加缓冲);代理是"别人偷偷加"(框架给你加事务) ✓
★ 动态代理的三个代价 ✓(本节实测):
| 代价 | 说明 |
|---|---|
① 每次调用走 InvocationHandler.invoke | 一次额外的方法派发 |
② method.invoke 内部有反射调用 | JDK 已大量优化,但仍比直接调用慢 |
③ 参数要装箱成 Object[] | 基本类型会有装箱开销 |
★★ 但要看绝对值 ✓✓:
多出的开销是"纳秒级",而一次数据库查询是"毫秒级" ✓
所以代理用于"横切关注点"(日志、事务、鉴权)非常划算 ✓✓
★ 这也解释了 Spring AOP 的两种代理 ✓:
| 情况 | 用的代理 |
|---|---|
| 目标类实现了接口 ✓ | JDK 动态代理(本节这种) |
| 没有接口 | CGLIB(生成子类) |
★ 所以 Spring 官方建议"面向接口编程" ✓✓
★ 与本项目已有内容的呼应 ✓: 本模块在 《Spring:AOP 与事务》 实测过"代理失效"的场景 —— 类内部方法直接互调(this.method())不走代理 ✗ 因为那时调用的是"原始对象自己",而不是代理对象 ✓✓
问题:新代码依赖一个接口,但已有的第三方类签名不同 ✗
interface PaymentGateway { // 我们定的接口
PaymentResult pay(PaymentRequest request);
}
class LegacyBankApi { // 第三方老接口,签名不兼容
String doTransfer(String account, long cents, String currency);
}✓ 适配器:
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)的区别 ✓:
| 模式 | 作用 |
|---|---|
| 适配器 ★★ | 转换接口(一个变另一个) |
| 外观 ★ | 简化接口(多个合成一个) |
| 模式 | 一句话 | 典型 |
|---|---|---|
| 装饰器 ★★★ | 运行时叠加功能 | IO 流、BufferedReader |
| 代理 ★★★ | 控制访问 + 横切增强 | Spring AOP |
| 适配器 ★★ | 转换接口 | 三方 SDK 封装 |
| 外观 ★ | 简化子系统调用 | 一个 Service 包住多个组件 |
| 桥接 ★ | 抽象与实现分离(两个维度独立变化) | JDBC 的 Driver |
| 组合 ★ | 树形结构统一处理 | 文件系统、组织架构 |
| 享元 | 共享细粒度对象 | 字符串常量池、连接池 |
★ 其中最常用的是前三个 ✓ ——其余在特定场景才出现 ✓
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(); }
}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); }
});★ 业务代码零侵入,就能给所有方法加指标 ✓✓
见 5.1 的 32 个类 ✗
代码:labs/design-patterns/src/main/java/cn/kb/learn/dp/DecoratorProxyLesson.java
运行:
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 可忽略 ✓ |
method.getName():观察它拦住了哪些调用 ✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 用继承做"功能组合" ✗✗ | 类数量 2^n 爆炸 ✓ |
| 2 | 搞混装饰器与代理 ✗ | 一个加功能、一个控访问 ✓ |
| 3 | 以为动态代理没有开销 ✗ | 有,但相对 IO 可忽略 ✓ |
| 4 | 在代理里捕获所有异常并吞掉 ✗✗ | 会掩盖问题 ✓ |
| 5 | 适配器里写了业务逻辑 ✗ | 它只该做"接口转换" ✓ |
| 6 | 忘记装饰器的顺序会影响语义 ✗ | 见 5.1 ✓ |
目标:亲手体会"2^n vs n"。
步骤:
DecoratorProxyLesson,确认功能数与类数的表 ✓;验收方式:能说出"你的项目里哪一处该用装饰器而不是继承"。
扩展任务:给"下单流程"实现可插拔的步骤链:
InputStream in = new BufferedInputStream(
new GZIPInputStream(
new FileInputStream("data.gz")));这段代码用了几层装饰?执行顺序是什么? 5. (设计)给"支付"设计适配层:业务只依赖 PaymentGateway 接口,底下接三家不同的三方 SDK,换供应商时零改动。
PaymentGateway,业务只依赖接口,切换只改装配处 ✓《设计模式:行为型》:策略、观察者与模板方法 —— 把"会变的行为"摘出来。