learn.wii.pub / software-design / design-patterns ← 学习站索引

DUOTONE FIELD GUIDE · 软件设计 · 双书合参

设计模式双书合参全图解

同一套 GoF 模式,两条讲法:一条从故事与坏代码重构走进去,一条从原则与头脑风暴走出来。这份手册把两本书摊开对读——每个模式一张卡:意图、双书场景、结构图、骨架代码、要点与坑,另附 7 条 OO 原则与「何时不用模式」。

PART 01 / BOOKS

两本书,怎么读

它们不是竞争关系,而是互补:《Head First》负责把「为什么」讲透,《大话》负责把「全」补齐。先看清楚各自的形状,再决定从哪本进门。

大话设计模式 · 程杰(2007)

  • 叙事:实习生小菜 + 导师大鸟的对话体小说,每章从一个「能跑但丑」的代码开始,一路重构到模式落地。
  • 语言:C#,示例朴素(收银台、简历、烧烤店),贴日常业务直觉。
  • 覆盖:GoF 23 个模式全部专章讲解,另把简单工厂当作开胃菜;这是它最大的不可替代性。
  • 强项:中文语感、面试向的记忆锚点(姚明翻译=适配器、抄试卷=模板方法)、浅拷贝/深拷贝、多线程单例这类工程细节。
  • 短处:原则的讲解穿插在章回里,系统性弱于 HF;对话篇幅大,信息密度偏低。

Head First 设计模式 · Freeman 夫妇(2004)

  • 叙事:图解 + 问答 + 练习 + 交叉字谜,每个模式先抛真实需求,再逼你撞上「继承解决不了」的墙。
  • 语言:Java,示例工程化(气象站、咖啡店、家庭影院、糖果机监控)。
  • 覆盖:14 个模式专章深讲(含复合模式 MVC);Bridge、Builder、职责链、享元、中介者、备忘录、原型、访问者 8 个只在附录各留一段;Interpreter 未收录。
  • 强项:OO 原则与模式的因果链——先有「找变化、封装变化」,才有模式;还有 push/pull、钩子、undo、双重检查这类进阶辨析。
  • 短处:纸张很厚,页数很多,23 个模式要凑齐得再配一本;Java 特有内容(RMI、动态代理)对其他语言读者是噪音。
本手册的合参读法:用 HF 打底——先口头禅、再原则、然后顺着策略/观察者/装饰三大件建立「模式的手感」;再用《大话》补齐 HF 没讲的 9 个模式,并借它的中文场景做记忆锚。下方每张模式卡的「双书场景」栏就是这个读法的产物。

PART 02 / PRINCIPLES

三句口头禅与七条原则

HF 全书反复念叨三句话;《大话》则把散落的原则收拢成章节。两者合起来,就是 GoF 那句「模式是针对特定场景的原则应用」的展开。

「找到变化, encapsulate it」
封装变化
把会变的部分抽出去单独封装,不变的部分不用理会变化。这是几乎所有模式的第一推动力——先问「什么在变」,再选模式。
「Program to an interface」
针对接口编程
变量的声明类型、方法的参数、字段的持有,尽量是抽象类型;「接口」是广义的——超类或接口都算。多态在运行期才决定具体行为。
「Favor composition」
多用组合,少用继承
继承是编译期焊死的强耦合,组合是运行期可插拔的弱耦合。鸭子的飞行行为不是「继承来的」,是「装进去的」。

单一职责原则SRP

就一个类而言,应该仅有一个引起它变化的原因。一个类承担两组职责,任何一组的变更都会波及另一组。

《大话》专章用游戏演示把「玩法规则」与「界面显示」拆开——改贴图不该惊动逻辑,反之亦然。HF未单列,但每个模式重构时都在做减法。

开放-封闭原则OCP

软件实体(类/模块/函数)应该对扩展开放,对修改关闭。需求变化时,加新代码,不改旧代码。

《大话》商场促销章:加一种促销是加一个子类,而不是去改 switch。HF视之为模式的靶心:「每个模式都为了让某类变化对关闭状态的部分免疫」。

里氏替换原则LSP

子类型必须能够替换掉它们的父类型,而程序的行为不变。继承不该「override 出奇怪」。

《大话》的名场面:士兵拿枪打仗,子类「玩具枪」把杀敌方法覆盖成「咔咔响」——语法上能替换,语义上全军覆没,这样的继承不如不继承。

依赖倒置原则DIP

高层模块不依赖低层模块,两者都依赖抽象;抽象不依赖细节,细节依赖抽象。「倒置」指的是依赖箭头从「指向具体」倒转为「指向抽象」。

《大话》以电脑装配讲解:主板/CPU/内存彼此只认接口,换件不用改板。HF第 4 章给出三条可操作判据:变量不持有具体类的实例;类不从具体类派生;方法不覆盖已由基类实现的方法。

接口隔离原则ISP

不应该强迫客户依赖它们用不到的方法。胖接口要拆成小而专的接口,接口属于客户,不属于实现类。

《大话》专章强调「接口的最小完备」:给只会飞的鸟实现 swim() 就是强迫症式肥胖接口。HF未单列,其精神融在「针对接口编程」与复合模式章节的接口裁剪里。

迪米特法则 / 最少知识LoD

一个对象只应与「直接朋友」说话:自己、成员、参数、自己创建的对象。别让一行代码跟一串对象点到底。

《大话》的课堂例:老师让体育委员清点女生——老师不该亲自遍历女生列表,跟谁干活就跟谁说话。HF第 4 章同名小节给出四条调用边界,并警告:遵守它会让包装类变多,别过度。

合成复用原则CRP

尽量使用对象的合成/聚合,而不是继承来达到复用目的。继承是白箱复用(暴露细节),合成是黑箱复用。

《大话》专章讨论「能 has-a 就别 is-a」,并借此引出桥接模式。HF把它浓缩成三句口头禅之一,策略/装饰/状态/代理全是它的落地。

把它们串起来HOW

三句口头禅是「手法」,七条原则是「法规」,23 个模式是「判例」。写代码时先想原则,模式常常自己浮现;直接套模式而不问变化在哪,是最常见的反模式。

这也是两本书共同的态度:《大话》结尾谈「模式之禅」的境界论,HF第 13 章整章讲「别为了模式而模式」。

PART 03 / CREATIONAL · 5

创建型:把 new 关进笼子

五种模式解决同一个问题的五个侧面:谁来 new、new 哪个、怎么 new、new 得像谁、new 几个。附一张「简单工厂」卡——它不是 GoF 23 之一,却是两本书共同的起手式。

创建型 · 附卡 《大话》专章 HF 第 4 章开篇 不计入 GoF 23

简单工厂Simple Factory

由一个工厂类凭参数决定实例化哪个产品子类,把 switch-case 的「选择职责」从客户代码里挪走。它不是 GoF 模式,却是最常用的「半个模式」。

大话 · 计算器与收银台

全书第一章:四则运算计算器先被写成 if-else 意大利面,重构为「运算类继承 + 简单工厂按运算符 new」。收银台促销一章再用它组合策略模式——工厂负责造,策略负责算。

Head First · 披萨店前传

第 4 章开场:PizzaStore 把 new Pizza(...) 的分支抽成 SimplePizzaFactory,授权店复用。HF 随即指出它的软肋——加新品要改工厂,于是顺势升级成工厂方法。

«abstract»Product ConcreteProductA ConcreteProductB SimpleFactory+create(type) Client «create» 调用
图解:实线空心三角 = 继承;虚线开放箭头 = 依赖(创建)
class CashFactory {                          // 大话:促销算法工厂(C# 风格)
  static CashSuper create(String type) {
    switch (type) {
      case "rebate": return new CashRebate(0.8);   // 打 8 折
      case "return": return new CashReturn(300, 100); // 满 300 返 100
      default:       return new CashNormal();
    }
  }
}
// HF 同款:SimplePizzaFactory——客户不 new 具体类,只问工厂要

要点与坑

  • 它违反 开闭原则:每加一种产品都要改工厂的分支——这正是两本书引出工厂方法的跳板。
  • 适合产品族稳定、扩展低频的场景;别为了「看起来有模式」就上工厂方法。
  • 《大话》的实战组合拳:简单工厂 + 策略模式(工厂造策略对象,上下文只管调)。
创建型 · GoF 《大话》专章 HF 第 4 章

工厂方法Factory Method

定义一个创建对象的接口,让子类决定实例化哪个类——把「new 的选择权」从分支语句下放到继承体系,对扩展开放。

大话 · 学雷锋

「大学生学雷锋」和「社区志愿者学雷锋」各自派出工厂:UndergraduateFactory、VolunteerFactory 都返回 LeiFeng 基类。加一类志愿者 = 加一对子类,老的代码一行不动。

Head First · 披萨加盟店

PizzaStore 把 orderPizza() 的流程锁死(prepare/bake/cut/box),只留 createPizza() 给子类——纽约店、芝加哥店各自决定口味。HF 点破本质:工厂方法让「使用」与「生产」解耦。

«abstract»Creator+factoryMethod() ConcreteCreator+factoryMethod() «abstract»Product ConcreteProduct 依赖抽象 «creates»
图解:左边是「造东西的人」的继承树,右边是「被造的东西」的继承树,两边平行生长
abstract class PizzaStore {                    // HF:把 new 钉在子类
  public Pizza orderPizza(String type) {          // 通用流程:近似模板方法
    Pizza p = createPizza(type);
    p.prepare(); p.bake(); p.cut(); p.box();
    return p;
  }
  protected abstract Pizza createPizza(String type); // 工厂方法:子类说了算
}
class NYPizzaStore extends PizzaStore {
  protected Pizza createPizza(String type) { return new NYStyleCheesePizza(); }
}

要点与坑

  • 与简单工厂的分水岭:简单工厂的分支长在工厂内部(改一处),工厂方法的分支消失、长成子类(加新类)。
  • orderPizza() 是天然的模板方法骨架——创建型和行为型经常这样咬合。
  • 代价:每加一种产品要加两个类(产品 + 创建者),小项目会显得过度设计。
创建型 · GoF 《大话》专章 HF 第 4 章

抽象工厂Abstract Factory

提供一个接口,创建一族相关或相互依赖的对象,而无需指定具体类。工厂方法管「一个产品怎么造」,抽象工厂管「一整套配套件」。

大话 · 换数据库

同一套系统要在 SQL Server 与 Access 之间切换:User、Department 两张表 × 两种数据库 = 两个「全家桶」工厂。再配合配置文件 + 反射,改一行 appSettings 就整体换库——换,就换一整个家族。

Head First · 披萨原料厂

纽约风味的面团配纽约的酱、芝加哥的深盘配芝加哥的奶酪——NYPizzaIngredientFactory 一口气交出一族原料(dough/sauce/cheese/veggie)。HF 顺手画出方法族:每个 createX() 都是一个工厂方法。

«abstract»AbstractFactory+createProductA() Factory1 Factory2 «abstract»ProductA ProductA1 ProductA2 «abstract»ProductB …同构的 B 族 «creates» «creates»
图解:一族产品 = 每个抽象产品各派生一份「配套件」,工厂只交族,不交散件
interface PizzaIngredientFactory {          // HF:一族原料的合同
  Dough createDough();  Sauce createSauce();  Cheese createCheese();
}
class NYPizzaIngredientFactory implements PizzaIngredientFactory {
  public Dough  createDough()  { return new ThinCrustDough(); }
  public Sauce  createSauce()  { return new MarinaraSauce(); }
}
// 大话:User/Department × SQLServer/Access 两族;加一张新表 = 所有工厂都要动

要点与坑

  • 命门:加一族新产品容易,加一个新产品类型难——所有工厂接口都要扩。HF 建议用工厂方法或原型缓解。
  • 判断口诀:换件要「成套换」就上抽象工厂;只换单件用工厂方法。
  • 实际框架里常退化为「约定 + 反射/依赖注入」:《大话》的 DataAccess 用反射选库,Spring 的 BeanFactory 家族同理。
创建型 · GoF 《大话》专章 HF 附录一段带过

建造者Builder

将一个复杂对象的构建过程与它的最终表示分离:同样的构建步骤,可以装配出不同的成品。工厂关心「造什么」,建造者关心「怎么造、按什么顺序造」。

大话 · 画小人

画一个「小人」:头、身体、左臂右臂、左腿右腿,顺序固定;瘦小人与胖小人只是笔触不同。PersonDirector 掌握画法顺序,ThinPersonBuilder / FatPersonBuilder 决定每笔怎么落——流程与手艺分家。

Head First · 附录一段

HF 只在附录给了 Builder 一小段:当构造对象要多个步骤、且希望同样的步骤能产出不同配置的对象时使用。今天的注脚:它已被 Lombok @Builder、Java 的 Builder 链式 API、Effective Java 条目 2 发扬光大——现代语言里它比 1994 年时更常见。

«abstract»Builder+buildPart() +getResult() ConcreteBuilder1 ConcreteBuilder2 Director+construct() 调用 Product «builds»
图解:指挥者只认识抽象建造者,装配顺序与具体手艺解耦
abstract class PersonBuilder {                 // 大话:画小人(C# 风格)
  protected final Graphics g;
  abstract void buildHead();  abstract void buildBody();
  abstract void buildArmL();  abstract void buildArmR();
  abstract void buildLegL();  abstract void buildLegR();
}
class PersonDirector {                       // 顺序固定在指挥者
  void createPerson(PersonBuilder pb) {
    pb.buildHead(); pb.buildBody();
    pb.buildArmL(); pb.buildArmR();  // 胖/瘦小人只换 Builder,顺序不变
    pb.buildLegL(); pb.buildLegR();
  }
}

要点与坑

  • 与工厂家族的分工:工厂「一口返回成品」;建造者「分步搭建,最后 getResult()」。
  • 省略 Director 的现代变体很常见:链式 setter(new Builder().head().body().build()),Director 的顺序约束由隐式调用顺序承担——顺序不受保护时,就退化成了「流式构造语法糖」。
  • 构造参数四个以上、参数类型重复、可选参数多——是 Builder(或具名参数语言特性)的信号。
创建型 · GoF 《大话》专章 HF 附录一段带过

原型Prototype

用原型实例指定创建对象的种类,并通过拷贝这个原型来创建新对象——当「造一个新对象」的成本高过「复印一份现成的」。

大话 · 投简历

投三份简历,工作经验栏不能一份改了三份跟着变——Clone() 默认浅复制,引用字段(工作经历)三份共享同一个对象,互相污染。由此引出深复制:引用字段逐层再拷。「浅拷深拷」是本章真正的主角。

Head First · 附录一段

HF 一句话定位:创建成本高或需要大量相似对象时,拷贝既有的便宜过 new。补充视角:当类层次在运行期才确定时,「拿现成对象复印」比「照着类名 new」更灵活——这也是 JS 原型链、Python copy/ deepcopy、Kotlin data class copy 的思想源头。

«interface»Prototype ConcretePrototype+clone() «clone()»复印自身
图解:客户端不 new 类,而是向原型要一份拷贝
class Resume implements Cloneable {         // 大话:简历
  private WorkExperience work;              // 引用字段:浅复制会共享!
  @Override protected Resume clone() {
    Resume r = (Resume) super.clone();      // 浅拷:字段逐位复制
    r.work = (WorkExperience) work.clone(); // 深拷:引用字段再拷一层
    return r;
  }
}

要点与坑

  • 面试高频:MemberwiseClone / Object.clone() 只做浅复制,嵌套引用要自己递归处理(或序列化往返一把梭)。
  • Java 的 clone() 陷阱:不调用构造函数、final 字段拷不了、Cloneable 是空接口却缺它就抛异常——Effective Java 条目 13 直接建议改用拷贝构造器/工厂。
  • 与抽象工厂搭档:用原型族替代工厂里的 new,可绕开「加产品类型要改工厂」的命门。
创建型 · GoF 《大话》专章 HF 第 5 章

单例Singleton

保证一个类只有一个实例,并提供一个全局访问点。三件套:私有构造器、静态私有实例、静态公有 getInstance()。

大话 · 一个国家一个皇帝

用「皇帝只有一个」起头,层层递进:懒汉式(用时再建,多线程下会建出俩)→ 加锁(每次都锁,慢)→ 双重锁定(锁前锁后各查一次)→ 静态初始化(饿汉,类加载即建)。把并发细节讲成了故事。

Head First · 巧克力工厂

ChocolateFactory 锅炉:fill → boil → drain,两个实例各干各的就会把巧克力做坏。HF 顺着同一条演进线走完,末尾补了 Java 彩蛋——枚举单例天生防反射、防序列化破坏。

Singleton -uniqueInstance ‹static› -Singleton() ‹private› +getInstance() ‹static› +其他业务方法() Client getInstance() 无论谁调用,拿到的永远是同一个实例
图解:构造器封死,唯一入口是静态方法
public class ChocolateBoiler {                  // HF:巧克力锅炉
  private static volatile ChocolateBoiler uniq; // 大话:双重锁定也要防指令重排
  private ChocolateBoiler() {}                  // 构造私有
  public static ChocolateBoiler getInstance() {
    if (uniq == null) {                          // 第一次检查:免锁快路径
      synchronized (ChocolateBoiler.class) {
        if (uniq == null) uniq = new ChocolateBoiler(); // 第二次检查
      }
    }
    return uniq;
  }
}

要点与坑

  • 单例常被叫「全局变量的马甲」:它确实降低了可见性成本,但也藏依赖、碍测试。替代品:依赖注入 + 容器管生命周期。
  • 破坏单例的三条路及对策:反射调私有构造(构造器里抛异常)、序列化复制实例(readResolve)、多类加载器(容器环境)。
  • 语言速记:C# 静态类 / Java 枚举或 holder 惯用法 / Kotlin object / Go sync.Once——本质都是「实例化动作只发生一次」的保证。

PART 04 / STRUCTURAL · 7

结构型:对象怎么搭在一起

不改变对象本身,改变对象的「组装方式」:接口转接、两维拆分、树形包装、加皮、封口、共享、隔层访问。这一族是「组合优于继承」的大本营。

结构型 · GoF 《大话》专章 HF 第 7 章

适配器Adapter

将一个类的接口转换成客户期望的另一个接口,让接口不兼容的类可以合作。适配器包裹已有的东西,替它「翻译」。

大话 · 姚明的翻译

姚明初到 NBA,听不懂教练的英文战术——不是姚明不会打球,是接口不通。翻译(Translator)把英文请求转成中文,姚明的「球技接口」原封不动。顺便讲清类适配器(多继承)与对象适配器(组合)的取舍。

Head First · 火鸡装鸭子

缺鸭子拿火鸡凑数:TurkeyAdapter 实现 Duck 接口,把 quack() 翻译成 gobble(),把「只能短飞」循环五次凑成「会飞」。HF 的准则:优先对象适配器——组合让适配器能服务被适配者的整个家族,还不踩多继承的坑。

«interface»Target Adapter Adaptee+gobble() +fly() 组合持有 Client 只认目标接口
图解:虚线空心三角 = 实现;适配器左手实现目标接口,右手持有被适配者
interface Duck { void quack(); void fly(); }
class Turkey {                                     // 火鸡:只会咯咯叫、短距离飞
  void gobble() { }  void fly() { }
}
class TurkeyAdapter implements Duck {          // HF:对象适配器
  private final Turkey turkey;              // 组合而非继承
  public void quack() { turkey.gobble(); }     // 接口翻译
  public void fly()   { for (int i = 0; i < 5; i++) turkey.fly(); }
}

要点与坑

  • 对象适配器 vs 类适配器:Java 单继承下类适配器很少用;C++/C# 多继承可用,但会把适配器焊死在具体被适配者上。
  • 与装饰的辨析(高频考点):两者都「包一层」,适配器改变接口,装饰保持接口只是加职责;外观则是对一群对象换一个新门面接口。
  • 现代注脚:老系统接新协议、第三方 SDK 包一层、HTTP 中间件转接——适配器无处不在,先想「翻译」,再想「重写」。
结构型 · GoF 《大话》专章 HF 附录一段带过

桥接Bridge

把抽象部分与它的实现部分分离,使两维(或多维)变化各自独立演化——把「M×N 种继承组合」炸开的类图,收缩成「M+N」。

大话 · 手机品牌 × 软件

手机品牌 M、N 两款 × 通讯录、游戏等软件,用继承做就是品牌下挂软件、软件下挂品牌,类数量相乘着涨。改法:品牌(Brand)聚合软件(Soft)接口——加品牌不动软件,加软件不动品牌。聚合那根线就是「桥」。

Head First · 附录一段

HF 的定义强调「抽象与实现解耦,使两者可以独立变化」,并给了组合模式的对照视角。日常判断法:当你在继承树里看到两个正交的变化方向(形状×渲染器、消息×通道、报表×导出格式),就该把其中一维从「继承」降级为「聚合」。

«abstract»Abstraction RefinedAbstraction «interface»Implementor ImplA ImplB 聚合(桥)
图解:左边变化维走继承,右边变化维走继承,中间一根聚合线架桥
abstract class HandsetSoft {                 // 大话:软件维
  abstract void run();
}
class HandsetGame extends HandsetSoft {
  public void run() { /* 运行游戏 */ }
}
abstract class HandsetBrand {                 // 品牌维:聚合软件
  protected HandsetSoft soft;
  void setSoft(HandsetSoft s) { soft = s; }   // 2 品牌 × 3 软件
  abstract void run();                          // = 6 种组合,零子类爆炸
}

要点与坑

  • 本质是合成复用原则(CRP)的结构化落地:一维变化用继承,另一维变化用组合。
  • 与策略模式同构(都是一个抽象持一个接口),区别在意图:桥接为「两棵树各自扩展」,策略为「运行期换算法」。
  • JDBC 是教科书案例:DriverManager(桥)之下,各数据库驱动各自实现,应用只面向 Connection 接口。
结构型 · GoF 《大话》专章 HF 第 9 章

组合Composite

将对象组合成树形结构,使客户对单个对象(叶)和组合对象(枝)的使用具有一致性——递归的世界里,叶和枝说同一种语言。

大话 · 公司组织架构

总公司—分公司—财务部/HR部:ConcreteCompany 是枝(能 Add 下级),HRDepartment/FinanceDepartment 是叶(不能 Add)。Display() 打印整棵树靠递归,每个节点只管自己那层缩进。

Head First · 餐厅菜单合并

煎饼屋(ArrayList)和餐馆(Array)的菜单要合成一份「煎烤全席」,还要支持甜点子菜单。MenuComponent 统一 getName()/print(),Menu 是枝、MenuItem 是叶。HF 顺势点出组合+迭代器是天生一对。

«abstract»Component+add() +operation() Leaf Composite-children[] children*
图解:Composite 内部装着一串 Component——枝里既有叶也有枝,递归由此而来
abstract class MenuComponent {                 // HF:菜单树
  void add(MenuComponent c) { throw new UnsupportedOperationException(); }
  abstract void print();
}
class Menu extends MenuComponent {                       // 枝:餐厅菜单/甜点子菜单
  private final List<MenuComponent> children = new ArrayList<>();
  void add(MenuComponent c) { children.add(c); }
  void print() { children.forEach(MenuComponent::print); } // 递归下钻
}

要点与坑

  • 透明性 vs 安全性的经典取舍:add() 放在基类,叶也得实现(透明但可能运行期炸);放只在 Composite(安全但客户要判型)。HF 选透明 + 抛异常,《大话》偏安全。
  • 识别信号:业务里出现「整体-部分」词汇(组织架构、文件系统、UI 容器、菜单、审批树)。
  • 搭档:迭代器遍历、访问者跨类型操作、装饰包节点。
结构型 · GoF 《大话》专章 HF 第 3 章

装饰Decorator

动态地给对象添加额外职责——装饰者与被装饰者实现同一接口并持有它,层层包裹、层层叠加,比继承灵活得多。

大话 · 小菜穿衣服

大T恤、垮裤、破球鞋……Finery(服饰)基类既「是人的一部分」又「能装下一个人」:Show() 先调里面那层的 Show() 再展示自己。《大话》用穿衣比喻点出要害——装饰顺序不同,结果不同(先穿西装再穿睡衣就出事故)。

Head First · 星巴兹咖啡

Beverage 为基类,CondimentDecorator 是调料装饰者:cost() = 自己的价格 + 内层饮料的 cost(),getDescription() 同样拼接。加调料不改饮料类,Double Mocha Soy Latte 就是 new Mocha(new Mocha(new Soy(new DarkRoast())))。HF 还埋了伏笔:java.io 就是装饰大军(BufferedReader 包 FileReader)。

«abstract»Component ConcreteComponent «abstract»Decorator-component 持有组件 DecoratorA+operation() DecoratorB+operation()
图解:两根线是精髓——装饰者「是」组件(继承),也「有」组件(聚合)
abstract class Beverage {                        // HF:咖啡
  abstract String getDescription();
  abstract double cost();
}
abstract class CondimentDecorator extends Beverage {   // 同类型 + 持有内层
  protected final Beverage beverage;                // 大话:Finery 穿衣服同构
  CondimentDecorator(Beverage b) { this.beverage = b; }
}
class Mocha extends CondimentDecorator {
  Mocha(Beverage b) { super(b); }
  String getDescription() { return beverage.getDescription() + ", Mocha"; }
  double cost() { return .20 + beverage.cost(); }            // 责任层层叠加
}

要点与坑

  • 为什么不用继承?继承是静态的、一次性的;装饰是运行期、可叠加、可任意排序的。但装饰会繁殖出大量小类,调试时栈也深(HF 的提醒)。
  • 与适配器、代理的辨析:装饰「同接口加料」;适配器「换接口」;代理「同接口控访问」。
  • 踩坑:装饰链中若某层忘了转发内层调用(如 getDescription 没拼内层),链条就断了——写装饰者时先写转发,再加料。
结构型 · GoF 《大话》专章 HF 第 7 章

外观Facade

为子系统中的一组接口提供一个统一的高层接口——不是加功能,是把「看个电影要按八个遥控器」简化成「一个 Home Theater 按钮」。

大话 · 基金

散户直接炒股要盯 N 支股票、债券、期货;买基金则由基金经理(Fund)替你打理一篮子——客户只面对一个「外观」,风险分散在内部。大鸟特别提醒:外观不屏蔽子系统,想深入的人随时可以绕过它。

Head First · 家庭影院

watchMovie() 一键:拉屏幕、开功放、设 DVD、开爆米花机……endMovie() 一键复原。HF 给出「最少知识原则」的落地:外观对象把复杂调用网收进自己体内,客户不再跟一串对象点到底。

Facade+watchMovie() Client SubsystemA SubsystemB SubsystemC 统一调度
图解:虚线 = 外观对子系统的依赖;客户只看得见外观
class HomeTheaterFacade {                         // HF:看电影一键化
  private final Amp amp; private final Dvd dvd; private final Screen screen;
  HomeTheaterFacade(Amp a, Dvd d, Screen s) { amp = a; dvd = d; screen = s; }
  void watchMovie(String movie) {
    screen.down(); amp.on(); dvd.on(); dvd.play(movie);   // 调用次序被封装
  }
  void endMovie() { /* 逆序关闭… */ }
}

要点与坑

  • 外观 ≠ 中介者:外观是单向简化入口(客户→子系统,子系统间不因此相识);中介者是双向交互中枢(同事间本来要互相喊话,改为只喊中介)。
  • 可以分层加外观,也可以给子系统不同侧面开多个外观;别把外观写成什么都塞的「上帝类」。
  • 工程里的外观:编译型 SDK 的 API 入口、微服务的 BFF 聚合层、第三方支付网关封装。
结构型 · GoF 《大话》专章 HF 附录一段带过

享元Flyweight

运用共享技术支持大量细粒度对象:把不变的「内部状态」抽出来共享,把随场景变的「外部状态」在调用时传入。

大话 · 网站共享

建 N 个企业展示网站:博客站、发布站、展示站的「代码与模板」(内部状态)其实一样,不同的只是「用户数据」(外部状态)。WebSiteFactory 按 key 缓存 ConcreteWebSite,同类型站点共用一个实例,User 调用时传进去。

Head First · 附录一段

HF 定义与《大话》一致,并提醒关键在状态拆分。注脚:Java Integer 的 -128~127 缓存池、字符串常量池、游戏里成千上万的粒子/树木渲染(GPU instancing 的 CPU 侧思想同源)都是享元。先拆状态,再谈共享。

«interface»Flyweight ConcreteFlyweight-intrinsicState FlyweightFactory-pool : Map+getFlyweight(key) 共享池 «复用»
图解:工厂维护池子;key 相同 → 拿回同一个共享实例
class WebSiteFactory {                             // 大话:网站共享
  private final Map<String, WebSite> pool = new HashMap<>();
  WebSite getWebSite(String key) {
    if (!pool.containsKey(key)) pool.put(key, new ConcreteWebSite(key));
    return pool.get(key);                           // 同 key → 同一实例
  }
}
// 内部状态(代码/模板)随实例共享;外部状态(用户 User)调用时作参数传入

要点与坑

  • 前置问题永远是「状态能不能拆干净」:内部状态一经共享就不可变,否则一个站点改模板,全网遭殃。
  • 与单例池的区别:享元是「按 key 多实例共享」,不是全局唯一;工厂 + 缓存是它的标准外壳。
  • 别提前享元:对象总量不大时,共享池的复杂度与哈希开销反而是负担。
结构型 · GoF 《大话》专章 HF 第 11 章

代理Proxy

为对象提供一个代理,控制对它的访问——代理与真实对象实现同一接口,在「转发调用」前后插入自己的逻辑。

大话 · 戴莉送礼物

小菜不好意思直接送 jiao娇娇 礼物,托「戴莉」代理去送:两人都实现 GiveGift 接口(都有 giveDolls/giveFlowers),Proxy 内部持有 Pursuit(本尊),调用原样转发——娇娇只见代理,不见本尊。

Head First · 糖果机监控

三种代理一网打尽:远程代理(RMI stub,本地对象替你摸远端糖果机)、虚拟代理(CD 封面先显示占位图,大图加载完再换)、保护代理(Java 动态代理实现相亲系统:本人能改兴趣,访客只能看)。最后落到反射 Proxy.newProxyInstance 的现场教学。

«interface»Subject +request() RealSubject Proxy-real : RealSubject 控制访问后转发 Client 面向 Subject
图解:客户以为在跟 Subject 说话,其实是代理在替 RealSubject 把门
interface GiveGift { void giveDolls(); void giveFlowers(); } // 大话:送礼接口
class Pursuit implements GiveGift { /* 男孩本尊 */ }

class Proxy implements GiveGift {          // HF:远程/虚拟/保护,同一副骨架
  private final Pursuit real;
  public void giveDolls() {                     // 「前后插手」的空隙就在这里:
    // 检查权限(保护)/ 确保已加载(虚拟)/ 联网转发(远程)
    real.giveDolls();
  }
}

要点与坑

  • 与装饰的辨析(必考):结构完全同构,差在意图与主动性——装饰是客户主动层层加料,代理是替真实对象把关,客户通常无感。
  • 家族成员:远程、虚拟、保护,再加智能引用(引用计数/延迟加载)、防火墙代理、Copy-On-Write 代理——名字不同,骨架同一。
  • 工程注脚:ORM 的懒加载、AOP 切面、RPC 客户端存根、CDN 回源,全是代理;Java 动态代理与字节码增强(ASM/CGLIB)是把这门手艺规模化的工具。

PART 05 / BEHAVIORAL · 11

行为型:对象之间怎么说话

算法怎么分发、请求怎么传递、状态怎么迁移、群体怎么协同——这一族占 GoF 十一席,是两书着墨最重的地方,也是日常重构里出场率最高的一族。

行为型 · GoF 《大话》专章 HF 第 8 章

模板方法Template Method

在父类中定义算法的骨架,把某些步骤延迟到子类——流程「何时做」钉死在基类,步骤「怎么做」留给子类。配好莱坞原则:别调用我们,我们会调用你。

大话 · 抄试卷

两个学生抄同一份试卷,题目(试卷类)一样,只有答案不同——把试题做成 TestPaper 基类,题干是模板方法,Answer1() 是抽象步骤,学生子类只填答案。复用的是流程,变化的是填空。

Head First · 咖啡与茶

咖啡和茶的冲泡流程几乎相同:烧水→冲泡→倒杯→加调料,区别只在「冲泡」用滤纸还是茶壶。HF 把 prepareRecipe() 上提为 final 模板方法,再引入钩子 hook(wantsCondiments()):子类不填空也能影响流程走向。

«abstract»AbstractClass+templateMethod() ‹final›#primitiveOp1/2() ConcreteClass1 ConcreteClass2
图解:模板方法留在基类;子类只覆盖被调用的原语操作(或钩子)
abstract class CaffeineBeverage {                     // HF:咖啡与茶
  final void prepareRecipe() {                    // 算法骨架锁死
    boilWater(); brew(); pourInCup();
    if (wantsCondiments()) addCondiments();       // 钩子:子类可选干预
  }
  abstract void brew();                          // 变化步骤留给子类
  abstract void addCondiments();
  boolean wantsCondiments() { return true; }        // 缺省钩子
}

要点与坑

  • 钩子 vs 抽象步骤:抽象步骤「必须填」,钩子「可以不填」——给子类留可选影响点,避免子类被迫覆盖一堆空方法。
  • 它与策略的辨析:模板方法用继承换流程中的片断,策略用组合换整个算法;前者静态、后者运行期可换。
  • 日常代码里它出镜率极高:Servlet 的 doGet/doPost、JUnit 的 @Before/@Test、AbstractList 的骨架实现——认出它们,就懂了「框架请你填空」是怎么回事。
行为型 · GoF 《大话》专章 HF 第 1 章(开门模式)

策略Strategy

定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换——算法的变化不再波及使用它的客户。

大话 · 商场促销

收银台三种算法:正常收费、打折、满减返利。CashContext 持有 CashSuper 抽象,促销算法做成子类——「打折」是策略,「换策略」不惊动收银台。《大话》还把策略+简单工厂的合体打了个样:客户端连 new 哪个策略都不用关心。

Head First · SimUDuck 鸭子

第 1 章开门模式:鸭子会飞会叫,但橡皮鸭不会飞、诱饵鸭不会叫——继承一改全炸,接口又导致代码重复。解法:FlyBehavior/QuackBehavior 拆成独立的算法族,「有一个」取代「是一个」,还能运行期换翅膀(FlyRocketPowered)。

Context-strategy «interface»Strategy 运行期可换 ConcreteStrategyA ConcreteStrategyB
图解:Context 聚合策略接口;菱形在 Context 一端,策略整体可拔插
interface CashStrategy { double acceptCash(double cash); }   // 大话:收银算法族

class CashContext {                               // HF:鸭子持 FlyBehavior 同构
  private final CashStrategy s;                 // 组合:算法是"装进去"的
  CashContext(CashStrategy s) { this.s = s; }
  double getResult(double cash) { return s.acceptCash(cash); }  // 只委托,不实现
}
// new CashContext(new CashRebate(0.8)).getResult(400) —— 换促销=换策略对象

要点与坑

  • 三句口头禅在这一个模式里全部现身:封装变化(促销/飞行)、面向接口(CashSuper/FlyBehavior)、组合优先(has-a)——这也是 HF 拿它开篇的原因。
  • 与 if-else 的关系:策略不是消灭分支,是把分支从业务代码挪到装配处(工厂/配置/DI 容器)。分支本身总得有人做。
  • 现代注脚:函数一等公民的语言里,「策略」常常就一个函数参数(Comparator、回调);类形式的策略在需要状态/多方法时才更划算。
行为型 · GoF 《大话》专章 HF 第 6 章

命令Command

将「请求」封装成对象,从而可以参数化客户、排队请求、记录日志、支持撤销——调用者只认命令对象,不认识真正的干活者。

大话 · 烧烤店

顾客不直接冲烤肉师傅喊「十串羊肉」,而是下单给服务员:订单(命令对象)记着烤什么,服务员(Invoker)攒一摞订单,按节奏 notify 厨师(Receiver),还能「这串不要了」撤销。 separating「点单的人」和「烤串的人」。

Head First · 万能遥控器

七个插槽的可编程遥控器:每个槽塞一个命令对象,按下即 execute()。空槽给 NoCommand(空对象,免判空);派对模式给 MacroCommand(一按全开/全关);撤销功能让每个命令自己记 undo——吊扇调速那节演示了状态回滚式 undo。

Invoker+setCommand() «interface»Command+execute() +undo() 持命令 ConcreteCommand-receiver Receiver+action() 调用 action() Client 组装并注入
图解:Client 造命令并塞给 Invoker;命令内部指向真正干活的 Receiver
interface Command { void execute(); void undo(); }

class LightOnCommand implements Command {
  private final Light light;                          // 真正干活的是接收者
  public void execute() { light.on(); }
  public void undo()    { light.off(); }                  // 撤销=反向操作
}
class RemoteControl {                                     // HF 遥控器:调用者只认命令
  private final Command[] onCmds = new Command[7];
  public void setCommand(int slot, Command c) { onCmds[slot] = c; }
  public void onButtonPushed(int slot)  { onCmds[slot].execute(); }
  public void undoButtonPushed(int slot) { onCmds[slot].undo(); }
}

要点与坑

  • 命令模式的价值清单:撤销/重做、任务队列(线程池把 Runnable 排队)、日志与事务(记下命令流,崩溃后重放)、宏命令组合。
  • 与策略的辨析:结构又是一模一样!差在意图——策略「选用哪种算法」,命令「把请求做成对象以便排队/撤销/日志」。
  • undo 三档:简单反向(off)、状态快照(吊扇记上一档,备忘录思想)、命令栈多步撤销。
行为型 · GoF 《大话》专章 HF 附录一段带过

职责链Chain of Responsibility

让多个对象都有机会处理请求,把它们串成一条链,沿着链传递,直到有人接手——发请求的人不知道谁会处理。

大话 · 加薪申请

请假/加薪申请逐级批:经理批小额,超了转总监,再超转总经理。每个 Manager 持有 superior 指针,handle() 里「能批就批,批不动就上交」。申请人只交一次表,不知道链的存在。

Head First · 附录一段

HF 一句话:沿链传递请求,解耦发送者与接收者。现代注脚最丰富的模式之一:Servlet Filter 链、中间件洋葱圈(Express/Koa)、事件冒泡与捕获、异常处理栈、审批流引擎——凡是「一层不行交给下一层」的地方都是它。

«abstract»Handler-successor+handleRequest() successor ConcreteHandler1 ConcreteHandler2
图解:Handler 持「下一个 Handler」——链是对象指针串出来的,不是配置写死的
abstract class Manager {                                // 大话:加薪审批链
  protected Manager superior;                          // 下一个处理者
  void setSuperior(Manager m) { superior = m; }
  abstract void requestApplications(Request req);     // 处理不了就上抛
}
// 经理:1 天以内;总监:5 天以内;总经理:10 天以内
// —— 链在哪一环断掉,客户端既不知道也不关心

要点与坑

  • 纯与不纯:GoF 原版允许处理者「处理一部分再继续传」(中间件就是这样:前置处理→next→后置处理)。
  • 坑:链太长或没人接,请求就「消失」——要么给默认终端处理者,要么显式返回 404/拒绝。
  • 变体:链的组装常用配置或装饰器风格(use(fn) 层层包裹),比手工 setSuperior 更顺。
行为型 · GoF 《大话》专章 HF 第 10 章

状态State

允许对象在内部状态改变时改变它的行为——把每个状态做成对象,状态自己知道该干什么、下一站去哪,Context 里的巨型 switch 就消失了。

大话 · 上班状态

同一句「写程序」,上午写、午后困、傍晚拼、深夜蔫——Work 类按 hour 分支判断状态,分支越积越多。重构:每种状态一个类,Work 持有当前 State,状态对象处理完自己把 Work 切到下一个状态。

Head First · 糖果机

糖果机四个状态(没投币/已投币/售出/售罄)×四个动作,if-else 组合出十几种分支,还容易卖出「无币糖果」。改法:NoQuarterState 等四个状态类各管各的动作,非法动作里报错。第 10 章还顺势讲了状态迁移交给状态对象 vs 交给 Context 的权衡。

Context-state / +request() «interface»State 委托当前状态 ConcreteStateA ConcreteStateB 迁移下一状态(由状态对象发起)
图解:Context 只存「当前状态」;状态间的箭头就是状态机里的迁移边
interface State { void insertQuarter(); void turnCrank(); }   // HF 糖果机

class HasQuarterState implements State {
  private final GumballMachine gumball;             // 状态认识机器
  public void turnCrank() {
    gumball.setState(gumball.getSoldState());           // 自己发起迁移
  }
}

要点与坑

  • 与策略的辨析(面试高频):结构仍是孪生,差在谁驱动切换——策略由客户从外面换;状态在对象内部自动迁移,状态对象知道彼此。
  • 状态类通常做成单例或无状态共享(Flyweight),迁移成本只是一次指针赋值。
  • 现代注脚:游戏 AI 的 FSM/行为树、订单状态机、协程的调度状态——业务上「状态机」需求出现时,先想 State,再考虑引入专门的状态机库。
行为型 · GoF 《大话》专章 HF 第 2 章

观察者Observer

定义对象间的一对多依赖:当 subject 状态改变,所有依赖于它的 observer 都得到通知——「出版-订阅」关系,双方松耦合。

大话 · 前台通风报信

同事趁老板不在看股票、看 NBA,前台/老板就是 Subject:谁登记谁被通知「老板回来了!」,关行情的关行情、切界面的切界面。同事类只实现 Update() 接口,前台根本不知道他们具体在干嘛——这就是解耦。

Head First · 气象站

WeatherData 一变,三块布告板实时刷新。HF 先实现一遍,再抛出「通知的时机该由谁触发」;然后讲 push(推)vs pull(拉):推模型把数据塞给观察者(接口胖),拉模型把自己递过去(观察者按需取)。末章还吐槽了 java.util.Observable 的设计(是类不是接口,没法复用)。

«interface»Subject+register/remove/notify ConcreteSubject «interface»Observer * ConcreteObserver-subject / +update() 拉模型:回头读主题状态
图解:Subject 聚合一串 Observer;通知一到,观察者也可反手来拉数据
interface Observer { void update(float temp, float humidity); }  // 推模型签名
interface Subject {
  void registerObserver(Observer o);  void removeObserver(Observer o);
  void notifyObservers();
}
class WeatherData implements Subject {                        // HF 气象站
  private final List<Observer> obs = new ArrayList<>();
  public void notifyObservers() { for (Observer o : obs) o.update(temp, humidity); }
  public void measurementsChanged() { notifyObservers(); }       // 数据一变只喊这一嗓子
}

要点与坑

  • 推 vs 拉:推快但接口随数据膨胀;拉灵活但观察者要认识主题。折中:事件对象里带个「变更摘要」。
  • 经典坑:通知时遍历中增删观察者(ConcurrentModificationException)、观察者互相触发造成通知风暴、忘记注销造成内存泄漏。
  • 它是最常用的模式之一:GUI 事件、MQ/消息总线、React 的响应式更新、Git hooks——「订阅」一词早已溢出模式圈。与中介者组合可做事件总线。
行为型 · GoF 《大话》专章 HF 附录一段带过

中介者Mediator

用一个中介对象封装一系列对象的交互——同事之间不直接互相喊话,只跟中介说,网状依赖收敛成星型。

大话 · 联合国安理会

美国和伊拉克互不直接通话,声明都递交给安理会(UnitedNationsSecurityCouncil)转发——Country 抽象只持有中介引用,中介持有各国引用并决定消息怎么路由。把「N 个国家两两建交」变成「N 个国家各认一个秘书处」。

Head First · 附录一段

HF 的提醒:中介者可能长成「上帝对象」,所有交互都挤在它体内,复杂度只是搬家不是消失。现代注脚:机场塔台、聊天室、事件总线(EventBus)、MVC 里的 Controller、微服务里的编排器——交互本来就是多对多时,中介者的集中管理才划算。

«abstract»Mediator ConcreteMediator-c1 -c2 «abstract»Colleague-mediator Colleague1 Colleague2 协调双方 只认识中介
图解:左树管中介,右树管同事;两棵树之间只有两根线——网状变星型
abstract class UnitedNations {                        // 大话:安理会
  abstract void declare(String msg, Country sender);
}
abstract class Country {
  protected final UnitedNations mediator;          // 同事只认识中介
  void send(String msg) { mediator.declare(msg, this); }
}

要点与坑

  • 与外观的分水岭:方向。外观是客户单向找子系统(简化入口);中介者是同事间双向交互的枢纽(替掉同事间的相互引用)。
  • 中介者与观察者常合体:中介者订阅各同事的事件,再决定通知谁——事件总线即两者的工业级合体。
  • 量变质点:同事 3、5 个时直接互相调用反而清楚;网状连线开始「糊」了再收进中介者。
行为型 · GoF 《大话》专章 HF 第 9 章

迭代器Iterator

提供一种方法顺序访问聚合对象中的元素,而不暴露该对象的内部表示——数组也好、链表也好,遍历的长相都一样。

大话 · 公交售票员

售票员从车头走到车尾挨个查票,不管你坐前排后排(内部结构)——她心里只有「还有没有下一个」和「下一个是谁」。书中顺势拆解了 C# 的 IEnumerator/foreach:语言级语法糖底下就是迭代器。

Head First · 两家餐馆菜单

煎饼屋用 ArrayList、餐馆用数组存菜单,女招待要写两套遍历。统一成 Iterator 接口后,printMenu() 只认 hasNext()/next()。HF 还埋了两个钩子:remove() 的「删除权」问题,以及「要不要为聚合自己写迭代器,还是交给内部集合」。

«interface»Iterator+hasNext() +next() ConcreteIterator-pos -aggregate «interface»Aggregate+createIterator() ConcreteAggregate «creates»
图解:聚合只交出迭代器,内部结构(数组/链表/哈希)永不露面
interface Iterator<T> { boolean hasNext(); T next(); }
class DinerMenuIterator implements Iterator<MenuItem> {
  private final MenuItem[] items;  private int pos = 0;   // 数组也能统一遍历
  public boolean hasNext() { return pos < items.length; }
  public MenuItem next()    { return items[pos++]; }
}
interface Menu { Iterator<MenuItem> createIterator(); }       // 聚合的合同

要点与坑

  • 今天它已被语言吸收:Java 的 for-each + Iterable、Python 的 __iter__/yield、C# 的 IEnumerator——「会遍历」成了语言的出厂能力,但你依然可能在自定义树/图/流上游标时手写它。
  • 并发坑:遍历中聚合被改,快照迭代器 vs fail-fast 迭代器行为不同(CME 只是 fail-fast 的表现)。
  • HF 的下一站就是组合模式:树形菜单 + 递归迭代器,两章连着读最顺。
行为型 · GoF 《大话》专章 HF 附录一段带过

备忘录Memento

在不破坏封装的前提下,捕获对象的内部状态,存在对象之外——日后可用该快照把对象恢复原样。

大话 · 打 Boss 前存档

打 Boss 前先存档:GameRole 的 vit/atk/def 打完就没了,Save() 把状态装进 RoleStateMemento,交给 Caretaker(游戏进度管理)保管;打输了 Load() 原样恢复。关键设计:备忘录的读写权只给发起人,Caretaker 只是个保险柜。

Head First · 附录一段

HF 点名它与命令模式联手:多级 undo 时,命令对象执行前拍快照,撤销时回放。现代注脚:游戏存档、编辑器撤销栈、浏览器历史、事务回滚(Rollback Segment / MVCC 旧版本)、虚拟机快照——「先存一份旧的」永远是最朴素的后悔药。

Originator-state+createMemento()/restore() Memento-state ‹私有› Caretaker 创建/读取(宽接口) 窄接口:只存不读
图解:双接口是灵魂——Originator 全权读写,Caretaker 只能保管
class GameRole {                                     // 大话:打 Boss 前存档
  private int vit, atk;                            // 状态私有,不开放 setter
  RoleStateMemento save()   { return new RoleStateMemento(vit, atk); }
  void recovery(RoleStateMemento m) { vit = m.getVit(); atk = m.getAtk(); }
}
class RoleStateCaretaker {                          // 管理者:只会存,不会读
  private RoleStateMemento memento;
}

要点与坑

  • 封装的关键是宽接口 vs 窄接口:同包/友元(C++)或嵌套类(Java)让 Caretaker 拿到的是「不透明盒子」。
  • 成本账:快照吃内存——大对象别整存,存增量或序列化压缩;频繁快照要考虑池化。
  • 与命令模式的配合:Command 负责「做什么/撤销做什么」,Memento 负责「撤销时恢复到哪」。
行为型 · GoF 《大话》专章 HF 附录一段带过

访问者Visitor

在不改变元素类的前提下,定义作用于这些元素的新操作——把「操作」从元素里搬出来,做成独立的访问者对象,靠双分派落到每个具体类型上。

大话 · 男人与女人

同样听到「成功」与「失败」,男人的反应和女人的反应不同——把「反应」抽象成 Action(成功/失败两个访问者),Person(Man/Woman)的 accept(action) 把自己交出去:action.getManConclusion(this)。加一种「反应」= 加一个访问者,Person 类不动。

Head First · 附录一段

HF 提醒适用面窄:元素结构要稳定——一旦 Man 之外再加个 Kid,所有 Visitor 的接口都得改。现代注脚:编译器 AST 上的各种 Pass(类型检查/代码生成/优化)、文档导出(HTML/PDF/Markdown)、规则引擎对异质集合的处理——「结构稳定、操作常加」时它无可替代。

«interface»Visitor+visitA(a) +visitB(b) ConcreteVisitor1 ConcreteVisitor2 «abstract»Element+accept(v) ElementA ElementB accept():双分派
图解:accept() 里那句 v.visitA(this) 是双分派的第一跳,运行期落到具体访问者的具体方法
interface Action {                                   // 大话:成功/失败时的反应
  void getManConclusion(Man m);
  void getWomanConclusion(Woman w);
}
abstract class Person { abstract void accept(Action v); }
class Man extends Person {
  void accept(Action v) { v.getManConclusion(this); }    // 双分派第一跳
}
// 加一种反应(如「恋爱」)= 加一个 Visitor 实现;Person 家族零改动

要点与坑

  • 双分派:第一次分派是 accept(visitor) 选元素类型,第二次是 visitor.visitX(this) 选操作类型——两者合起来才把「类型×操作」矩阵定位到一个方法。
  • 命门:元素家族不稳定就别用——加一种元素,天下所有 Visitor 接口都要动(和抽象工厂的「加产品难」对称)。
  • 与迭代器/组合搭档:遍历组合树,对每个节点跑 Visitor——AST Pass 的标准姿势。
行为型 · GoF 《大话》专章 HF 未收录

解释器Interpreter

给定一门语言,定义它的文法的一种表示,并定义一个解释器——终结符表达式管词,非终结符表达式管句法组合,递归求值。

大话 · 音乐解释器

简谱「O 3 E E 1」:音符(O/3/E)是终结符,音阶/节拍组合是非终结符。Expression 抽象的 interpret(Context) 递归下降,Context 存待解释的字符串——用最小例子演示「文法→表达式树→求值」三步。

Head First · 未收录

HF 全书没讲它——实用频率最低的 GoF 模式。真实工程里,手写递归下降/parser 生成器(ANTLR、Bison)、正则引擎、SQL 解析器、规则引擎(如 Drools/CEL)接管了它的地盘;它剩下的价值是理解表达式求值的教科书模型。

«abstract»AbstractExpression+interpret(ctx) TerminalExpression NonterminalExpression-children* * Context 解释时传入
图解:非终结符表达式聚合更小的表达式,整棵树递归 interpret
abstract class Expression {                      // 大话:音乐解释器
  abstract void interpret(Context ctx);
}
class TerminalNote extends Expression {           // 终结符:音符 O C E G
  void interpret(Context ctx) { /* 取出并演奏一个音符 */ }
}
class Scale extends Expression {                   // 非终结符:组合
  private final Expression[] parts;
  void interpret(Context ctx) { for (Expression e : parts) e.interpret(ctx); }
}

要点与坑

  • 什么时候自己上解释器:文法小、简单、变化不频繁;一旦文法复杂,parser 生成器/DSL 工具链几乎总比手搓类层次强。
  • 性能是它的老病灶:每条产生式一个类,递归层层开栈。
  • 面试定位:知道它、能说清终结符/非终结符与组合的关系,足够了。

PART 06 / CHEAT SHEET

意图速查:遇到什么,想什么

从「症状」反查模式的对照表。先读左边的真实处境,再翻到上面的模式卡——面试前的最后一眼也够用。

当你遇到……模式家族
这个对象全系统只该有一个单例 Singleton创建
不想把「new 哪个类」的分支写在客户代码里,且产品种类稳定简单工厂创建
客户/子类要自己决定造哪一种,且加新品不能动老代码工厂方法创建
换配置要「成套换」——一整套相关对象一起变抽象工厂创建
对象构造步骤多、顺序重要、同样的步骤要能装出不同成品建造者创建
造新对象太贵,而它和某个现成对象几乎一模一样原型创建
两套接口不兼容,要把旧的接进新的适配器结构
类图在两个正交维度上「乘法式」爆炸桥接结构
整体-部分的树形结构,叶和枝要一视同仁组合结构
要给对象运行期叠加职责,继承会类爆炸装饰结构
子系统调用太啰嗦,想要一个简化入口外观结构
海量细粒度对象把内存吃爆,其中大半状态相同享元结构
要替真实对象把门:懒加载、远程、权限、AOP代理结构
算法族要可以互换,不想让促销方式污染业务代码策略行为
一个变了,一堆都要知道(订阅/通知)观察者行为
请求要做成对象:排队、日志、宏、撤销命令行为
流程骨架固定,只有个别步骤各家不同模板方法行为
一件事多个处理者接力,谁接手客户端不关心职责链行为
行为随状态切换而变,switch(state) 越长越长状态行为
一群对象互相调用,依赖图画成了蜘蛛网中介者行为
要遍历聚合对象,又不想暴露内部结构迭代器行为
要存档/撤销:把状态快照存到对象外备忘录行为
对象结构稳定,对它们的操作却不断新增访问者行为
要解释一门自己定义的小语言(文法小而稳定)解释器行为
复合模式一瞥:HF 第 12 章用一场鸭子乐队演示 MVC 的诞生——View 借策略插拔表现、借观察者订阅模型变化、控件树是组合、适配旧系统用适配器。真实系统从来不是「一个模式」,而是几个模式各守一角。

PART 07 / WHEN NOT TO

何时不用模式

两本书在「劝退」上出奇一致:模式是给重复出现的问题用的,不是给所有问题用的。

HF 第 13 章的三个忠告

① 先简单,后模式。「不确定时,用最简单的实现先跑起来;等变化真的发生了,再重构引入模式。」这是 Refactoring to Patterns 的路线:模式是重构的目的地,不是设计的起跑线。

② 模式是词汇,不是教条。它最大的价值是给团队一套共同语言——说一句「这里用策略」,胜过十分钟的架构描述。但如果要为「用了模式」而加四个类,那就是本末倒置(anti-pattern:模式瘾)。

③ 记得你的上下文。GoF 成书于 1994:那时迭代器要手写、策略要造类、单例要自己锁。今天很多模式已被语言吸收(for-each、一等函数、模块系统、DI 容器)——原则比模式长寿,模式是原则在特定时代的解法。

第一境 · 套用照猫画虎:能认出场景、把模式代码抄对。容易犯的错是见谁都是锤子。
第二境 · 活用举一反三:知道每个模式付出什么代价,能拆能并,能在模式之间迁移。
第三境 · 忘用《大话》的收梢:手中无剑,心中有剑——写的代码里有模式的骨,没有模式的名。

检验学会的标准不是能背类图,而是回答得出:这里什么在变?为谁封装?代价是什么?答得上来,用不用模式都对;答不上来,套了模式也是错的。