设计模式复习课

[TOC]

考试答题提醒

模式要记得别名的英文

设计题先写出来用哪几个设计模式

模式体现了什么设计原则,首先面向可维护性、可复用性

要搞清楚原则的关系,不能只答开闭原则这种通用的

设计题中对前提条件、效果的描述会让设计题答案变唯一,仔细审题

模式的联合使用,一般都是不同类型(按目的分,按对象分了解即可)的设计模式进行联合,创建型、结构型、其中结构型最灵活

设计题一定要写清设计意图

  • 设计题通常不是只考单个模式,而是考多个设计模式的联合使用
  • 答题时不要只画一张类图,要先写:
    • 使用了哪些设计模式
    • 每个模式解决了什么问题
    • 哪些类/接口对应模式中的哪些角色
    • 为什么这个模式符合题目中的约束和效果要求

老师强调的点:

  • 类图里抽象类、接口、具体类如果名字不标准,至少要标出它们在模式中的角色
  • 例如使用命令模式时,至少标清 CommandConcreteCommandInvokerReceiver
  • 如果多个模式叠加,要说明角色重叠,否则阅卷时很难看出你到底用了哪些模式

设计题要从“问题 + 前提条件 + 解法 + 效果”推模式

软件模式不是简单的“问题 -> 解法”,而是一整套结构:

  • 问题描述
  • 前提条件 / 环境 / 约束条件
  • 解法
  • 效果 / 优缺点

因此审题时要特别关注:

  • 题目明确要求“不能修改原有类”吗?
  • 题目强调“接口不兼容”吗?
  • 题目强调“整体和部分一致处理”吗?
  • 题目强调“实时通知”吗?
  • 题目强调“对象创建不能散落在客户端”吗?
  • 题目强调“避免遗漏某个包装/装饰步骤”吗?

这些描述会把设计题答案限制到某几个模式,甚至限制成唯一答案。

多模式设计题答题流程

列出使用模式

不要让阅卷人猜。

示例:

本设计使用了 Adapter、Decorator、Abstract Factory、Composite、Observer 五个模式。
Adapter 用于解决 Goose 和 Quackable 的接口不兼容;
Decorator 用于在不修改鸭子类的前提下增加计数功能;
Abstract Factory 用于集中创建并保证对象被正确装饰;
Composite 用于统一处理单个鸭子和鸭群;
Observer 用于实时通知 Quackologist。

找稳定抽象

问自己:

  • 谁是客户端?
  • 客户端真正需要的接口是什么?
  • 哪些具体类可以隐藏在接口后面?

本例稳定抽象:

Quackable

识别变化点

题目关键词 优先想到
interface mismatch / 接口不兼容 Adapter
without changing original classes / 不修改原类 Decorator 或 Adapter
add responsibilities dynamically / 动态增加职责 Decorator
creation and decorating should be encapsulated / 创建与装饰封装 Factory / Abstract Factory
family of products / 产品族 Abstract Factory
collections and sub-collections / 集合和子集合 Composite
treat individual objects and collections uniformly / 个体和整体一致处理 Composite
in real time / 实时跟踪 Observer
one object changes, others need notification / 状态变化通知多个对象 Observer

写角色映射

每个模式至少写出关键角色映射。

例如:

Adapter:
Target = Quackable
Adapter = GooseAdapter
Adaptee = Goose
Client = DuckSimulator

说明模式叠加关系

答题时要写:

  • GooseAdapterQuackCounterFlock 都实现 Quackable
  • 因此它们可以被 DuckSimulator 一致处理
  • AbstractDuckFactory 的方法返回 Quackable
  • Quackable 还扩展被观察能力,使鸭子可以通知 Observer

考前速背

  • 设计题先写用了哪些模式,再画图
  • 设计图里要标出重要接口和模式角色
  • 设计模式要从问题、约束、效果三方面推导
  • 开闭原则是目标,不要所有题都只答 OCP
  • 软件模式包含问题描述、前提条件、解法和效果
  • 设计模式四个关键要素:名称、问题、解决方案、效果
  • 创建型管对象创建,结构型管组合,行为型管交互和职责分配
  • 类模式偏继承和编译期,对象模式偏组合和运行期
  • 多模式设计最重要的是找稳定抽象接口
  • Duck Simulator 的核心接口是 Quackable

Duck Simulator 主线:

Quackable 统一接口
-> GooseAdapter 解决接口不兼容
-> QuackCounter 动态增加计数
-> CountingDuckFactory 保证统一创建带计数对象
-> Flock 统一处理个体和整体
-> Observer / Observable 实时通知观察者

易错点

  • 适配器不是修改被适配者,而是包一层转换接口
  • 装饰者必须和被装饰对象实现同一接口,否则客户端不能透明使用
  • 抽象工厂强调产品族,不是简单把 new 挪到一个方法里
  • 组合模式的核心是整体和部分具有一致接口
  • 本例采用安全组合:只有 Flockadd(),不是所有 Quackable 都有 add()
  • 观察者模式中,主题对象不应该依赖具体观察者类,而应依赖观察者接口
  • 本例中的 Observable 是辅助类,用来复用注册和通知逻辑
  • 设计题不要画一堆抽象类和具体类却不说明角色
  • 多模式联合使用时,重点看接口是否重叠、角色是否重叠

设计原则

面向对象软件系统设计的目标:

  • 在支持可维护性的同时,提高系统可复用性
  • 软件复用可以提高开发效率、提高软件质量、节约开发成本
  • 恰当的复用还能改善系统可维护性

设计原则之间的关系

目标:开闭原则
指导:最小知识原则 / 迪米特法则
基础:单一职责原则、可变性封装原则
实现:依赖倒转原则、合成复用原则、里氏代换原则、接口隔离原则

套到模式时可以这样写:

  • 工厂方法 / 抽象工厂:通过抽象工厂和抽象产品体现 DIP,通过新增具体工厂和具体产品支持 OCP
  • 装饰者模式:通过组合代替继承体现 CRP,在不修改原类的前提下动态扩展职责,支持 OCP
  • 观察者模式:主题依赖观察者接口而不是具体观察者,体现 DIP;新增观察者不影响主题,支持 OCP
  • 外观模式:通过外观类减少客户端和子系统的直接交互,体现迪米特法则
  • 策略模式:封装算法变化,客户端依赖策略接口,体现 EVP 和 DIP

单一职责原则 SRP

Single Responsibility Principle
  • 一个类只负责一个功能领域中的相应职责
  • 一个类不应该承担太多变化原因

开闭原则 OCP

Open-Closed Principle
  • 软件实体应当对扩展开放,对修改关闭
  • 即在不修改源代码的基础上扩展系统行为
  • 可通过可变性封装原则 EVP更具体地理解:找到系统变化点,并将变化点封装起来

里氏代换原则 LSP

Liskov Substitution Principle
  • 在软件系统中,一个可以接受基类对象的地方必然可以接受子类对象
  • 子类替换父类后,系统行为不应被破坏

依赖倒转原则 DIP

Dependency Inversion Principle
  • 抽象不应该依赖于细节,细节应该依赖于抽象
  • 要针对接口编程,不要针对实现编程

接口隔离原则 ISP

Interface Segregation Principle
  • 客户端不应该依赖它不需要的接口
  • 将大接口拆成更小、更专门的接口

合成复用原则 CRP

Composite Reuse Principle
  • 复用时尽量使用对象组合 / 聚合,而不是继承
  • 组合是黑盒复用,继承是白盒复用

迪米特法则 LoD

Law of Demeter / Least Knowledge Principle
  • 一个软件实体应尽可能少地与其他实体发生相互作用
  • 如果两个类不必直接通信,就通过第三者发生间接交互
  • 常见体现:外观模式、中介者模式

软件模式与设计模式

软件模式

软件模式是将“模式”的一般概念应用于软件开发领域,是软件开发的总体指导思路或参照样板。

软件模式不仅包括设计模式,还包括:

  • 架构模式
  • 分析模式
  • 过程模式

软件模式可以看成:

一定条件下出现的问题 + 对应解法 + 解法效果

基础结构:

  • 问题描述
  • 前提条件 / 环境 / 约束条件
  • 解法
  • 效果

大三律:

Rule of Three
  • 一个候选解决方案只有经过三个以上不同类型或不同领域系统的校验,才能升格为模式

设计模式

设计模式是一套被反复使用、多数人知晓、经过分类编目的代码设计经验总结。

使用设计模式的目的:

  • 代码复用
  • 让代码更容易被他人理解
  • 保证代码可靠性

基本要素:

  • 模式名称 Pattern name
  • 问题 Problem
  • 目的
  • 解决方案 Solution
  • 效果 Consequences
  • 实例代码
  • 相关设计模式

最关键的四个:

Pattern name + Problem + Solution + Consequences

设计模式分类

按目的分类:

类型 作用
创建型模式 Creational 主要用于创建对象
结构型模式 Structural 主要用于处理类或对象的组合
行为型模式 Behavioral 主要描述类或对象如何交互、如何分配职责

按范围分类:

类型 特点
类模式 处理类和子类之间的关系,通过继承建立,编译期确定,偏静态
对象模式 处理对象之间的关系,运行时可以变化,更动态

考试注意:

  • 创建型模式通常容易和结构型/行为型模式联合
  • 行为型模式通常较少彼此联合,但正交行为模式可以叠加
  • 结构型模式最灵活,常和创建型、行为型、结构型本身联合

设计模式不是类库和框架

如果设计模式这么好,为什么没人构建一个包含所有模式的库?

  • 设计模式比库层次更高
  • 设计模式告诉我们如何构建类和对象来解决特定问题
  • 具体如何调整,要结合应用场景

类库和框架是不是设计模式?

  • 类库和框架不是设计模式
  • 它们提供具体实现,供自己的代码调用或继承
  • 但类库和框架内部可能使用设计模式

UML

依赖:A - - - - > B 关联:A ─────> B 聚合:A ◇──── B 组合:A ◆──── B 泛化:A ─────▷ B 实现:A - - - -▷ B

设计模式

创建型模式

简单工厂模式 Simple Factory Pattern

  • 核心意图:专门定义一个工厂类,根据传入参数创建不同的具体产品对象。
  • 关键角色:Factory 工厂角色、Product 抽象产品、ConcreteProduct 具体产品。

简单工厂模式类图

  • 掌握重点:客户端只需要知道参数,不需要知道具体产品类名和创建细节。
  • 适用关键词:对象较少、创建逻辑集中、客户端只关心“我要哪一种产品”。
  • 优点:分离对象创建和业务处理,客户端使用简单。
  • 缺点:工厂类职责过重;新增产品通常要修改工厂判断逻辑,违背开闭原则。
  • 易错点:简单工厂不是 GoF 23 种设计模式之一,但课堂讲过,考试可能用来和工厂方法、抽象工厂比较。

工厂方法模式 Factory Method Pattern

  • 核心意图:定义创建产品对象的工厂接口,把具体实例化延迟到工厂子类。
  • 关键角色:Product 抽象产品、ConcreteProduct 具体产品、Factory 抽象工厂、ConcreteFactory 具体工厂。

工厂方法模式类图

  • 掌握重点:一个具体工厂通常对应一种具体产品;新增产品时新增具体产品和具体工厂。
  • 适用关键词:客户端不知道需要创建的具体类;由子类决定创建哪个对象;需要替代简单工厂的集中判断。
  • 优点:符合开闭原则,隐藏具体产品创建细节,客户端面向抽象工厂和抽象产品编程。
  • 缺点:类数量增加,抽象层增加理解难度。
  • 易错点:工厂方法针对一个产品等级结构;如果每个工厂只创建一个产品,就是典型工厂方法。

抽象工厂模式 Abstract Factory Pattern

  • 核心意图:提供一个创建一系列相关或相互依赖对象的接口,而无须指定具体类。
  • 关键角色:AbstractFactory 抽象工厂、ConcreteFactory 具体工厂、AbstractProduct 抽象产品、ConcreteProduct 具体产品。

抽象工厂模式类图

截屏2026-05-09 16.42.30

  • 掌握重点:抽象工厂面向产品族 product family,一个具体工厂负责创建同一产品族中的多个产品。
  • 适用关键词:多个产品族;每次只使用某一个产品族;同一产品族产品必须一起使用。
  • 优点:隔离具体类生成;方便更换产品族;保证客户端使用同一产品族对象。
  • 缺点:新增产品族容易,新增产品等级结构困难,这叫开闭原则的倾斜性
  • 易错点:工厂方法针对一个产品等级结构,抽象工厂针对多个产品等级结构组成的产品族。

建造者模式 Builder Pattern

  • 核心意图:将复杂对象的构建过程与表示分离,使同样的构建过程可以创建不同表示。
  • 关键角色:Builder 抽象建造者、ConcreteBuilder 具体建造者、Director 指挥者、Product 产品。

建造者模式类图

  • 掌握重点:一步一步构建复杂对象;Director 控制构建顺序并隔离客户端与生产过程。
  • 适用关键词:复杂对象、多个部件、构建顺序、属性相互依赖、客户端不关心内部组装细节。
  • 优点:产品和创建过程解耦;可精细控制创建过程;新增具体建造者较方便。
  • 缺点:产品差异很大时不适合;内部变化复杂时具体建造者会很多。
  • 易错点:建造者返回一个组装好的完整产品;抽象工厂返回一组相关产品。

原型模式 Prototype Pattern

  • 核心意图:用原型实例指定创建对象的种类,并通过复制原型创建新对象。
  • 关键角色:Prototype 抽象原型、ConcretePrototype 具体原型、Client 客户端。

原型模式类图

  • 掌握重点:通过 clone() 复制已有对象;Java 中常涉及 Cloneable 标识接口。
  • 适用关键词:创建对象成本大;对象状态组合少;需要复制粘贴或保存状态。
  • 优点:简化对象创建;提高创建效率;可动态增加或移除原型对象。
  • 缺点:每个类需要实现克隆方法;深克隆实现复杂。
  • 易错点:浅克隆只复制对象本身,内部引用对象仍共享;深克隆会复制内部引用对象。

结构型模式

适配器模式 Adapter Pattern

  • 核心意图:将一个接口转换成客户端期望的另一个接口,使接口不兼容的类可以协同工作。
  • 关键角色:Target 目标接口、Adapter 适配器、Adaptee 适配者、Client 客户端。

对象适配器:

对象适配器类图

类适配器:

类适配器类图

  • 掌握重点:适配器把客户端请求转换成对适配者方法的调用;改变的是对外接口,不是原有类本身。
  • 适用关键词:接口不兼容、复用已有类、现有类接口不符合系统需要。
  • 优点:目标类和适配者解耦,提高已有类复用性。
  • 缺点:类适配器受单继承限制;对象适配器不容易直接重写适配者行为。
  • 易错点:类适配器用继承,对象适配器用组合;对象适配器通常更灵活。

组合模式 Composite Pattern

  • 核心意图:将对象组合成树形结构表示整体-部分层次,使客户端一致处理单个对象和组合对象。
  • 关键角色:Component 抽象构件、Leaf 叶子构件、Composite 容器构件、Client 客户端。

组合模式类图

  • 掌握重点:容器对象可以包含叶子,也可以包含容器,形成递归组合。
  • 适用关键词:树形结构、整体-部分、文件目录、XML、GUI 组件、希望忽略叶子和容器差异。
  • 优点:客户端调用简单;增加新构件容易;适合表示复杂层次结构。
  • 缺点:设计更抽象;很难限制容器中构件类型;叶子和容器方法差异可能带来问题。
  • 易错点:透明组合把 add/remove/getChild 放在 Component 中,强调一致性;安全组合只放在 Composite 中,强调类型安全。

桥接模式 Bridge Pattern

  • 核心意图:将抽象部分与实现部分分离,使二者可以独立变化。
  • 关键角色:Abstraction 抽象类、RefinedAbstraction 扩充抽象类、Implementor 实现接口、ConcreteImplementor 具体实现类。

截屏2026-06-08 15.39.35

  • 掌握重点:把继承关系转换为关联关系;用组合 / 聚合替代多层继承。
  • 适用关键词:两个独立变化维度、类爆炸、不希望抽象和实现静态绑定。
  • 优点:分离抽象和实现;提高扩展性;实现细节对客户端透明。
  • 缺点:需要正确识别两个变化维度,设计理解难度较高。
  • 易错点:桥接通常用于系统初步设计;适配器常用于已有系统接口不兼容后的补救。

装饰模式 Decorator Pattern

  • 核心意图:动态地给一个对象增加额外职责,提供比继承更灵活的功能扩展方式。
  • 关键角色:Component 抽象构件、ConcreteComponent 具体构件、Decorator 抽象装饰类、ConcreteDecorator 具体装饰类。

装饰模式类图

  • 掌握重点:装饰者和被装饰者实现同一接口;装饰者内部持有 Component 引用,并在调用前后添加行为。
  • 适用关键词:动态增加 / 撤销功能、不想用继承、功能组合很多、只给单个对象添加职责。
  • 优点:比继承灵活;可运行时组合多个装饰;符合开闭原则。
  • 缺点:会产生很多小对象;多层装饰调试困难。
  • 易错点:装饰模式扩展对象职责;代理模式控制对象访问;适配器模式转换接口。

外观模式 Facade Pattern

  • 核心意图:为子系统中的一组接口提供统一的高层接口,使子系统更容易使用。
  • 关键角色:Facade 外观角色、SubSystem 子系统角色、Client 客户端。

外观模式类图

  • 掌握重点:客户端只与外观对象交互,复杂子系统调用由外观统一处理。
  • 适用关键词:复杂子系统、统一入口、降低客户端和多个子系统之间的依赖。
  • 优点:屏蔽子系统复杂性;降低耦合;体现迪米特法则。
  • 缺点:不能完全限制客户端直接访问子系统;没有抽象外观时,增加新子系统可能违背开闭原则。
  • 易错点:外观模式负责简化访问,不负责给子系统增加新行为。

享元模式 Flyweight Pattern

  • 核心意图:运用共享技术有效支持大量细粒度对象的复用。
  • 关键角色:Flyweight 抽象享元、ConcreteFlyweight 具体享元、UnsharedConcreteFlyweight 非共享享元、FlyweightFactory 享元工厂。

享元模式类图

  • 掌握重点:共享内部状态 intrinsic state,外部状态 extrinsic state 由客户端保存并传入。
  • 适用关键词:大量相同或相似对象、内存消耗大、对象大部分状态可外部化、重复使用频繁。
  • 优点:减少内存中对象数量,节约空间,提高性能。
  • 缺点:必须区分内部状态和外部状态,系统逻辑更复杂;传入外部状态可能增加运行开销。
  • 易错点:享元工厂维护享元池;常和简单工厂、单例、组合模式联用。

代理模式 Proxy Pattern

  • 核心意图:为某个对象提供代理,并由代理对象控制对真实对象的访问。
  • 关键角色:Subject 抽象主题、Proxy 代理主题、RealSubject 真实主题、Client 客户端。

代理模式类图

  • 掌握重点:代理和真实对象实现同一接口;代理内部持有真实对象引用,并在访问前后执行额外操作。
  • 适用关键词:远程访问、延迟加载、权限控制、缓存、防火墙、智能引用。
  • 优点:降低调用者和真实对象耦合;可控制访问、节省资源、隐藏远程通信细节。
  • 缺点:增加代理层可能降低请求速度;实现复杂度增加。
  • 易错点:Remote Proxy 远程代理、Virtual Proxy 虚拟代理、Protection Proxy 保护代理、Cache Proxy 缓冲代理要能识别。

行为型模式

策略模式 Strategy Pattern

  • 核心意图:定义一族算法,分别封装每个算法,并使它们可以相互替换。
  • 关键角色:Context 上下文、Strategy 抽象策略、ConcreteStrategy 具体策略。
  • 掌握重点:上下文通过组合持有策略对象,算法可以独立于客户端变化。
  • 适用关键词:一组算法、行为可替换、大量 if-else/switch、运行时切换行为。
  • 优点:封装变化;替代继承;消除条件语句;符合开闭原则。
  • 缺点:客户端需要知道不同策略;策略类数量增加;上下文和策略之间可能有通信开销。
  • 易错点:策略模式的三条原则主线是封装变化、面向接口编程、优先组合而不是继承。

状态模式 State Pattern

  • 核心意图:允许对象在内部状态改变时改变其行为,对象看起来像修改了它的类。
  • 关键角色:Context 环境类、State 抽象状态、ConcreteState 具体状态。

状态模式类图

  • 掌握重点:把不同状态下的行为和转换规则封装到不同状态类中。
  • 适用关键词:对象行为依赖状态、状态经常切换、大量与状态有关的条件语句。
  • 优点:封装状态转换规则;状态相关行为集中;可共享状态对象。
  • 缺点:类和对象数量增加;使用不当会导致结构混乱;增加新状态可能需要修改其他状态或环境类。
  • 易错点:状态模式关注“同一对象不同状态下行为不同”;策略模式关注“算法族可替换”。

命令模式 Command Pattern

  • 核心意图:将一个请求封装为对象,使请求发送者和接收者解耦。
  • 关键角色:Command 抽象命令、ConcreteCommand 具体命令、Invoker 调用者、Receiver 接收者、Client 客户端。

命令模式类图

  • 掌握重点:命令对象可以被存储、传递、排队、记录日志,并支持撤销 / 重做。
  • 适用关键词:请求发送者与接收者解耦、命令队列、日志、撤销 Undo、恢复 Redo、宏命令。
  • 优点:降低耦合;容易新增命令;支持命令队列和组合命令。
  • 缺点:具体命令类可能过多。
  • 易错点:画类图时一定标清 CommandConcreteCommandInvokerReceiver;宏命令是命令模式和组合模式联用。

观察者模式 Observer Pattern

  • 核心意图:定义对象间一对多依赖关系,使一个对象状态变化时,所有依赖对象得到通知并自动更新。
  • 关键角色:Subject 目标、ConcreteSubject 具体目标、Observer 观察者、ConcreteObserver 具体观察者。

观察者模式类图

  • 掌握重点:观察目标维护观察者列表,通过 attach/detach/notify 注册、移除和通知观察者。
  • 适用关键词:状态变化通知多个对象、发布-订阅、事件监听、Model 改变 View 自动更新。
  • 优点:主题和观察者抽象耦合;支持广播通信;新增观察者方便,符合开闭原则。
  • 缺点:通知大量观察者耗时;循环依赖可能导致循环调用;观察者不一定知道目标如何变化。
  • 易错点:Java 委派事件模型中 Event Source 类似主题,Event Listener 类似观察者,Event Object 封装事件信息。

中介者模式 Mediator Pattern

  • 核心意图:用一个中介对象封装一系列对象交互,避免对象之间显式相互引用。
  • 关键角色:Mediator 抽象中介者、ConcreteMediator 具体中介者、Colleague 抽象同事、ConcreteColleague 具体同事。

中介者模式类图

  • 掌握重点:把多对多复杂交互改成同事对象与中介者之间的一对多关系。
  • 适用关键词:对象之间引用关系复杂、多个对象交互混乱、想用中间类集中协调交互。
  • 优点:简化对象交互;同事类解耦;减少子类生成。
  • 缺点:具体中介者可能过于复杂,变成维护难点。
  • 易错点:中介者体现迪米特法则;MVC 中的 Controller 常可理解为中介者。

模板方法模式 Template Method Pattern

  • 核心意图:在父类中定义算法骨架,把某些步骤延迟到子类实现。
  • 关键角色:AbstractClass 抽象类、ConcreteClass 具体子类。

截屏2026-06-08 15.42.56

  • 掌握重点:模板方法负责组织整体流程;基本方法 Primitive Method 表示算法步骤;钩子方法 Hook Method 提供可选扩展点。
  • 适用关键词:算法流程固定、部分步骤变化、公共行为提取到父类、框架控制子类扩展。
  • 优点:复用公共代码;父类控制流程;子类扩展具体步骤,符合开闭原则。
  • 缺点:每种不同实现都需要定义子类,类数量增加。
  • 易错点:体现好莱坞原则 Don't call us, we'll call you,即父类控制对子类方法的调用。

Patterns of Patterns / Compound Pattern

PPT 原话:

A compound pattern combines two or more patterns into a solution
that solves a recurring or general problem.

中文理解:

  • 多个模式可以在同一个设计方案中协作
  • 当多个模式组合起来反复解决某类通用问题时,它们可以形成一个复合模式
  • 典型例子:MVC 是 Observer、Strategy、Composite 等模式思想的组合

老师强调:

  • 多模式设计不是“东一个模式、西一个模式”
  • 多个模式会叠加成一个复合结构
  • 关键是看它们是否围绕同一个抽象接口协作,以及是否存在角色重叠

Duck Simulator 复合模式例题

这个例题来自 Head First Design Patterns 的 Duck Simulator,是本次复习 PPT 的主线。

它非常重要,因为它不是问“某一个模式怎么写”,而是通过连续需求变化,考你能不能把多个模式叠加到一个设计里。

初始背景:Quackable 接口

要做一个鸭子模拟器,系统中有多种会叫的对象:

  • MallardDuck
  • RedheadDuck
  • DuckCall
  • RubberDuck

统一抽象:

interface Quackable {
    void quack();
}

所有会叫的对象都实现 Quackable

DuckSimulator -> Quackable
MallardDuck implements Quackable
RedheadDuck implements Quackable
DuckCall implements Quackable
RubberDuck implements Quackable

考点:

  • 先找稳定抽象
  • 客户端依赖接口,而不是依赖具体类
  • 后续模式都围绕 Quackable 叠加

需求:加入 Goose

需求描述

PPT:

Let’s say we wanted to be able to use a Goose anywhere we’d want to use a Duck.
What pattern would allow Geese to easily intermingle with Ducks?

已有类:

class Goose {
    void honk() {
        // goose sound
    }
}

问题:

  • 模拟器期望的是 Quackable
  • Goose 提供的是 honk()
  • 接口不兼容
  • 希望在使用 Duck / Quackable 的地方使用 Goose

使用模式:适配器模式

Adapter Pattern

解法:

  • 创建 GooseAdapter
  • GooseAdapter implements Quackable
  • 内部组合一个 Goose
  • quack() 中转调 goose.honk()

代码结构:

class GooseAdapter implements Quackable {
    private Goose goose;

    public GooseAdapter(Goose goose) {
        this.goose = goose;
    }

    public void quack() {
        goose.honk();
    }
}

角色映射:

适配器模式角色 本例
Target Quackable
Adapter GooseAdapter
Adaptee Goose
Client DuckSimulator

答题要点:

  • 适配器解决的是接口不兼容
  • 不修改 Goose
  • 不修改 DuckSimulator
  • DuckSimulator 仍然只依赖 Quackable
  • 适配器通过包装对象完成接口转换

需求:统计叫声次数

需求描述

PPT:

How can we add the ability to count duck quacks
without having to change the duck classes?

问题:

  • Quackologists 想统计所有鸭子一共叫了多少次
  • 需要给鸭子增加“计数”行为
  • 约束:不能修改具体鸭子类

使用模式:装饰者模式

Decorator Pattern

解法:

  • 创建 QuackCounter
  • QuackCounter implements Quackable
  • 内部包装一个 Quackable
  • 调用 quack() 时先委托被包装对象叫,再累加计数

代码结构:

class QuackCounter implements Quackable {
    private Quackable duck;
    private static int numberOfQuacks;

    public QuackCounter(Quackable duck) {
        this.duck = duck;
    }

    public void quack() {
        duck.quack();
        numberOfQuacks++;
    }

    public static int getQuacks() {
        return numberOfQuacks;
    }
}

角色映射:

装饰者模式角色 本例
Component Quackable
ConcreteComponent MallardDuckRedheadDuckDuckCallRubberDuckGooseAdapter
Decorator / ConcreteDecorator QuackCounter
Client DuckSimulator

答题要点:

  • 装饰者用于动态增加职责
  • 装饰者和被装饰对象实现同一接口
  • 客户端仍然按 Quackable 使用
  • 通过组合扩展行为,不修改原类
  • static 计数变量用于统计所有被装饰对象的叫声总数

易错点:

  • 如果直接让每个鸭子类加计数字段,就违反“不修改原类”的约束
  • 如果用观察者统计,会要求原鸭子类实现额外通知接口,改动更大,不符合当前需求

需求:保证对象创建时都被正确装饰

需求描述

PPT:

Too many quacks aren’t being counted.
That’s the problem with wrapping objects:
you have to make sure they get wrapped or they don’t get the decorated behavior.

Why don’t we take the duck creation and decorating and encapsulate it?

问题:

  • 装饰者的缺点:必须确保对象被包装
  • 如果客户端忘记写 new QuackCounter(...),叫声就不会被统计
  • 需要把“创建鸭子 + 装饰鸭子”的过程集中起来

使用模式:抽象工厂模式

Abstract Factory Pattern

为什么不是单纯工厂方法?

  • 这里要创建的是一组相关产品:多种鸭子
  • 并且有不同产品族:
    • 普通鸭子产品族
    • 带计数装饰的鸭子产品族

抽象工厂:

abstract class AbstractDuckFactory {
    abstract Quackable createMallardDuck();
    abstract Quackable createRedheadDuck();
    abstract Quackable createDuckCall();
    abstract Quackable createRubberDuck();
}

普通工厂:

class DuckFactory extends AbstractDuckFactory {
    Quackable createMallardDuck() {
        return new MallardDuck();
    }
}

计数工厂:

class CountingDuckFactory extends AbstractDuckFactory {
    Quackable createMallardDuck() {
        return new QuackCounter(new MallardDuck());
    }
}

角色映射:

抽象工厂角色 本例
AbstractFactory AbstractDuckFactory
ConcreteFactory DuckFactoryCountingDuckFactory
AbstractProduct Quackable
ConcreteProduct 各种具体鸭子 / 被 QuackCounter 包装的鸭子
Client DuckSimulator

答题要点:

  • 工厂把创建逻辑从客户端移走
  • CountingDuckFactory 把创建和装饰封装在同一个工厂方法里
  • 通过传入不同工厂,客户端获得不同产品族
  • 防止忘记装饰,提高质量控制

老师强调:

  • 装饰者和工厂经常联合使用
  • 装饰者的问题是包装容易遗漏
  • 工厂负责把包装过程局部化、统一化

需求:管理一群鸭子

需求描述

PPT:

Is there any way you can help us manage ducks as a whole,
and perhaps even allow us to manage a few duck families?

What we need is a way to talk about collections of ducks
and even sub-collections of ducks.
It would also be nice if we could apply operations across the whole set of ducks.

问题:

  • 鸭子越来越多,逐个管理很麻烦
  • 需要管理鸭群和子鸭群
  • 希望对单个鸭子和一群鸭子统一调用 quack()

使用模式:组合模式

Composite Pattern

解法:

  • 创建 Flock
  • Flock implements Quackable
  • 内部维护 List<Quackable>
  • Flock.quack() 遍历成员并调用每个成员的 quack()

代码结构:

class Flock implements Quackable {
    private List<Quackable> quackers = new ArrayList<>();

    public void add(Quackable quacker) {
        quackers.add(quacker);
    }

    public void quack() {
        for (Quackable quacker : quackers) {
            quacker.quack();
        }
    }
}

角色映射:

组合模式角色 本例
Component Quackable
Leaf 具体鸭子、GooseAdapterQuackCounter
Composite Flock
Client DuckSimulator

答题要点:

  • 组合模式适合整体-部分结构
  • Flock 和单个鸭子都实现 Quackable
  • 客户端可以一致调用 quack()
  • 子群可以继续加入更大的群,形成递归结构

安全性 vs 透明性

PPT 强调:

  • 透明组合模式中,Component 拥有 add()remove()getChild() 等方法
  • 好处:客户端不区分叶子和容器
  • 问题:叶子节点也暴露无意义的 add() 方法

本例采取更安全的做法:

  • Quackable 只有 quack()
  • 只有 Flockadd()
  • 普通鸭子没有 add()

取舍:

做法 优点 缺点
透明组合 客户端完全一致处理 Leaf 和 Composite 叶子节点会拥有无意义方法
安全组合 不会对叶子调用无意义方法 客户端要知道当前对象是不是 Flock 才能添加成员

需求:实时跟踪个体鸭子叫声

需求描述

PPT:

We also need to track individual ducks.
Can you give us a way to keep track of individual duck quacking in real time?

问题:

  • 不只是统计总次数,还要实时知道某只鸭子什么时候叫
  • 需要跟踪不确定个体的状态变化
  • 当鸭子叫时,观察者应收到通知

使用模式:观察者模式

Observer Pattern

解法:

  • 定义观察者接口 Observer
  • 定义被观察者接口 QuackObservable
  • Quackable 继承/扩展 QuackObservable
  • 使用辅助类 Observable 封装注册和通知逻辑
  • 具体鸭子把注册和通知委托给内部的 Observable

接口:

interface Observer {
    void update(QuackObservable duck);
}

interface QuackObservable {
    void registerObserver(Observer observer);
    void notifyObservers();
}

interface Quackable extends QuackObservable {
    void quack();
}

辅助类:

class Observable implements QuackObservable {
    private List<Observer> observers = new ArrayList<>();
    private QuackObservable duck;

    public Observable(QuackObservable duck) {
        this.duck = duck;
    }

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(duck);
        }
    }
}

具体鸭子中的委托:

class MallardDuck implements Quackable {
    private Observable observable;

    public MallardDuck() {
        observable = new Observable(this);
    }

    public void quack() {
        System.out.println("Quack");
        notifyObservers();
    }

    public void registerObserver(Observer observer) {
        observable.registerObserver(observer);
    }

    public void notifyObservers() {
        observable.notifyObservers();
    }
}

具体观察者:

class Quackologist implements Observer {
    public void update(QuackObservable duck) {
        System.out.println("Quackologist: " + duck + " just quacked.");
    }
}

角色映射:

观察者模式角色 本例
Subject / Observable QuackObservable
ConcreteSubject 具体鸭子、FlockQuackable
Observer Observer
ConcreteObserver Quackologist
辅助类 Observable

答题要点:

  • 观察者模式用于“一对多依赖”
  • 对象发生变化时自动通知观察者
  • 本例没有在每个鸭子类重复维护观察者列表,而是把通用逻辑封装到辅助类 Observable
  • Observable 不是这里的重点,重点是用组合复用观察者注册/通知逻辑

老师强调:

  • 观察者模式经常和其他模式联合使用
  • 因为“被观察能力”与对象原本的主要业务功能是正交的
  • 但观察者模式往往需要额外接口,因此要尽量减少对原有类的改动

最终复合结构总结

本题用到的模式

需求变化 使用模式 关键做法
鹅接口不兼容,但要放进鸭子模拟器 适配器模式 GooseAdapter implements Quackable,把 quack() 转成 honk()
不改鸭子类,给鸭子增加计数行为 装饰者模式 QuackCounter implements Quackable,包装 Quackable 并累加计数
防止创建对象时忘记装饰 抽象工厂模式 CountingDuckFactory 统一创建被装饰的产品族
把一群鸭子当成单个鸭子使用 组合模式 Flock implements Quackable,内部保存 Quackable 集合
实时观察鸭子叫声 观察者模式 Observer + QuackObservable + 辅助类 Observable

最终结构速记

  • DuckSimulator:客户端 / 模拟器,依赖抽象工厂和 Quackable
  • Quackable:所有会叫对象的统一接口
  • MallardDuckRedheadDuckDuckCallRubberDuck:具体会叫对象
  • Goose:已有鹅类,方法为 honk()
  • GooseAdapter:适配器,把 Goose 适配成 Quackable
  • QuackCounter:装饰者,包装 Quackable 并统计叫声
  • AbstractDuckFactory:抽象工厂,声明一组创建方法
  • DuckFactory:普通工厂,创建普通 Quackable
  • CountingDuckFactory:计数工厂,创建带 QuackCounterQuackable
  • Flock:组合对象,也实现 Quackable,内部保存多个 Quackable
  • QuackObservable:被观察者接口
  • Observable:辅助类,集中处理观察者注册和通知
  • Observer:观察者接口
  • Quackologist:具体观察者

最重要的角色重叠

本题关键不是“用了五个模式所以要画五套图”,而是很多模式共享同一个抽象接口。

核心接口:

Quackable

它在不同模式中承担不同角色:

模式 Quackable 对应角色
适配器模式 Target
装饰者模式 Component
抽象工厂模式 AbstractProduct
组合模式 Component
观察者模式 ConcreteSubject 的公共业务接口,且扩展 QuackObservable

这就是老师强调的“模式叠加”:

  • 多个模式不是孤立并列
  • 它们围绕同一个抽象接口协作
  • 类图中实际类数量不会特别多
  • 关键是看清哪些模式角色可以合并