第 5 章 · 设计模式

设计模式(GoF,1994)是 23 个「面向对象场景的常见解法」的命名与整理。它名声的两极:入门者把它当咒语集(每个类都想套个模式),老手把它当「重构的目标形态」(先有烂代码,模式是重构出来的结果)。本章讲三种类型的代表模式 + 每个模式的「解决什么问题、什么时候别用」,以及把 23 个模式串起来的两条底层原则。Java 课第 8 章(接口、多态)是本章的语法基础。

5.1 两条底层原则:比 23 个模式值钱

开闭原则(OCP):对扩展开放,对修改关闭。新功能靠「加新类」而不是「改老代码里的 if」。改老代码意味着重新测试所有依赖它的路径;加新代码风险面小。

依赖倒置原则(DIP):高层模块不该依赖低层模块,两者都依赖抽象(接口)。业务逻辑依赖「存储接口」而不是 MySQL 类,换数据库不改业务;测试时注入假存储(第 8 章的 mock)。

还有单一职责(SRP,一个类只有一个变化的理由)和里氏替换(LSP,子类必须能安全地当父类用)。这四条原则(SOLID 的主干)是所有模式的「为什么」。模式 = 原则的常见落地姿势,先懂原则,模式就是「哦,这样落」,而不是死记 UML 图。

5.2 创建型:对象的出生地

单例(Singleton):全局只需一个的对象(配置中心、连接池)。Java 实现:私有构造 + 静态方法返回唯一实例。注意事项比模式本身多:并发下的懒加载(双重检查锁)、以及第 7 章的警告「单例 = 全局可变状态的温床,能不用少用」。

工厂方法(Factory Method):把「new 什么类」集中到一个工厂函数,调用方只见接口:

interface Storage { void save(String s); }
class FileStorage implements Storage { ... }
class DbStorage implements Storage { ... }

static Storage createStorage(String env) {
    return "prod".equals(env) ? new DbStorage() : new FileStorage();
}

调用方从此不 import 具体类(依赖倒置的落地),换实现改一处。它解决的问题真实且高频:构造逻辑复杂、实现可切换。

建造者(Builder):多参数对象的构造(十几个字段的可选参数),链式 setter 代替千参构造器,可读性和默认值管理双赢。

5.3 结构型:组合的方式

装饰器(Decorator):不改原类,包一层加功能。Java 的 IO 流(第 12 章)是教科书级实现:new BufferedReader(new FileReader(f)) 一层套一层,每层加一点能力(缓冲),组合爆炸被优雅化解。

代理(Proxy):与装饰器同形,但意图不同:控制访问(延迟加载、权限检查、远程调用桩)。MyBatis 的 Mapper、RPC 客户端的桩都是代理。区分装饰器与代理看意图:加功能 vs 管访问。

适配器(Adapter):把不兼容的旧接口包成新接口。真实世界的刚需:引入的第三方库接口和你的内部接口不一致,包一层,业务代码零改动。技术债的常用药。

5.4 行为型:职责的流转

观察者(Observer):一对多的依赖,被观察者状态变化时自动通知所有观察者。事件系统(Java 课第 11 章 GUI 的 listener)、消息队列、前端框架的响应式(第 8 章 Vue 的依赖收集),全是观察者或它的变体。它是「解耦通知」的标准答案:被观察者不知道观察者是谁。

策略(Strategy):一组可互换的算法,调用方注入其一。排序的比较器(Java 课第 11 章 qsort 的 cmp 回调,计组课第 11 章函数指针)就是策略;支付方式选择(微信/支付宝/银行卡)是业务版。它消灭的是「方法里 300 行 if-else」的怪物。

模板方法(Template Method):父类定流程骨架,子类填步骤。抽象类的经典用途(Java 课第 8 章 Shape.describe() 调抽象 area()):「流程固定、步骤可变」的场景。现代替代品是「组合优于继承」:把可变步骤做成函数参数传入,减少继承层级。

5.5 反面:模式的滥用

三种真实的滥用病:为了模式而模式(三层类结构包一个 if,半年后没人看得懂);简历驱动设计(微服务里硬塞 18 个模式);以及过早抽象(只有一个实现就抽接口+工厂,YAGNI 原则:You Aren't Gonna Need It)。模式的上手正确姿势:从重构出发——先有重复代码/巨石条件语句(第 7 章的坏味道),再识别「这像某个模式解决的痛」,重构到位。模式是重构的目的地,不是起点。

常见错误

  • 背 UML 图不懂意图。面试时能画单例,写代码时到处 new;先问「这个模式消灭的是什么坏味道」。
  • 单例当全局变量用。并发隐患(Java 课第 14 章)、测试不可替换、隐式依赖(函数签名里看不出它用了单例);能通过参数传入的依赖就传。
  • 继承替代组合。为了复用 300 行代码继承一个 2000 行的父类,父类一改全崩;GoF 原书就写了「组合优先于继承」,三十年了依然是最常违反的一条。
  • 接口爆炸。每个类都配接口+实现,90% 的接口只有一个实现且无替换预期;抽象要为真实的多态服务。

练习

  1. 动手:Java 实现策略模式:一个订单折扣计算器(普通会员 9 折 / VIP 8 折 / 满减),对比「switch 版」和「策略版」在「新增一种会员类型」时的改动范围(开闭原则的实证)。
  2. 动手:观察者模式:实现一个简易事件总线(EventBus:subscribe/publish),用它解耦「订单创建成功后:发短信、记日志、加分」三个动作,再想想这和第 10 章消息队列的关系。
  3. 手算:某项目有 42 处 new FileInputStream(Java 课第 12 章),需求变更要求统一加上重试逻辑。对比「逐处修改」和「引入工厂 + 装饰器」的改动点数量,写一段选择论证。
  4. 思考:组合优于继承这条原则,和「模板方法模式」(基于继承)是否矛盾?从「复用代码」与「复用契约」的区别出发,给一个「什么时候继承仍是正解」的判断标准。