架构师 - 4.设计模式
23 种设计模式
标签:
设计模式Java面向对象GoF
设计模式(Design Pattern)是一套被反复使用、经过分类编目的代码设计经验总结,由 GoF 在《Design Patterns: Elements of Reusable Object-Oriented Software》中归纳为 23 种。按解决的问题划分为三类:创建型处理对象创建,结构型处理类与对象的组合,行为型处理对象间的职责分配与通信。
一、分类总览
| 分类 | 关注点 | 模式 |
|---|---|---|
| 创建型(5) | 对象的创建机制,解耦"使用"与"创建" | 单例、工厂方法、抽象工厂、建造者、原型 |
| 结构型(7) | 类与对象如何组合成更大的结构 | 适配器、桥接、组合、装饰器、外观、享元、代理 |
| 行为型(11) | 对象间的职责分配与通信 | 责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者 |
描述一个模式包含四个要素:模式名称(交流词汇)、问题(适用条件与痛点)、解决方案(角色、职责、协作关系)、效果(灵活性与性能的权衡)。
二、六大设计原则
原则是模式的底层依据,23 种模式是它在具体场景下的应用。
| 原则 | 定义 | 落地方式 |
|---|---|---|
| 单一职责 SRP | 一个类只有一个引起它变化的原因 | 职责混杂的 Service 拆分 |
| 里氏替换 LSP | 子类可以替换父类且不破坏原有行为 | 重写不改语义,不抛父类未声明的异常 |
| 依赖倒置 DIP | 高层与低层都依赖抽象,面向接口编程 | Controller 依赖 Service 接口而非实现类 |
| 接口隔离 ISP | 调用方不依赖它用不到的方法 | 大而全的接口按调用方拆分 |
| 迪米特 LoD | 只与直接依赖的对象交互 | 避免 a.getB().getC().doX() 式链式穿透 |
| 开闭 OCP | 对扩展开放,对修改关闭 | 新增实现类而非追加 if-else 分支 |
三、创建型模式
| 模式 | 意图 | 辨识特征 |
|---|---|---|
| 单例 Singleton | 全局只有一个实例 | getInstance()、私有构造 |
| 工厂方法 Factory Method | 子类决定实例化哪个类 | XxxFactory.create(),每产品一工厂 |
| 抽象工厂 Abstract Factory | 成套产出产品族 | 工厂内有多个 createXxx() |
| 建造者 Builder | 分步骤装配复杂对象 | 链式 .xxx().build() |
| 原型 Prototype | 复制已有对象创建新对象 | clone()、深/浅拷贝 |
创建型的共同目标:把对象的创建过程从使用方剥离出去,让"我要一个对象"与"这个对象怎么造出来"解耦。
3.1 单例模式
意图:确保一个类只有一个实例,并提供全局访问点。适用于配置中心、线程池、连接池等本就唯一存在的重资源。
┌──────────────────────────┐
│ Singleton │
├──────────────────────────┤
│ - static INSTANCE │ ← 唯一实例
│ - Singleton() │ ← 构造私有,外部无法 new
│ + static getInstance() │ ← 全局唯一访问点
└──────────────────────────┘
Client ──getInstance()──> 同一个 INSTANCE(多次调用返回同一对象)
// 饿汉式:类加载即实例化,线程安全,但可能提前占用资源
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() { return INSTANCE; }
}
// DCL:懒加载,volatile 用于禁止指令重排序,不可省略
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) instance = new LazySingleton();
}
}
return instance;
}
}
// 静态内部类:推荐写法,懒加载且无锁
public class HolderSingleton {
private HolderSingleton() {}
private static class Holder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() { return Holder.INSTANCE; }
}
注意点:
volatile防止的是对象半初始化时被其他线程取用,而非可见性本身。- 反射与反序列化都会破坏单例;枚举单例是 JVM 层面唯一能同时防住两者的写法。
- Spring 的单例是容器级单例,与类加载器级单例不是同一概念。
3.2 工厂方法模式
意图:也称虚拟构造函数,由子类决定实例化哪一个类,把对象的创建延迟到子类。
Factory(PaymentFactory 接口)──create()──> Product(Payment 接口)
△ △
┌──────────┴──────────┐ ┌─────────┴─────────┐
AliPayFactory WeChatPayFactory AliPay WeChatPay
每新增一种产品,就新增一个工厂类;客户端只依赖 Factory 与 Product
public interface Payment { void pay(BigDecimal amount); }
public class AliPay implements Payment {
public void pay(BigDecimal amount) { /* 支付宝 */ }
}
public class WeChatPay implements Payment {
public void pay(BigDecimal amount) { /* 微信 */ }
}
public interface PaymentFactory { Payment create(); }
public class AliPayFactory implements PaymentFactory {
public Payment create() { return new AliPay(); }
}
// 使用方只依赖 Payment 与 PaymentFactory
PaymentFactory factory = new AliPayFactory();
factory.create().pay(new BigDecimal("99.00"));
注意点:
- 简单工厂(
if-else判断类型)每新增产品都要修改工厂类,违反开闭原则;工厂方法用"增加实现类"替代"修改既有代码"。 - 客户端只依赖
Factory与Product两个抽象,新增支付方式时调用方代码零改动。 - 产品种类多时会产生"一个产品配一个工厂"的类膨胀,此时应评估改用抽象工厂或反射注册。
3.3 抽象工厂模式
意图:为创建一组相关或相互依赖的对象提供接口,无需指定具体类。用于产品族的成套产出。
AbstractFactory(ThemeFactory)
├─ createButton() ──> Button
└─ createDialog() ──> Dialog
△
┌──────────┴──────────┐
DarkThemeFactory LightThemeFactory
产出同族:DarkButton + DarkDialog(成套使用,不跨族混用)
产品族一(深色):Button + Dialog
产品族二(浅色):Button + Dialog
public interface ThemeFactory {
Button createButton();
Dialog createDialog();
}
public class DarkThemeFactory implements ThemeFactory {
public Button createButton() { return new DarkButton(); }
public Dialog createDialog() { return new DarkDialog(); }
}
对比工厂方法:
| 维度 | 工厂方法 | 抽象工厂 |
|---|---|---|
| 产品 | 单一产品等级结构 | 一个产品族 |
| 工厂方法数 | 一个 create() |
多个 createXxx() |
| 可扩展性 | 新增产品容易 | 新增产品族容易,新增产品种类需改动所有工厂 |
注意点:
- 抽象工厂保证拿到的是同一族产品,避免深色按钮配浅色弹窗这类混搭问题。
- 代价是开闭原则的倾斜性:新增产品族只需加一个实现类,新增产品种类则要改所有工厂。
- 选型前先确认"产品种类稳定、产品族会变",否则会陷入频繁改接口的窘境。
3.4 建造者模式
意图:将复杂对象的构建与表示分离,同样的构建过程可创建不同表示。用于参数多、可选参数多或参数间存在约束的场景。
Director ──装配顺序──> Builder(接口)──setX()/setY()──┐
△ │
ConcreteBuilder │ 分步设值
└────── build() ────────────┘──> Product(HttpConfig)
链式调用:builder().url().timeout().retry().build()
(先分步传参,最后一次性产出完整对象)
HttpConfig config = HttpConfig.builder()
.url("https://api.example.com")
.timeout(3000)
.retry(2)
.build();
// 对比:伸缩构造函数无法分辨第 5 个 boolean 的含义
new HttpConfig(url, 3000, 2, true, null, null, false);
// 对比:setter 方式对象处于半初始化状态,且无法做成不可变对象
注意点:
- 角色为
Product(产品)、Builder(抽象建造者)、Director(装配顺序);日常链式调用中Director通常被省略。 - 与抽象工厂的区别:建造者分步骤装配一个复杂对象并最终一次性返回,抽象工厂一次性产出一族产品。
- 相比伸缩构造函数可读性更好,相比 setter 能保证对象一次成型、可做成不可变对象。
- Lombok 的
@Builder、MyBatis 的SqlSessionFactoryBuilder、JDK 的StringBuilder都是这一模式的应用。
3.5 原型模式
意图:通过复制已有对象创建新对象,跳过构造过程。适用于构造成本高或需保留当前状态的场景。
Prototype(Cloneable 接口)
△
Report(原对象)──clone()──> Report(副本)
├─ data 引用 ├─ 浅拷贝:与原件共用同一 data
└─ 深拷贝:data 也复制一份
public class Report implements Cloneable {
private Map<String, Object> data;
@Override
public Report clone() {
try {
Report copy = (Report) super.clone();
copy.data = new HashMap<>(this.data); // 深拷贝
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
注意点:
- 核心是浅拷贝与深拷贝的区分:
Object.clone()默认浅拷贝,引用类型字段仍与原对象共享,需要手动复制才能达到隔离效果。 - 相比
new,克隆跳过了构造函数,适合对象初始化成本高(如读库、读文件)的场景。 - 实现
Cloneable只是标记,真正深拷贝要逐层复制引用字段;更稳妥的替代是拷贝构造器或序列化反序列化。
四、结构型模式
| 模式 | 意图 | 辨识特征 |
|---|---|---|
| 适配器 Adapter | 转换接口使原本不兼容的类协作 | XxxAdapter,旧接口适配新接口 |
| 桥接 Bridge | 抽象与实现各自独立扩展 | 两个维度正交,如"形状 × 颜色" |
| 组合 Composite | 树形结构,部分与整体一致对待 | 菜单/部门树,add(Component) |
| 装饰器 Decorator | 不修改原类,动态叠加职责 | 同接口包装,如 BufferedInputStream |
| 外观 Facade | 为子系统提供统一入口 | XxxFacade,一次调用完成多步操作 |
| 享元 Flyweight | 共享大量细粒度对象 | 池化、Integer 缓存 |
| 代理 Proxy | 控制对对象的访问 | XxxProxy、AOP、懒加载 |
结构型的共同目标:在不改变现有结构的前提下,通过组合或包装获得更灵活的对象结构。
4.1 适配器 Adapter
意图:将一个类的接口转换成客户端期望的另一个接口,使原本因接口不兼容而无法协作的类可以一起工作。
Client ──调用──> Target(DC5V 接口)
△
│ 实现
PowerAdapter ──持有──> Adaptee(AC220V)
output5V() output220V()
分为两种实现:类适配器通过继承被适配者(Java 单继承下受限),对象适配器通过组合持有被适配者(推荐)。
以手机充电为例:插座输出 220V 交流电,手机只接受 5V 直流电,中间的充电头就是适配器。
// 目标接口:手机需要的 5V 直流
public interface DC5V { int output5V(); }
// 被适配者:家用插座,220V 交流
public class AC220V {
public int output220V() { return 220; }
}
// 适配器(充电头):把 220V 转成 5V
public class PowerAdapter implements DC5V {
private final AC220V ac = new AC220V();
@Override
public int output5V() {
int input = ac.output220V();
return input / 44; // 降压转换
}
}
public class Phone {
public void charge(DC5V power) {
System.out.println("充电电压:" + power.output5V() + "V");
}
}
new Phone().charge(new PowerAdapter()); // 充电电压:5V
注意点:
- 优先对象适配器(组合),避免继承带来的强耦合;Java 单继承下类适配器无法同时适配多个被适配者。
- 适配器是事后补救手段,用于整合遗留系统或三方 SDK;新系统在设计阶段应直接统一接口,而不是靠适配器兜底。
- 与装饰器的区别:适配器改接口,装饰器加职责且接口不变。
- 既有实例:
Arrays.asList()、InputStreamReader(字节流转字符流)、Spring MVC 的HandlerAdapter。
4.2 桥接 Bridge
意图:将抽象与实现分离,使两者可以独立变化,用组合替代多层继承。
Abstraction ──── 持有 ────> Implementor(接口)
△ △
│ 继承 │ 实现
RefinedAbstraction ConcreteImplementorA / B
维度一(形状) 维度二(颜色)
圆形 ─┐ ┌─ 红色
方形 ─┼───── 自由组合 ───────────┼─ 蓝色
三角形 ─┘ └─ 绿色
以"形状 × 颜色"为例:用继承需要 RedCircle、BlueSquare… 共 M×N 个类;用桥接只需 M+N 个类。
// 实现维度:颜色
public interface Color { String name(); }
public class Red implements Color { public String name() { return "红色"; } }
public class Blue implements Color { public String name() { return "蓝色"; } }
// 抽象维度:形状,持有 Color 引用(这条引用就是"桥")
public abstract class Shape {
protected final Color color;
protected Shape(Color color) { this.color = color; }
public abstract void draw();
}
public class Circle extends Shape {
public Circle(Color color) { super(color); }
public void draw() { System.out.println("画出" + color.name() + "的圆形"); }
}
public class Square extends Shape {
public Square(Color color) { super(color); }
public void draw() { System.out.println("画出" + color.name() + "的方形"); }
}
// 形状与颜色各自扩展,自由组合
new Circle(new Red()).draw(); // 画出红色的圆形
new Square(new Blue()).draw(); // 画出蓝色的方形
注意点:
- 与适配器的区别:桥接是设计初期的预防性结构,适配器是事后兼容;一个为将来,一个为过去。
- 判断标准:如果用继承会产生类爆炸(M×N 个子类),就该换成桥接。
- 抽象层持有的那条实现接口引用就是"桥",两个维度沿各自方向扩展,互不影响。
4.3 组合 Composite
意图:将对象组合成树形结构以表示"部分-整体"层次,使客户端对单个对象和组合对象的使用保持一致。
Component(统一接口)
△ △
│ │
Leaf Composite ──children──◇ Component
(File 文件) (Directory 文件夹) (递归包含自身)
以文件系统为例:文件和文件夹都是"节点",文件夹里还能放文件夹,客户端用同一套 ls() 遍历整棵树。
public interface FileSystemNode {
void ls(); // 统一操作
}
public class File implements FileSystemNode { // 叶子
private final String name;
public File(String name) { this.name = name; }
public void ls() { System.out.println(name); }
}
public class Directory implements FileSystemNode { // 组合节点
private final String name;
private final List<FileSystemNode> children = new ArrayList<>();
public Directory(String name) { this.name = name; }
public void add(FileSystemNode node) { children.add(node); }
public void ls() {
System.out.println("[" + name + "]");
children.forEach(FileSystemNode::ls); // 递归,不区分文件与文件夹
}
}
Directory root = new Directory("root");
root.add(new File("README.md"));
Directory src = new Directory("src");
src.add(new File("Main.java"));
root.add(src);
root.ls(); // [root] / README.md / [src] / Main.java
注意点:
- 透明方式:
add/remove声明在Component上,客户端完全统一,但叶子节点只能抛UnsupportedOperationException。 - 安全方式:
add/remove只声明在Composite上,编译期即安全,但客户端需要区分类型。 - 适用于菜单、部门树、组织架构、XML/JSON 树、表达式树等天然递归结构。
- 注意树深度过大导致的递归栈溢出,以及父子互相引用造成的环。
4.4 装饰器 Decorator
意图:动态地给对象增加职责,相比继承更灵活,且不改变原有接口。
Component(统一接口)
△ △
│ │
ConcreteComponent Decorator ──持有──◇ Component
(Espresso) (Condiment 配料基类)
△
┌────────────┴────────────┐
(Mocha 摩卡) (Milk 牛奶)
计价链:Mocha → Milk → Espresso,每一层累加自己的价格
以咖啡点单为例:一杯浓缩,加摩卡、加牛奶,每加一样就多一层包装,价格和描述逐层累加。
public interface Beverage {
String desc();
double cost();
}
public class Espresso implements Beverage { // 被装饰的原始对象
public String desc() { return "浓缩咖啡"; }
public double cost() { return 20.0; }
}
public abstract class Condiment implements Beverage { // 装饰器基类,持有饮品
protected final Beverage beverage;
protected Condiment(Beverage beverage) { this.beverage = beverage; }
}
public class Mocha extends Condiment {
public Mocha(Beverage b) { super(b); }
public String desc() { return beverage.desc() + " + 摩卡"; }
public double cost() { return beverage.cost() + 5.0; }
}
public class Milk extends Condiment {
public Milk(Beverage b) { super(b); }
public String desc() { return beverage.desc() + " + 牛奶"; }
public double cost() { return beverage.cost() + 3.0; }
}
Beverage order = new Mocha(new Milk(new Espresso()));
System.out.println(order.desc()); // 浓缩咖啡 + 牛奶 + 摩卡
System.out.println(order.cost()); // 28.0
注意点:
- 装饰器与被装饰对象实现同一接口,可无限嵌套,客户端无感知。
- Java IO 是标准实例:
new BufferedInputStream(new FileInputStream(path))。 - 与代理的区别:装饰器关注增强功能,通常层层叠加;代理关注控制访问,一般只包一层。
- 代价:会产生大量小类,且问题排查时调用链较长。
4.5 外观 Facade
意图:为子系统中的一组接口提供统一的高层入口,降低客户端与子系统的耦合。
Client ──> HomeTheaterFacade ────> Light(灯光)
├─────────────────> Projector(投影仪)
├─────────────────> SoundSystem(音响)
└─────────────────> DvdPlayer(播放器)
(客户端只调 watchMovie();子系统之间仍可互相调用)
以家庭影院为例:看电影要依次调灯光、投影仪、音响、播放器,客户端只想要一个"开始观影"按钮。
// 子系统:各自独立,互不关心调用顺序
public class Light { public void dim(int level) {} }
public class Projector { public void on() {} public void off() {} }
public class SoundSystem { public void on() {} public void off() {} }
public class DvdPlayer { public void play() {} public void stop() {} }
// 外观:把四个设备的操作封装成两个动作
public class HomeTheaterFacade {
private final Light light = new Light();
private final Projector projector = new Projector();
private final SoundSystem sound = new SoundSystem();
private final DvdPlayer dvd = new DvdPlayer();
public void watchMovie() {
light.dim(10);
projector.on();
sound.on();
dvd.play();
}
public void endMovie() {
dvd.stop();
sound.off();
projector.off();
}
}
new HomeTheaterFacade().watchMovie();
注意点:
- 外观不禁止客户端直接访问子系统,只是提供一个更省事的入口,这是它与"包装"的本质区别。
- 与中介者的区别:外观是单向封装(客户端 → 子系统),中介者是双向协调(子系统内部对象互相通信)。
- 常见于服务聚合层、API Gateway、SDK 统一入口。
4.6 享元 Flyweight
意图:运用共享技术复用大量细粒度对象,减少内存开销。核心是区分内部状态(可共享、不可变)与外部状态(随场景变化、由客户端传入)。
TreeTypeFactory
└── pool: Map<key, TreeType> ← 内部状态:树种名 / 颜色 / 纹理(共享、不可变)
Tree(外部状态:x, y 坐标)──> TreeType.draw(x, y)
以森林渲染为例:10 万棵树只有几十种"树种",名称、颜色、纹理完全相同,只有坐标不同。把树种共享,坐标留在外部。
public final class TreeType { // 内部状态:可共享
private final String name;
private final String color;
public TreeType(String name, String color) { this.name = name; this.color = color; }
public void draw(int x, int y) { // x/y 为外部状态,调用时传入
System.out.printf("在 (%d,%d) 画一棵%s的%s%n", x, y, color, name);
}
}
public class TreeTypeFactory {
private static final Map<String, TreeType> CACHE = new ConcurrentHashMap<>();
public static TreeType get(String name, String color) {
return CACHE.computeIfAbsent(name + color, k -> new TreeType(name, color));
}
}
public class Tree { // 外部状态:每棵树各自持有
private final int x, y;
private final TreeType type;
public Tree(int x, int y, TreeType type) { this.x = x; this.y = y; this.type = type; }
public void draw() { type.draw(x, y); }
}
Tree t1 = new Tree(10, 20, TreeTypeFactory.get("松树", "绿"));
Tree t2 = new Tree(11, 25, TreeTypeFactory.get("松树", "绿"));
// 两棵树是不同的 Tree 对象,但共享同一个 TreeType 实例
System.out.println(TreeTypeFactory.get("松树", "绿") == TreeTypeFactory.get("松树", "绿")); // true
注意点:
- 只有存在大量高度重复的对象、且内部状态可共享时才值得引入,否则工厂与状态拆分的复杂度得不偿失。
- 享元对象必须不可变(至少内部状态不可变),否则共享即串味。
- 既有实例:
Integer.valueOf()的 -128~127 缓存、String常量池、数据库连接池、线程池。
4.7 代理 Proxy
意图:为对象提供一种代理,以控制对它的访问。
Client(租客)──> Subject(RentHouse 接口)
△
┌────────────┴────────────┐
Agent(中介) Landlord(房东)
──持有──> Landlord
(控制访问:带看房 / 签合同 / 收中介费)
以租房为例:租客只跟中介打交道,中介在房东的签约动作前后插入带看、收费等逻辑,租客不需要直接接触房东。
public interface RentHouse { void rent(); }
public class Landlord implements RentHouse { // 真实对象
public void rent() { System.out.println("房东签合同,交付钥匙"); }
}
public class Agent implements RentHouse { // 代理,持有真实对象
private final Landlord landlord = new Landlord();
public void rent() {
System.out.println("中介带看房屋"); // 前置增强
landlord.rent();
System.out.println("中介收取服务费"); // 后置增强
}
}
RentHouse house = new Agent();
house.rent(); // 中介带看房屋 / 房东签合同,交付钥匙 / 中介收取服务费
上面的写法需要为每个接口手写一个代理类。JDK 动态代理可以在运行时生成,框架里更常见:
RentHouse proxy = (RentHouse) Proxy.newProxyInstance(
Landlord.class.getClassLoader(),
new Class[]{RentHouse.class},
(p, method, args) -> {
System.out.println("before:" + method.getName());
return method.invoke(new Landlord(), args);
});
proxy.rent();
注意点:
- JDK 动态代理要求目标实现接口;CGLib 通过子类化代理,无法代理
final类和方法。 - Spring AOP 正是基于此:有接口走 JDK 代理,无接口走 CGLib(Spring Boot 2.x 起默认 CGLib)。
- 与装饰器的区别:代理通常在框架层确定,用于控制访问;装饰器由客户端显式层层包裹,用于增强功能。
- 自调用失效:类内部方法互调不会经过代理对象,
@Transactional、@Cacheable会静默失效。
五、行为型模式
| 模式 | 意图 | 辨识特征 |
|---|---|---|
| 责任链 Chain of Responsibility | 请求沿链传递,由能处理者处理 | Filter、拦截器链、setNext() |
| 命令 Command | 将请求封装为对象 | 撤销/重做、任务队列 |
| 解释器 Interpreter | 定义语法并解释执行 | 表达式解析、规则引擎 |
| 迭代器 Iterator | 不暴露内部结构地遍历集合 | Iterator、for-each |
| 中介者 Mediator | 用中介对象封装对象间交互 | 消息中心、MVC 的 Controller |
| 备忘录 Memento | 保存并恢复对象状态 | 快照、撤销、存档 |
| 观察者 Observer | 一对多依赖,状态变化自动通知 | 发布订阅、事件监听、MQ |
| 状态 State | 状态变化引起行为变化 | 订单状态机,替代条件分支 |
| 策略 Strategy | 算法族可互换 | 支付方式、排序算法 |
| 模板方法 Template Method | 固定流程骨架,步骤交由子类 | 抽象父类 + 钩子方法 |
| 访问者 Visitor | 不改动类结构的前提下新增操作 | 复杂对象结构的统计与导出 |
行为型的共同目标:在对象之间划分职责、安排通信方式,让变化的部分各自独立,避免把控制逻辑堆进一个类。
5.1 责任链 Chain of Responsibility
意图:让多个处理者依次有机会处理请求,请求沿链传递直到被处理,从而解耦发送者与接收者。
Client ──> Handler ──next──> Handler ──next──> Handler
(组长 ≤1天) (经理 ≤3天) (总监 ≤7天)
请求沿链前进:能处理就返回,处理不了交给下一个,链尾兜底
以请假审批为例:1 天以内组长批,3 天以内经理批,7 天以内总监批,申请人不需要知道该找谁。
public abstract class Approver {
protected Approver next;
public Approver setNext(Approver next) { this.next = next; return next; }
public abstract void handle(int days);
}
public class GroupLeader extends Approver {
public void handle(int days) {
if (days <= 1) System.out.println("组长批准 " + days + " 天假");
else if (next != null) next.handle(days);
}
}
public class Manager extends Approver {
public void handle(int days) {
if (days <= 3) System.out.println("经理批准 " + days + " 天假");
else if (next != null) next.handle(days);
}
}
public class Director extends Approver {
public void handle(int days) {
if (days <= 7) System.out.println("总监批准 " + days + " 天假");
else System.out.println(days + " 天假,不予批准"); // 链尾兜底
}
}
Approver leader = new GroupLeader();
leader.setNext(new Manager()).setNext(new Director());
leader.handle(5); // 经理批准 5 天假
注意点:
- 责任链不保证请求一定被处理,链尾必须有兜底(拒绝或默认处理)。
- 纯责任链:恰好一个节点处理;不纯责任链:每个节点都做部分处理,如 Filter 链的前后置逻辑。
- 既有实例:Servlet
Filter、HandlerInterceptor、Sentinel 的 Slot 链、Netty 的ChannelPipeline。 - 链过长会拖慢响应且难以排查,节点数量要控制。
5.2 命令 Command
意图:将请求封装成对象,使请求可以被排队、记录、撤销和重做。
Client ──> Invoker(遥控器)──持有──> Command ──持有──> Receiver(电灯)
△
LightOnCommand / LightOffCommand
以遥控器为例:按钮只认识"命令",不关心控制的是电灯还是电视;命令对象持有真正干活的接收者。
public interface Command { void execute(); void undo(); }
public class Light { // Receiver:真正执行
public void on() { System.out.println("灯开了"); }
public void off() { System.out.println("灯关了"); }
}
public class LightOnCommand implements Command { // ConcreteCommand
private final Light light;
public LightOnCommand(Light light) { this.light = light; }
public void execute() { light.on(); }
public void undo() { light.off(); } // 逆操作,支撑撤销
}
public class RemoteControl { // Invoker:只依赖 Command
private Command command;
public void setCommand(Command c) { this.command = c; }
public void press() { command.execute(); }
public void pressUndo() { command.undo(); }
}
Light light = new Light();
RemoteControl rc = new RemoteControl();
rc.setCommand(new LightOnCommand(light));
rc.press(); // 灯开了
rc.pressUndo(); // 灯关了
注意点:
- 命令把"做什么"与"谁来做"解耦,
Invoker不需要知道Receiver的存在;换台电视只需换一个 Command。 - 支持撤销的关键是命令要么提供逆操作,要么保存执行前的状态快照。
- 可用于任务队列、异步执行、批量执行、事务回滚、操作日志回放。
- 代价:每个操作一个类,命令类数量会膨胀。
5.3 解释器 Interpreter
意图:给定一门语言,定义其语法的表示,并提供解释器来解释该语言中的句子。
Expression(抽象表达式)
△ △
TerminalExpression NonTerminalExpression(Add / Subtract)
(数字,不再拆分) (持有左右两个 Expression,递归解释)
以四则运算为例:数字是终结符,加减是非终结符,构成一棵语法树后递归求值。
public interface Expression { int interpret(); }
public class Number implements Expression { // 终结符表达式
private final int value;
public Number(int value) { this.value = value; }
public int interpret() { return value; }
}
public class Add implements Expression { // 非终结符表达式
private final Expression left, right;
public Add(Expression left, Expression right) { this.left = left; this.right = right; }
public int interpret() { return left.interpret() + right.interpret(); }
}
public class Subtract implements Expression {
private final Expression left, right;
public Subtract(Expression left, Expression right) { this.left = left; this.right = right; }
public int interpret() { return left.interpret() - right.interpret(); }
}
// 表达式:10 + 5 - 3
Expression expr = new Subtract(new Add(new Number(10), new Number(5)), new Number(3));
System.out.println(expr.interpret()); // 12
注意点:
- 只适合简单且稳定的语法;语法复杂时应改用 ANTLR、JavaCC 等解析器生成器。
- 上例为便于演示直接手工搭建语法树,实际使用中语法树应由解析器解析字符串后构造。
- 类数量随语法规则线性增长,递归解释的执行效率也低于手写解析。
- 既有实例:
java.util.regex.Pattern、SpEL、SQL 解析、规则引擎的条件表达式。
5.4 迭代器 Iterator
意图:提供一种顺序访问聚合对象元素的方法,又不暴露聚合对象的内部结构。
Aggregate(BookShelf)──createIterator()──> Iterator
△
ConcreteIterator(内部维护游标 index)
以遍历书架为例:客户端拿到迭代器就能逐本取书,无需知道书架底层是数组还是 List。
public interface Iterator<T> { boolean hasNext(); T next(); }
public class BookShelf { // 聚合对象
private final List<String> books = new ArrayList<>();
public void add(String book) { books.add(book); }
public Iterator<String> createIterator() {
return new Iterator<String>() {
private int index = 0;
public boolean hasNext() { return index < books.size(); }
public String next() { return books.get(index++); }
};
}
}
BookShelf shelf = new BookShelf();
shelf.add("设计模式");
shelf.add("重构");
Iterator<String> it = shelf.createIterator();
while (it.hasNext()) System.out.println(it.next());
注意点:
- 遍历与存储分离,底层容器从数组换成链表也不影响遍历代码。
- Java 已内建
java.util.Iterator/Iterable,实现后者即可支持for-each。 - 遍历过程中结构性修改集合会触发
ConcurrentModificationException(fail-fast 机制)。
5.5 中介者 Mediator
意图:用一个中介对象封装一组对象之间的交互,减少它们之间的直接相互引用。
UserA ──┐ ┌── UserB
├──> ChatRoom(中介者)<──┤ (用户之间不再互相持有引用)
UserC ──┘ └── UserD
以聊天室为例:小王发一条消息,由聊天室转发给房间里的其他人;小王不需要认识小李、小张,也不需要知道房间里到底有谁。
public class ChatRoom { // 中介者:唯一知道所有人的地方
private final List<User> users = new ArrayList<>();
public void register(User u) { users.add(u); }
public void send(String from, String msg) { // 转发给除发送者外的所有人
users.stream()
.filter(u -> !u.getName().equals(from))
.forEach(u -> u.receive(from, msg));
}
}
public class User { // 同事类:只认识聊天室
private final String name;
private final ChatRoom room;
public User(String name, ChatRoom room) {
this.name = name;
this.room = room;
room.register(this);
}
public String getName() { return name; }
public void send(String msg) { room.send(name, msg); }
public void receive(String from, String msg) {
System.out.println(name + " 收到 " + from + " 的消息:" + msg);
}
}
ChatRoom room = new ChatRoom();
User wang = new User("小王", room);
new User("小李", room);
new User("小张", room);
wang.send("晚上聚餐吗?");
// 小李 收到 小王 的消息:晚上聚餐吗?
// 小张 收到 小王 的消息:晚上聚餐吗?
注意点:
- 网状依赖变星形依赖,对象间引用从 N×N 降到 N。
- 与外观的区别:中介者是双向协调(同事对象之间互相通信),外观是单向封装。
- 风险:中介者容易膨胀成上帝类,需按业务场景拆分。
- 既有实例:MQ 的 Broker、服务注册中心、MVC 中的 Controller。
5.6 备忘录 Memento
意图:在不破坏封装的前提下捕获对象的内部状态,并在需要时恢复到该状态。
Originator(编辑器)──save()──> Memento(快照,不可变)
▲ │
└────── restore(m) ──────────┘
▲
Caretaker(History 历史栈)
以文本编辑器撤销为例:每次输入前存一份快照,Ctrl+Z 时从历史栈弹出恢复。
public class Editor { // 原发器
private String content = "";
public void type(String words) { content += words; }
public Memento save() { return new Memento(content); }
public void restore(Memento m) { this.content = m.getContent(); }
public String getContent() { return content; }
}
public class Memento { // 备忘录:只读快照
private final String content;
Memento(String content) { this.content = content; }
String getContent() { return content; }
}
public class History { // 管理者:保存快照栈
private final Deque<Memento> stack = new ArrayDeque<>();
public void push(Memento m) { stack.push(m); }
public Memento pop() { return stack.pop(); }
}
Editor editor = new Editor();
History history = new History();
editor.type("第一段");
history.push(editor.save());
editor.type("第二段");
editor.restore(history.pop());
System.out.println(editor.getContent()); // 第一段
注意点:
- 备忘录必须不可变,否则恢复时拿到的是被后续修改过的引用,等于没恢复。
- 快照频繁、对象体积大时内存开销高,可改为增量快照或只存 diff。
- 既有实例:数据库事务的 undo log、游戏存档、编辑器的撤销栈。
5.7 观察者 Observer
意图:定义对象间一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象自动收到通知。
Subject(公众号)──publish()──> Observer(读者A)
▲ 订阅 / 退订 Observer(读者B)
Observer(读者C)
以公众号推文为例:读者订阅后,公众号一发文就推送给所有订阅者。
public interface Observer { void update(String article); }
public class OfficialAccount { // 被观察者
private final List<Observer> readers = new ArrayList<>();
public void subscribe(Observer o) { readers.add(o); }
public void unsubscribe(Observer o) { readers.remove(o); }
public void publish(String article) {
readers.forEach(o -> o.update(article)); // 通知所有订阅者
}
}
public class Reader implements Observer {
private final String name;
public Reader(String name) { this.name = name; }
public void update(String article) {
System.out.println(name + " 收到推文:" + article);
}
}
OfficialAccount account = new OfficialAccount();
account.subscribe(new Reader("小王"));
account.publish("23 种设计模式");
注意点:
- 推模型(直接把数据传过去)与拉模型(只发通知,订阅者自行查询)的取舍。
- 对象销毁前必须解除注册,否则被观察者持有引用会造成内存泄漏。
- 同步通知会拖慢主流程,且任一订阅者异常会中断整条通知链;生产环境多用 MQ 异步解耦。
- 既有实例:
ApplicationEvent、MQ 发布订阅、Vue 的响应式系统。
5.8 状态 State
意图:允许对象在内部状态改变时改变其行为,把与状态相关的逻辑局部化到各自的状态类中。
Context(订单)
──持有──> State(接口)
△
┌─────────────────┼─────────────────┐
PendingState PaidState ShippedState
pay() → 切 Paid ship() → 切 Shipped 终态,无下一步
以订单状态机为例:不同状态下可执行的操作不同,且操作完成后自动迁移到下一状态。
public interface OrderState { void next(Order order); String name(); }
public class PendingState implements OrderState {
public void next(Order order) { order.setState(new PaidState()); }
public String name() { return "待支付"; }
}
public class PaidState implements OrderState {
public void next(Order order) { order.setState(new ShippedState()); }
public String name() { return "待发货"; }
}
public class ShippedState implements OrderState {
public void next(Order order) { System.out.println("已是终态"); }
public String name() { return "已发货"; }
}
public class Order {
private OrderState state = new PendingState();
public void setState(OrderState state) { this.state = state; }
public void next() { state.next(this); }
public void print() { System.out.println("当前状态:" + state.name()); }
}
Order order = new Order();
order.next(); order.print(); // 当前状态:待发货
注意点:
- 状态模式消除的是状态驱动的条件分支,把
if-else转换成多态。 - 与策略模式结构几乎相同,区别在于意图:策略由客户端主动选择且彼此独立;状态由状态自身驱动迁移,状态之间互相知晓。
- 迁移逻辑放在 Context 或 State 内均可,放在 State 内更符合开闭原则。
5.9 策略 Strategy
意图:定义一系列可互换的算法并封装起来,使算法的变化独立于使用它的客户端。
Context(购物车)──持有──> Strategy(计价接口)
△
┌───────────────────┼───────────────────┐
NormalStrategy MemberStrategy PromotionStrategy
以购物计价为例:原价、会员折扣、满减活动是三套可切换的算法,客户端按需选择。
public interface PriceStrategy { double calc(double price); }
public class NormalStrategy implements PriceStrategy {
public double calc(double price) { return price; }
}
public class MemberStrategy implements PriceStrategy {
public double calc(double price) { return price * 0.9; } // 会员 9 折
}
public class PromotionStrategy implements PriceStrategy {
public double calc(double price) { return price >= 100 ? price - 20 : price; } // 满 100 减 20
}
public class Cart {
private PriceStrategy strategy = new NormalStrategy();
public void setStrategy(PriceStrategy s) { this.strategy = s; }
public double checkout(double price) { return strategy.calc(price); }
}
Cart cart = new Cart();
cart.setStrategy(new MemberStrategy());
System.out.println(cart.checkout(200)); // 180.0
注意点:
- 策略模式是消灭
if-else/switch分支最直接的工具;配合 Spring 注入Map<String, Strategy>可按类型自动选取。 - 与状态模式结构相似:策略强调替换算法,状态强调行为随状态迁移。
- 客户端需要知道有哪些策略可选,这是它相对于模板方法的取舍点。
5.10 模板方法 Template Method
意图:在父类中定义算法的骨架,把某些步骤延迟到子类实现,使子类不改变结构即可重定义特定步骤。
AbstractClass(冲泡饮料模板)
prepare() ← final,锁定流程
├─ boilWater() 通用步骤,父类实现
├─ brew() abstract,子类实现
├─ pourInCup() 通用步骤,父类实现
└─ addCondiments() abstract / 钩子,子类可选
以泡咖啡和泡茶为例:烧水、倒杯是共用的,冲泡方式和加料各不相同。
public abstract class Beverage {
public final void prepare() { // 模板方法:禁止子类改流程
boilWater();
brew();
pourInCup();
if (needCondiment()) addCondiment(); // 钩子方法决定是否执行
}
private void boilWater() { System.out.println("烧水"); }
private void pourInCup() { System.out.println("倒入杯中"); }
protected abstract void brew();
protected abstract void addCondiment();
protected boolean needCondiment() { return true; }
}
public class Coffee extends Beverage {
protected void brew() { System.out.println("冲泡咖啡粉"); }
protected void addCondiment() { System.out.println("加糖和牛奶"); }
}
public class Tea extends Beverage {
protected void brew() { System.out.println("浸泡茶叶"); }
protected void addCondiment() { System.out.println("加柠檬"); }
protected boolean needCondiment() { return false; } // 茶不加料
}
new Coffee().prepare();
// 烧水 / 冲泡咖啡粉 / 倒入杯中 / 加糖和牛奶
new Tea().prepare();
// 烧水 / 浸泡茶叶 / 倒入杯中 ← 钩子返回 false,跳过了加料
注意点:
- 模板方法用
final锁住流程骨架,防止子类破坏结构,这是它与策略(组合方式)的根本差异。 - 钩子方法(默认返回 true 的空实现)让子类决定是否执行某一步,避免强制重写。
- 既有实例:
JdbcTemplate、HttpServlet.service()、AbstractList。 - 缺点是依赖继承,新增或修改步骤需要改动父类。
5.11 访问者 Visitor
意图:在不改变各元素类的前提下,定义作用于这些元素的新操作。
Element ──accept(Visitor)──> Visitor
△ △
┌──────┴──────┐ ┌────────┴────────┐
Engineer / Manager SalaryVisitor / ReportVisitor
(员工类型稳定) (算薪资 / 出报表,操作可任意新增)
以公司员工为例:员工只有工程师和经理两类,长期不变;但"算薪资""出报表""算绩效"这些操作会不断增加。访问者让这些操作写在外面,员工类一行不改。
public interface Employee { void accept(Visitor v); }
public class Engineer implements Employee { // 元素:工程师
public final String name; public final int kpi;
public Engineer(String name, int kpi) { this.name = name; this.kpi = kpi; }
public void accept(Visitor v) { v.visit(this); }
}
public class Manager implements Employee { // 元素:经理
public final String name; public final int kpi; public final int teamSize;
public Manager(String name, int kpi, int teamSize) {
this.name = name; this.kpi = kpi; this.teamSize = teamSize;
}
public void accept(Visitor v) { v.visit(this); }
}
public interface Visitor {
void visit(Engineer engineer);
void visit(Manager manager);
}
public class SalaryVisitor implements Visitor { // 操作一:算薪资
private int total;
public void visit(Engineer e) { total += 10000 + e.kpi * 100; }
public void visit(Manager m) { total += 15000 + m.kpi * 100 + m.teamSize * 500; }
public int getTotal() { return total; }
}
public class ReportVisitor implements Visitor { // 操作二:出报表(新增,员工类不动)
private final StringBuilder sb = new StringBuilder();
public void visit(Engineer e) { sb.append("工程师 ").append(e.name).append('\n'); }
public void visit(Manager m) { sb.append("经理 ").append(m.name).append(",带 ").append(m.teamSize).append(" 人\n"); }
public String getReport() { return sb.toString(); }
}
List<Employee> staff = List.of(new Engineer("小王", 80), new Manager("老李", 90, 5));
SalaryVisitor salary = new SalaryVisitor();
ReportVisitor report = new ReportVisitor();
staff.forEach(e -> { e.accept(salary); e.accept(report); });
System.out.println(salary.getTotal()); // 44500
System.out.print(report.getReport());
// 工程师 小王
// 经理 老李,带 5 人
注意点:
- 双分派:
accept()依据元素实际类型选择,visit()依据访问者选择,两次动态绑定确定最终操作。 - 适合元素类稳定、操作频繁新增的场景;反过来新增一种元素就要修改所有 Visitor 接口,代价很大。
- 既有实例:ASM 的
ClassVisitor、编译器的 AST 遍历、java.nio.file.FileVisitor。
Spring 本身就是设计模式的实例集合:
FactoryBean(工厂方法)、AOP(代理)、JdbcTemplate(模板方法)、ApplicationEvent(观察者)、HandlerInterceptor(责任链)、BeanWrapper(装饰器)。


浙公网安备 33010602011771号