设计模式(Design Pattern)是软件设计领域中被反复使用、多数人知晓、经过分类编目、代码设计经验的总结。它不是一段可以直接拷贝的代码,而是在特定场景下解决某一类问题的成熟方案——你可以把它理解成”前人踩坑后沉淀下来的套路”。一套好的设计模式能让你在面对复杂系统时少走弯路:它降低了团队成员之间的沟通成本(提到”工厂方法”大家都知道你在说什么),也让代码结构更清晰、更易于扩展和维护。
为什么学习设计模式? 因为在真实的工程实践中,”能跑起来”只是最低要求。随着业务迭代,缺乏设计的代码会迅速演变成谁都不敢碰的”大泥球”(Big Ball of Mud)。设计模式帮助你识别可变与不变的部分,把变化隔离,把稳定固化。它训练的是一种抽象思维——学会用接口而非实现编程,用组合而非继承搭建结构。当然,设计模式不是银弹,过度使用同样会把简单问题复杂化。真正掌握设计模式的人,既知道何时使用,也知道何时不使用。
本系列文章将系统梳理 23 种经典设计模式,附以完整可运行的 Java 代码示例。先从设计模式的基础概念、分类与七大原则讲起,再逐一拆解创建型模式中的单例模式与工厂模式族。
设计模式概述
什么是设计模式
设计模式的概念最早源自建筑学。1977 年,建筑师 Christopher Alexander 在《A Pattern Language》一书中提出,建筑中存在一些反复出现的设计要素,可以被归纳为”模式”。这一思想后来被软件工程界借鉴——人们发现,软件设计中也存在大量反复出现的问题结构和对应的解决方案。
1994 年,四位作者 Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides 合著了《Design Patterns: Elements of Reusable Object-Oriented Software》一书,系统整理了 23 种经典面向对象设计模式。这本书被业界亲切地称为”GoF(Gang of Four)之书”,四位作者也得名”四人帮”。从此,设计模式正式成为面向对象设计领域的核心知识体系。
每一个设计模式都包含三个基本要素(三要素):
- 模式名称(Name):用一两个词概括模式,便于交流。比如”单例”、”观察者”。
- 问题(Problem):描述何时使用该模式,即在什么场景下、解决什么设计问题。
- 解决方案(Solution):描述设计的组成结构、各部分之间的关系及协作方式,通常以类图和代码呈现。
除了三要素,完整的模式描述通常还包括”效果(Consequence)”——即采用该模式带来的权衡与 trade-off,帮助你判断是否值得使用。
设计模式的分类
GoF 将 23 种设计模式按其关注点分为三大类:
1. 创建型模式(Creational Patterns)——关注”对象的创建”
这类模式将对象的创建与使用分离,隐藏创建细节,让系统不依赖于对象如何被创建、组合和表示。包含以下 5 种:
| 模式 | 简述 |
|---|---|
| 单例模式(Singleton) | 确保一个类只有一个实例,并提供全局访问点 |
| 工厂方法模式(Factory Method) | 定义创建对象的接口,让子类决定实例化哪个类 |
| 抽象工厂模式(Abstract Factory) | 提供创建一系列相关对象的接口,无需指定具体类 |
| 建造者模式(Builder) | 将复杂对象的构建与表示分离,同样的构建过程可创建不同表示 |
| 原型模式(Prototype) | 通过拷贝现有实例来创建新对象,避免重新初始化 |
2. 结构型模式(Structural Patterns)——关注”对象的组合”
这类模式处理类或对象的组合,通过继承或组合的方式获得新功能,形成更大的结构。包含以下 7 种:
| 模式 | 简述 |
|---|---|
| 适配器模式(Adapter) | 将一个类的接口转换成客户端期望的另一个接口 |
| 桥接模式(Bridge) | 将抽象部分与实现部分分离,使它们可以独立变化 |
| 组合模式(Composite) | 将对象组合成树形结构,统一对待单个对象和组合对象 |
| 装饰器模式(Decorator) | 动态地给对象添加职责,比生成子类更灵活 |
| 外观模式(Facade) | 为子系统中的一组接口提供一个一致的界面 |
| 享元模式(Flyweight) | 运用共享技术有效支持大量细粒度对象 |
| 代理模式(Proxy) | 为其他对象提供代理以控制对这个对象的访问 |
3. 行为型模式(Behavioral Patterns)——关注”对象间的通信”
这类模式关注对象之间的职责划分和算法交互,描述类或对象之间怎样协作完成单个对象无法独自完成的任务。包含以下 11 种:
| 模式 | 简述 |
|---|---|
| 策略模式(Strategy) | 定义一系列算法,封装起来,使它们可以互相替换 |
| 模板方法模式(Template Method) | 定义算法骨架,将某些步骤延迟到子类实现 |
| 观察者模式(Observer) | 定义对象间一对多依赖,当一个对象状态变化时通知所有依赖者 |
| 迭代器模式(Iterator) | 提供一种方法顺序访问聚合对象中的元素,不暴露其内部表示 |
| 责任链模式(Chain of Responsibility) | 将请求沿处理者链传递,直到有一个处理者处理它 |
| 命令模式(Command) | 将请求封装为对象,从而可用不同请求对客户进行参数化 |
| 备忘录模式(Memento) | 在不破坏封装的前提下,捕获并保存对象内部状态 |
| 状态模式(State) | 允许对象在内部状态改变时改变其行为 |
| 访问者模式(Visitor) | 在不改变元素类的前提下定义作用于这些元素的新操作 |
| 中介者模式(Mediator) | 用中介对象封装一系列对象交互,使各对象不需显式引用 |
| 解释器模式(Interpreter) | 给定一个语言,定义其文法表示,并定义一个解释器解释句子 |
设计模式的七大原则
设计模式并不是凭空发明的,每一种模式都是为了让代码更好地遵循某些设计原则。下面这七大原则是面向对象设计的基石,理解了它们,再回头看每个设计模式,你会恍然大悟——原来它的”形状”是原则塑造的。
1. 单一职责原则(Single Responsibility Principle, SRP)
简述:一个类应该只有一个引起它变化的原因。换句话说,一个类只负责一项职责。
问题在于,当一个类承担了过多职责时,任何一个职责的变化都可能影响其他职责,导致”牵一发而动全身”。下面是一个违反该原则的反面示例:
// 反面示例:一个类既管用户数据又管用户格式化展示 —— 职责过多
class UserService {
// 职责一:用户数据持久化
public void saveUser(String name, String email) {
System.out.println("保存用户到数据库: " + name + ", " + email);
}
// 职责二:格式化用户信息 —— 这应该是另一个类的职责
public String formatUserReport(String name, String email) {
return "用户报告: 姓名=" + name + ", 邮箱=" + email;
}
}
拆分后各自只负责一件事:
// 正面示例:按职责拆分
class UserRepository {
public void saveUser(String name, String email) {
System.out.println("保存用户到数据库: " + name + ", " + email);
}
}
class UserReportFormatter {
public String format(String name, String email) {
return "用户报告: 姓名=" + name + ", 邮箱=" + email;
}
}
2. 接口隔离原则(Interface Segregation Principle, ISP)
简述:客户端不应该被迫依赖它不使用的方法。一个类对另一个类的依赖应建立在最小的接口上。
换种说法:不要设计”胖接口”。如果一个接口包含了太多方法,而某些实现类用不到其中一部分,就应该把接口拆细。
// 反面示例:一个臃肿的接口,鸟类实现类被迫实现它用不到的 swim()
interface Animal {
void eat();
void fly();
void swim();
}
// Bird 根本不会游泳,却被迫实现 swim() —— 接口过胖
class Bird implements Animal {
public void eat() { System.out.println("鸟在吃"); }
public void fly() { System.out.println("鸟在飞"); }
public void swim() { throw new UnsupportedOperationException("鸟不会游泳"); }
}
拆分为细粒度接口:
// 正面示例:按行为拆分接口
interface Eatable { void eat(); }
interface Flyable { void fly(); }
interface Swimmable { void swim(); }
class Bird implements Eatable, Flyable {
public void eat() { System.out.println("鸟在吃"); }
public void fly() { System.out.println("鸟在飞"); }
// 不再被迫实现 swim()
}
3. 依赖倒转原则(Dependency Inversion Principle, DIP)
简述:高层模块不应依赖低层模块,二者都应依赖抽象;抽象不应依赖细节,细节应依赖抽象。
核心思想是”面向接口编程”。传统做法中,高层模块直接依赖低层模块的具体实现,这让高层模块绑死在具体实现上,难以替换。依赖倒转要求把这种依赖”倒过来”,让双方都依赖于抽象接口。
// 反面示例:高层的 OrderService 直接依赖具体的 MySQLRepository,耦合死板
class MySQLRepository {
public void save(String data) { System.out.println("存入 MySQL: " + data); }
}
class OrderService {
private MySQLRepository repo = new MySQLRepository(); // 硬依赖具体实现
public void createOrder(String order) { repo.save(order); }
}
倒转后,高层依赖抽象:
// 正面示例:双方都依赖抽象
interface OrderRepository {
void save(String data);
}
class MySQLRepository implements OrderRepository {
public void save(String data) { System.out.println("存入 MySQL: " + data); }
}
class OrderService {
private final OrderRepository repo; // 依赖抽象接口
public OrderService(OrderRepository repo) { this.repo = repo; } // 通过构造注入
public void createOrder(String order) { repo.save(order); }
}
// 现在想换成 PostgreSQLRepository,只需换注入对象,OrderService 无需改动
4. 里氏替换原则(Liskov Substitution Principle, LSP)
简述:所有引用基类的地方必须能透明地使用其子类对象,而不破坏程序的正确性。
这是 Barbara Liskov 提出的原则:子类必须能够替换掉父类而不产生任何异常或意外行为。它本质上是对继承关系的约束——继承不应只为了复用代码,而应满足 “is-a” 语义。
// 反面示例:经典的"正方形继承长方形"
class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
class Square extends Rectangle {
// 正方形强行重写,让宽高始终相等 —— 破坏了父类的行为契约
public void setWidth(int w) { width = w; height = w; }
public void setHeight(int h) { width = h; height = h; }
}
// 使用方:接收 Rectangle,但传入 Square 时行为异常
class ResizeUtil {
public static void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
// 期望面积 50,但传 Square 时面积变为 100 —— 违反 LSP
assert r.getArea() == 50 : "面积不对,里氏替换被破坏";
}
}
这里 Square 不能安全替换 Rectangle,说明这个继承关系本身就是错误的——正方形并不是长方形的”特化”。正确做法是用单独的类或接口表示,而不是强行用继承。
5. 开闭原则(Open-Closed Principle, OCP)
简述:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
这是七大原则中最核心的一条,其他原则往往是实现开闭原则的手段。含义是:当需求变化时,你应该通过新增代码来扩展功能,而不是修改已有代码——因为修改已有代码可能引入 bug,破坏既有逻辑。
// 反面示例:每加一种形状都要改 GraphicEditor 的 draw 方法 —— 对修改开放
class GraphicEditor {
public void drawShape(Shape s) {
if (s.type == 1) drawRectangle();
else if (s.type == 2) drawCircle();
// 新增三角形?得再改这里 —— 违反开闭原则
}
private void drawRectangle() { System.out.println("画矩形"); }
private void drawCircle() { System.out.println("画圆形"); }
}
class Shape { int type; }
改造后,对扩展开放、对修改关闭:
// 正面示例:通过多态扩展,新增形状不需改动 GraphicEditor
abstract class Shape {
public abstract void draw();
}
class Rectangle extends Shape {
public void draw() { System.out.println("画矩形"); }
}
class Circle extends Shape {
public void draw() { System.out.println("画圆形"); }
}
// 新增三角形只需新增类,无需修改已有代码
class Triangle extends Shape {
public void draw() { System.out.println("画三角形"); }
}
class GraphicEditor {
public void drawShape(Shape s) {
s.draw(); // 对扩展开放,对修改关闭
}
}
6. 迪米特法则(Law of Demeter, LoD)/ 最少知道原则
简述:一个对象应该对其他对象保持最少的了解。一个类只应该与它的直接朋友(成员变量、方法参数、方法返回值中的对象)通信,而不应该跟”陌生人的陌生人”打交道。
通俗讲就是”不要跟陌生人说话”。如果一个类要经过好几层间接引用才能拿到目标对象,说明耦合过深。
// 反面示例:TeamLeader 直接深入到成员的部门的公司地址,链条太长
class Company {
public Address getAddress() { return new Address(); }
}
class Department {
private Company company;
public Company getCompany() { return company; }
}
class Employee {
private Department department;
public Department getDepartment() { return department; }
}
class TeamLeader {
public void printAddress(Employee emp) {
// 一路 .getDepartment().getCompany().getAddress() —— 违反迪米特法则
String city = emp.getDepartment().getCompany().getAddress().city;
System.out.println("公司所在城市: " + city);
}
}
class Address { String city = "北京"; }
改进方式是在 Employee / Department 上暴露需要的方法,而非暴露内部对象链:
// 正面示例:只跟直接朋友通信
class Employee {
private Department department;
public String getCompanyCity() { // 封装掉内部链路
return department.getCompanyCity();
}
}
class Department {
private Company company;
public String getCompanyCity() {
return company.getCity();
}
}
class Company {
private Address address;
public String getCity() { return address.city; }
}
class TeamLeader {
public void printAddress(Employee emp) {
String city = emp.getCompanyCity(); // 只跟直接朋友 Employee 通信
System.out.println("公司所在城市: " + city);
}
}
class Address { String city = "北京"; }
7. 合成复用原则(Composite Reuse Principle, CRP)
简述:优先使用对象组合/聚合,而不是继承来达到复用的目的。
继承是强耦合关系——子类在编译期就绑定了父类实现,父类变化直接影响子类,而且继承破坏了封装(子类能访问父类 protected 成员)。组合则更灵活,可以在运行时动态替换被包含的对象。
// 反面示例:用继承来复用"发声"能力 —— 强耦合
class Animal { public void breathe() { System.out.println("呼吸"); } }
class Dog extends Animal { // 用继承复用 breathe
public void bark() { System.out.println("汪汪"); }
}
用组合代替继承:
// 正面示例:用组合复用,更灵活
class Breather {
public void breathe() { System.out.println("呼吸"); }
}
class Dog {
private Breather breather = new Breather(); // 组合而非继承
public void breathe() { breather.breathe(); }
public void bark() { System.out.println("汪汪"); }
}
// 如果某天 Dog 不再是"呼吸"的实现,而是注入不同策略,组合方式改动极小
七大原则中,开闭原则是总纲——它是面向对象设计”可维护性”的终极目标;单一职责、接口隔离、依赖倒转主要负责降低耦合、提高内聚;里氏替换保证继承体系的正确性;迪米特法则控制对象间的通信广度;合成复用指导复用的手段选择。它们相辅相成,共同构成了设计模式的理论基石。
创建型模式
创建型模式关注的是”如何创建对象”。在面向对象系统中,创建对象看起来很简单(new 一下就行),但当系统变大、对象创建逻辑变复杂时,直接 new 会让调用方与具体类型紧密耦合。创建型模式的作用,就是把对象的创建过程封装起来,让调用方只关心”我要什么”,而不关心”它怎么被造出来”。
单例模式
概念与应用场景
单例模式(Singleton Pattern)是最简单也最广为人知的设计模式之一。它保证一个类在整个应用生命周期中只有一个实例,并提供一个全局访问点。
典型应用场景包括:
- 配置管理:应用配置只读取一份,全局共享。
- 日志管理器:统一日志写入入口,避免多实例并发写冲突。
- 数据库连接池:连接池本身只需一个实例,复用连接资源。
- 线程池:系统级线程池通常共享一个实例。
- 缓存:全局缓存管理器。
单例看似简单,但要在多线程环境下安全实现,且防止反射、序列化攻击,实际上有诸多讲究。下面分别介绍五种常见实现方式。
饿汉式
饿汉式在类加载时就创建实例,天然线程安全(由 JVM 类加载机制保证)。缺点是无论用不用都会创建实例,可能造成资源浪费。
public class SingletonEHan {
// 1. 构造方法私有化,防止外部 new
private SingletonEHan() {
System.out.println("饿汉式实例已创建");
}
// 2. 类加载时即创建唯一实例(static final 保证初始化一次)
private static final SingletonEHan INSTANCE = new SingletonEHan();
// 3. 提供全局访问点
public static SingletonEHan getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("饿汉式单例工作中...");
}
// 测试
public static void main(String[] args) {
SingletonEHan instance1 = SingletonEHan.getInstance();
SingletonEHan instance2 = SingletonEHan.getInstance();
System.out.println("是否同一实例: " + (instance1 == instance2)); // true
instance1.doSomething();
}
}
懒汉式(线程安全)
懒汉式在第一次调用 getInstance() 时才创建实例,达到”懒加载”效果。但普通懒汉式在多线程下不安全,需要加 synchronized。加在方法上的 synchronized 虽然安全,但每次调用都会同步,性能较差。
public class SingletonLazy {
private SingletonLazy() {
System.out.println("懒汉式实例已创建");
}
// 注意:volatile 防止指令重排(配合后续的双重检查锁更关键)
private static SingletonLazy instance;
// 整个方法加 synchronized —— 线程安全但性能差
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
public void doSomething() {
System.out.println("懒汉式单例工作中...");
}
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println("是否同一实例: " + (s1 == s2)); // true
s1.doSomething();
}
}
双重检查锁(Double-Checked Locking)
为了解决懒汉式整方法同步的性能问题,引入双重检查锁(DCL)。它只在第一次创建实例时加锁,后续调用直接返回。这里 volatile 关键字至关重要:new 操作并非原子,它分为”分配内存→初始化对象→引用指向内存”三步,JVM 可能重排序导致其他线程拿到未初始化的半成品,volatile 禁止了这种重排序。
public class SingletonDCL {
private SingletonDCL() {
System.out.println("双重检查锁实例已创建");
}
// volatile 防止指令重排序 —— 这一行是 DCL 正确性的关键
private static volatile SingletonDCL instance;
public static SingletonDCL getInstance() {
// 第一次检查:避免不必要的同步,提升性能
if (instance == null) {
synchronized (SingletonDCL.class) {
// 第二次检查:防止多个线程同时通过第一次检查后重复创建
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
public void doSomething() {
System.out.println("双重检查锁单例工作中...");
}
public static void main(String[] args) {
// 多线程测试
for (int i = 0; i < 5; i++) {
new Thread(() -> {
SingletonDCL s = SingletonDCL.getInstance();
System.out.println(Thread.currentThread().getName()
+ " 拿到实例: " + s.hashCode());
}, "Thread-" + i).start();
}
SingletonDCL s1 = SingletonDCL.getInstance();
SingletonDCL s2 = SingletonDCL.getInstance();
System.out.println("是否同一实例: " + (s1 == s2)); // true
s1.doSomething();
}
}
静态内部类
静态内部类方式利用了 JVM 的类加载机制实现懒加载和线程安全,写法简洁优雅。外部类加载时不会立即加载内部类,只有在调用 getInstance() 时内部类才会被加载并初始化 INSTANCE。JVM 保证类初始化过程的线程安全,因此无需 synchronized。
public class SingletonInner {
private SingletonInner() {
System.out.println("静态内部类实例已创建");
}
// 静态内部类,持有外部类的唯一实例
// 只有在被引用时才会被加载(懒加载),由 JVM 保证线程安全
private static class Holder {
private static final SingletonInner INSTANCE = new SingletonInner();
}
public static SingletonInner getInstance() {
return Holder.INSTANCE; // 触发 Holder 类的加载与初始化
}
public void doSomething() {
System.out.println("静态内部类单例工作中...");
}
public static void main(String[] args) {
SingletonInner s1 = SingletonInner.getInstance();
SingletonInner s2 = SingletonInner.getInstance();
System.out.println("是否同一实例: " + (s1 == s2)); // true
s1.doSomething();
}
}
这种方式兼顾了懒加载、线程安全和高性能,是除枚举外最推荐的写法。唯一不足是它仍可被反射机制破坏。
枚举实现
枚举方式是《Effective Java》作者 Joshua Bloch 强烈推荐的单例实现方式。利用枚举类型的特性,JVM 从底层保证了枚举实例的唯一性,天然线程安全,且自动防御反射攻击和序列化/反序列化导致的重复实例问题。
public enum SingletonEnum {
INSTANCE; // 唯一实例
// 枚举可以有构造方法(默认 private)
SingletonEnum() {
System.out.println("枚举单例实例已创建");
}
public void doSomething() {
System.out.println("枚举单例工作中...");
}
// 枚举单例的标准调用方式:SingletonEnum.INSTANCE.doSomething()
public static void main(String[] args) {
SingletonEnum s1 = SingletonEnum.INSTANCE;
SingletonEnum s2 = SingletonEnum.INSTANCE;
System.out.println("是否同一实例: " + (s1 == s2)); // true
s1.doSomething();
// 枚举天然防御反射攻击:
// Constructor#newInstance() 对枚举类型会直接抛 IllegalArgumentException
// 枚举天然防御序列化攻击:枚举的序列化由 JVM 特殊处理,反序列化不会创建新对象
}
}
五种实现方式对比总结:
| 方式 | 懒加载 | 线程安全 | 防反射 | 防序列化 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | ✗ | ✓ | ✗ | ✗ | ★★★ |
| 懒汉式(synchronized) | ✓ | ✓ | ✗ | ✗ | ★★ |
| 双重检查锁 | ✓ | ✓ | ✗ | ✗ | ★★★★ |
| 静态内部类 | ✓ | ✓ | ✗ | ✗ | ★★★★ |
| 枚举 | ✗ | ✓ | ✓ | ✓ | ★★★★★ |
实践建议:如果没有懒加载需求,直接用枚举最省心;需要懒加载则选静态内部类或双重检查锁。
工厂模式
概念
工厂模式(Factory Pattern)是创建型模式中使用频率最高的一族。它的核心思想是:将对象的创建逻辑封装到工厂类中,调用方通过工厂获取对象,而不直接 new。 这样做的好处是调用方与具体类解耦——当具体实现变化时,只需修改工厂,调用方无需改动。
工厂模式按抽象程度和结构,分为三种:简单工厂模式、工厂方法模式、抽象工厂模式。三者的复杂度递增,适用场景也逐步扩展。
简单工厂模式
简单工厂模式(Simple Factory)又称静态工厂方法模式。它由一个工厂类,根据传入的参数决定创建哪种产品对象。
先定义产品接口和具体产品:
// 产品接口
public interface Shape {
void draw();
}
// 具体产品:圆形
class Circle implements Shape {
public void draw() {
System.out.println("画一个圆形 ○");
}
}
// 具体产品:矩形
class Rectangle implements Shape {
public void draw() {
System.out.println("画一个矩形 □");
}
}
// 具体产品:三角形
class Triangle implements Shape {
public void draw() {
System.out.println("画一个三角形 △");
}
}
然后是简单工厂:
// 简单工厂:根据参数创建对应产品
public class ShapeFactory {
public static Shape createShape(String type) {
if (type == null) {
return null;
}
switch (type.toLowerCase()) {
case "circle":
return new Circle();
case "rectangle":
return new Rectangle();
case "triangle":
return new Triangle();
default:
throw new IllegalArgumentException("未知形状类型: " + type);
}
}
public static void main(String[] args) {
Shape circle = ShapeFactory.createShape("circle");
circle.draw(); // 画一个圆形 ○
Shape rectangle = ShapeFactory.createShape("rectangle");
rectangle.draw(); // 画一个矩形 □
Shape triangle = ShapeFactory.createShape("triangle");
triangle.draw(); // 画一个三角形 △
}
}
分析:简单工厂最大的问题是违背开闭原则——每新增一种产品,就要修改工厂类的 switch/if 分支。当产品种类不多且不常扩展时,简单工厂足够轻量好用;但若产品频繁新增,就需要工厂方法模式登场了。
工厂方法模式
工厂方法模式(Factory Method)对简单工厂做了改进:不再由一个工厂类包揽所有产品的创建,而是为每种产品定义一个专属的工厂类。新增产品时只需新增产品类和对应的工厂类,不修改已有代码——满足开闭原则。
先定义工厂接口:
// 产品接口
public interface Shape {
void draw();
}
// 具体产品
class Circle implements Shape {
public void draw() { System.out.println("画一个圆形 ○"); }
}
class Rectangle implements Shape {
public void draw() { System.out.println("画一个矩形 □"); }
}
// 工厂接口:每个具体工厂只负责创建一种产品
interface ShapeFactory {
Shape createShape();
}
// 具体工厂:圆形工厂
class CircleFactory implements ShapeFactory {
public Shape createShape() { return new Circle(); }
}
// 具体工厂:矩形工厂
class RectangleFactory implements ShapeFactory {
public Shape createShape() { return new Rectangle(); }
}
// 使用
public class FactoryMethodDemo {
public static void main(String[] args) {
ShapeFactory circleFactory = new CircleFactory();
Shape circle = circleFactory.createShape();
circle.draw(); // 画一个圆形 ○
ShapeFactory rectangleFactory = new RectangleFactory();
Shape rectangle = rectangleFactory.createShape();
rectangle.draw(); // 画一个矩形 □
// 新增三角形?只需新增 Triangle 和 TriangleFactory,无需改动任何已有代码
}
}
分析:工厂方法把”判断创建哪种产品”的决策从工厂类内部移到了调用方——调用方选择具体工厂。这带来了良好的扩展性,但类的数量也随之增加(每加一种产品加两个类),且调用方需要知道所有工厂类型。
抽象工厂模式
抽象工厂模式(Abstract Factory)提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。简单工厂创建”一种产品”,工厂方法给”每种产品一个工厂”,抽象工厂则给”每个产品族一个工厂”。
理解”产品族”是关键:比如一套 UI 主题包含按钮、文本框、复选框等多个组件,这些组件属于同一个”族”。抽象工厂的每个工厂能产出整套族的组件,保证风格统一。
以”跨平台 UI 组件”为例,既要支持 Windows 风格,又要支持 Mac 风格:
// ========== 抽象产品:定义一族产品的接口 ==========
public interface Button {
void render();
}
public interface TextField {
void render();
}
// ========== 具体产品:Windows 风格 ==========
class WindowsButton implements Button {
public void render() { System.out.println("渲染 Windows 风格按钮 [Win Button]"); }
}
class WindowsTextField implements TextField {
public void render() { System.out.println("渲染 Windows 风格文本框 [Win TextField]"); }
}
// ========== 具体产品:Mac 风格 ==========
class MacButton implements Button {
public void render() { System.out.println("渲染 Mac 风格按钮 [Mac Button]"); }
}
class MacTextField implements TextField {
public void render() { System.out.println("渲染 Mac 风格文本框 [Mac TextField]"); }
}
// ========== 抽象工厂:定义创建产品族的接口 ==========
public interface GUIFactory {
Button createButton();
TextField createTextField();
}
// ========== 具体工厂:Windows 工厂(产出整套 Windows 组件)==========
class WindowsFactory implements GUIFactory {
public Button createButton() { return new WindowsButton(); }
public TextField createTextField() { return new WindowsTextField(); }
}
// ========== 具体工厂:Mac 工厂(产出整套 Mac 组件)==========
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
public TextField createTextField() { return new MacTextField(); }
}
// ========== 客户端:只依赖抽象,不关心具体风格 ==========
public class Application {
private Button button;
private TextField textField;
// 通过抽象工厂注入一整套组件,保证风格一致
public Application(GUIFactory factory) {
button = factory.createButton();
textField = factory.createTextField();
}
public void paint() {
button.render();
textField.render();
}
public static void main(String[] args) {
System.out.println("=== Windows 风格 ===");
Application winApp = new Application(new WindowsFactory());
winApp.paint();
System.out.println("=== Mac 风格 ===");
Application macApp = new Application(new MacFactory());
macApp.paint();
}
}
输出:
=== Windows 风格 ===
渲染 Windows 风格按钮 [Win Button]
渲染 Windows 风格文本框 [Win TextField]
=== Mac 风格 ===
渲染 Mac 风格按钮 [Mac Button]
渲染 Mac 风格文本框 [Mac TextField]
分析:抽象工厂的精髓在于”族”的约束——WindowsFactory 产出的永远是 Windows 系列组件,不会出现 Win 按钮配 Mac 文本框的混搭。当需要切换整套风格时,只需替换工厂实例。但它的代价是:如果产品族中新增一种产品类型(比如增加 Checkbox),所有抽象工厂和具体工厂都要改——这违反了开闭原则。所以抽象工厂适用于”产品族稳定、产品等级结构可能扩展”的场景,反之则需斟酌。
三种工厂模式对比:
| 特性 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 创建一种产品 | ✓ | ✓ | ✓ |
| 创建产品族 | ✗ | ✗ | ✓ |
| 新增产品类型 | 修改工厂(违反 OCP) | 新增工厂类(满足 OCP) | 修改所有工厂(违反 OCP) |
| 新增产品族 | — | — | 新增工厂类(满足 OCP) |
| 复杂度 | 低 | 中 | 高 |
选型建议:产品少且稳定用简单工厂;产品类型可能扩展用工厂方法;存在产品族概念、需要成套创建用抽象工厂。
结构型模式
结构型模式关注的是如何将类或对象组合成更大的结构,就像搭积木一样,把简单的零件拼装成功能更复杂的整体。这一类模式回答的是“怎么组装”的问题。
代理模式
代理模式(Proxy Pattern)的核心思想是:给一个对象提供一个代理,由代理控制对原对象的访问。你访问的并不是目标对象本身,而是它的“替身”,这个替身可以在调用前后做一些额外的事情——比如权限校验、日志记录、延迟加载、远程调用等。
生活中的例子很直观:你找明星代言,不能直接联系本人,得先经过经纪人;经纪人就是代理,他在你和明星之间加了“谈档期、谈价格”的逻辑。
代理模式分为两类:
- 静态代理:程序员手动编写代理类,在编译期就确定代理关系。简单直观,但每个被代理对象都要写一个代理类,维护成本高。
- 动态代理:在运行时动态生成代理类,无需手写代理代码。JDK 动态代理基于接口实现(
Proxy.newProxyInstance),CGLIB 动态代理基于子类继承。Spring AOP 默认对接口用 JDK 代理,对类用 CGLIB。
静态代理示例
我们以一个“发送短信”的服务为例,代理在发送前后加上日志。
// 1. 共同接口
public interface SmsService {
String send(String phone, String content);
}
// 2. 真实对象(被代理对象)
public class SmsServiceImpl implements SmsService {
@Override
public String send(String phone, String content) {
System.out.println("正在向 " + phone + " 发送短信:" + content);
return "success";
}
}
// 3. 代理对象:持有被代理对象的引用,在调用前后增强
public class SmsServiceProxy implements SmsService {
private final SmsService target;
public SmsServiceProxy(SmsService target) {
this.target = target;
}
@Override
public String send(String phone, String content) {
// 前置增强:记录调用日志
System.out.println("[LOG] 调用 send 方法,参数:phone=" + phone + ", content=" + content);
long start = System.currentTimeMillis();
// 真正调用目标方法
String result = target.send(phone, content);
// 后置增强:记录耗时
long cost = System.currentTimeMillis() - start;
System.out.println("[LOG] send 方法执行完毕,耗时 " + cost + "ms,结果:" + result);
return result;
}
}
// 4. 客户端
public class StaticProxyDemo {
public static void main(String[] args) {
SmsService target = new SmsServiceImpl();
SmsService proxy = new SmsServiceProxy(target);
proxy.send("13800138000", "您的验证码是 9527");
}
}
运行结果:
[LOG] 调用 send 方法,参数:phone=13800138000, content=您的验证码是 9527
正在向 13800138000 发送短信:您的验证码是 9527
[LOG] send 方法执行完毕,耗时 1ms,结果:success
静态代理的缺点很明显:如果 SmsService 有 10 个方法都需要增强,你要在代理类里把 10 个方法都写一遍;如果有多个 Service 都要加日志,又得为每个 Service 写一个代理类。这就引出了动态代理。
JDK 动态代理示例
JDK 动态代理通过实现 InvocationHandler 接口,在 invoke 方法里统一处理所有方法的增强逻辑。代理类由 JVM 在运行时自动生成。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 1. 接口和真实对象(复用上面的 SmsService / SmsServiceImpl)
public interface SmsService {
String send(String phone, String content);
}
public class SmsServiceImpl implements SmsService {
@Override
public String send(String phone, String content) {
System.out.println("正在向 " + phone + " 发送短信:" + content);
return "success";
}
}
// 2. 调用处理器:所有方法调用的增强逻辑集中在这里
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target;
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[LOG] 调用 " + method.getName() + ",参数:" + java.util.Arrays.toString(args));
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long cost = System.currentTimeMillis() - start;
System.out.println("[LOG] " + method.getName() + " 执行完毕,耗时 " + cost + "ms");
return result;
}
}
// 3. 客户端:用 Proxy.newProxyInstance 动态生成代理对象
public class DynamicProxyDemo {
public static void main(String[] args) {
SmsService target = new SmsServiceImpl();
SmsService proxy = (SmsService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 代理需要实现的接口列表
new LoggingInvocationHandler(target) // 调用处理器
);
proxy.send("13800138000", "您的验证码是 9527");
}
}
相比静态代理,动态代理的最大优势是一个 InvocationHandler 可以代理任意接口的任意方法——无论 SmsService 有多少方法,也不论你有几个不同的 Service,日志、耗时统计这些通用逻辑只需要写一次。这正是 Spring AOP 的底层原理:Spring 通过动态代理为你生成切面织入后的对象,你用 @Aspect、@Around 写的通知,最终都会被翻译成 InvocationHandler.invoke(或 CGLIB 的 MethodInterceptor)里的逻辑。
JDK 动态代理有一个限制:被代理对象必须实现至少一个接口。如果目标类没有实现任何接口(比如就是个普通的 class),JDK 代理就无能为力,这时需要用 CGLIB 基于子类继承来代理。
适配器模式
适配器模式(Adapter Pattern)的作用是将一个类的接口转换成客户端期望的另一个接口,让原本因接口不兼容而无法一起工作的类可以协同工作。可以理解为现实世界的“转接头”。
适配器模式有两种实现方式:
- 类适配器:通过继承被适配者类、实现目标接口来实现。Java 不支持多继承,所以这种方式要求被适配者是个类且能被继承。
- 对象适配器:不继承被适配者,而是持有它的引用(组合优于继承)。这种方式更灵活,被适配者可以是接口、也可以是 final 类的实例。
下面以一个实际场景为例:你的笔记本电脑只有 Type-C 接口,但手上有一个传统的 USB-A 接口 U 盘,需要一个 Type-C 转 USB 的转接器才能用。
// 1. 目标接口:客户端期望使用的接口(电脑上的 Type-C 接口)
public interface TypeCPort {
void connectWithTypeC();
}
// 2. 被适配者:已有的、接口不兼容的类(USB-A 接口的 U 盘)
public class UsbDisk {
public void connectWithUsb() {
System.out.println("USB-A 接口 U 盘已连接,开始读取数据...");
}
}
// 3. 对象适配器:持有 UsbDisk 的引用,把 TypeCPort 的调用转接给它
public class TypeCToUsbAdapter implements TypeCPort {
private final UsbDisk usbDisk;
public TypeCToUsbAdapter(UsbDisk usbDisk) {
this.usbDisk = usbDisk;
}
@Override
public void connectWithTypeC() {
System.out.println("Type-C 转 USB 适配器已接入");
// 接口转换:把 Type-C 的调用委托给 USB 接口
usbDisk.connectWithUsb();
}
}
// 4. 客户端代码
public class AdapterDemo {
public static void main(String[] args) {
// 电脑只认 TypeCPort 接口
UsbDisk usbDisk = new UsbDisk();
TypeCPort port = new TypeCToUsbAdapter(usbDisk);
port.connectWithTypeC();
}
}
运行结果:
Type-C 转 USB 适配器已接入
USB-A 接口 U 盘已连接,开始读取数据...
适配器模式在 JDK 中很常见:java.io.InputStreamReader 就是一个适配器,它把字节流 InputStream 适配成了字符流 Reader;java.util.Arrays.asList() 把数组适配成了 List 视图。日常开发中,当你需要复用一个功能正确但接口不匹配的老旧类时,适配器是最经济的选择。
装饰模式
装饰模式(Decorator Pattern)允许在不改变原有对象、也不通过继承的情况下,动态地给一个对象添加额外的职责。它通过创建一个包装对象(装饰器)来包裹真实对象,并在调用时叠加新行为。
装饰模式常被拿来和继承比较:
- 继承是静态的,在编译期就确定扩展关系。每加一层功能就多一个子类,如果功能组合多(比如加奶、加糖、加奶盖、加珍珠……各两种就是十几个子类),会导致“类爆炸”。
- 装饰模式是动态的,可以在运行时用不同的装饰器组合出任意功能搭配,且装饰器之间可以互相嵌套,像俄罗斯套娃一样层层包装。
以咖啡店为例:基础是一杯 Espresso(浓缩咖啡),可以加 Milk(牛奶)、加 Sugar(糖)、加 Whip(奶油),每种加料都额外收费。用装饰模式实现:
// 1. 抽象组件:饮品接口
public interface Beverage {
String getDescription();
double cost();
}
// 2. 具体组件:基础咖啡
public class Espresso implements Beverage {
@Override
public String getDescription() {
return "浓缩咖啡";
}
@Override
public double cost() {
return 15.0;
}
}
// 3. 抽象装饰器:继承 Beverage,并持有一个 Beverage 的引用
public abstract class CondimentDecorator implements Beverage {
protected Beverage beverage; // 被装饰的对象
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
}
// 4. 具体装饰器:牛奶
public class MilkDecorator extends CondimentDecorator {
public MilkDecorator(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + " + 牛奶";
}
@Override
public double cost() {
return beverage.cost() + 3.0; // 牛奶加 3 元
}
}
// 5. 具体装饰器:糖
public class SugarDecorator extends CondimentDecorator {
public SugarDecorator(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + " + 糖";
}
@Override
public double cost() {
return beverage.cost() + 1.0; // 糖加 1 元
}
}
// 6. 具体装饰器:奶油
public class WhipDecorator extends CondimentDecorator {
public WhipDecorator(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + " + 奶油";
}
@Override
public double cost() {
return beverage.cost() + 5.0; // 奶油加 5 元
}
}
// 7. 客户端:用装饰器层层包装
public class DecoratorDemo {
public static void main(String[] args) {
// 先来一杯浓缩咖啡
Beverage coffee = new Espresso();
System.out.println(coffee.getDescription() + " —— ¥" + coffee.cost());
// 加牛奶
Beverage coffeeWithMilk = new MilkDecorator(coffee);
System.out.println(coffeeWithMilk.getDescription() + " —— ¥" + coffeeWithMilk.cost());
// 再加糖
Beverage sweetCoffee = new SugarDecorator(coffeeWithMilk);
System.out.println(sweetCoffee.getDescription() + " —— ¥" + sweetCoffee.cost());
// 还要奶油,一层层套娃
Beverage fullCoffee = new WhipDecorator(sweetCoffee);
System.out.println(fullCoffee.getDescription() + " —— ¥" + fullCoffee.cost());
}
}
运行结果:
浓缩咖啡 —— ¥15.0
浓缩咖啡 + 牛奶 —— ¥18.0
浓缩咖啡 + 牛奶 + 糖 —— ¥19.0
浓缩咖啡 + 牛奶 + 糖 + 奶油 —— ¥24.0
装饰模式在 JDK 中的经典应用是 I/O 体系:BufferedReader 装饰了 Reader,给它加了缓冲功能;InputStreamReader 装饰了 InputStream,把字节流变成字符流。你写 new BufferedReader(new InputStreamReader(new FileInputStream("a.txt"))) 时,那一长串嵌套就是装饰器的层层包装。
行为型模式
行为型模式关注的是对象之间的职责分配和算法交互——谁做什么、怎么通信、什么时候做。这一类模式回答的是“怎么协作”的问题。
策略模式
策略模式(Strategy Pattern)定义了一系列算法,将每个算法封装成独立的策略类,并使它们可以互相替换。客户端在运行时可以根据条件选择不同的策略,而不需要修改使用策略的代码。它的核心价值是消除代码里那一堆 if-else / switch 分支。
典型的应用场景是支付方式选择:用户下单后可以选微信支付、支付宝支付、银行卡支付,每种支付方式的实现完全不同,但对系统来说它们都是“支付”这个动作。
// 1. 策略接口:定义统一的支付行为
public interface PaymentStrategy {
boolean pay(String orderId, double amount);
}
// 2. 具体策略:微信支付
public class WeChatPayStrategy implements PaymentStrategy {
@Override
public boolean pay(String orderId, double amount) {
System.out.println("调用微信支付 SDK,订单 " + orderId + " 扣款 " + amount + " 元");
return true;
}
}
// 3. 具体策略:支付宝支付
public class AliPayStrategy implements PaymentStrategy {
@Override
public boolean pay(String orderId, double amount) {
System.out.println("调用支付宝 SDK,订单 " + orderId + " 扣款 " + amount + " 元");
return true;
}
}
// 4. 具体策略:银行卡支付
public class BankCardPayStrategy implements PaymentStrategy {
@Override
public boolean pay(String orderId, double amount) {
System.out.println("调用银联渠道,订单 " + orderId + " 扣款 " + amount + " 元");
return true;
}
}
// 5. 上下文:持有策略引用,把支付请求委托给具体策略
public class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public boolean checkout(String orderId, double amount) {
if (strategy == null) {
throw new IllegalStateException("未选择支付方式");
}
return strategy.pay(orderId, amount);
}
}
// 6. 客户端
public class StrategyDemo {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// 用户选了微信支付
context.setStrategy(new WeChatPayStrategy());
context.checkout("ORD-2026-001", 99.9);
// 换成支付宝,上下文代码完全不变
context.setStrategy(new AliPayStrategy());
context.checkout("ORD-2026-002", 199.0);
// 换成银行卡
context.setStrategy(new BankCardPayStrategy());
context.checkout("ORD-2026-003", 5000.0);
}
}
运行结果:
调用微信支付 SDK,订单 ORD-2026-001 扣款 99.9 元
调用支付宝 SDK,订单 ORD-2026-002 扣款 199.0 元
调用银联渠道,订单 ORD-2026-003 扣款 5000.0 元
与工厂模式结合使用
上面用 setStrategy 手动传入策略对象,如果不想让客户端感知具体策略类,可以把策略的选择交给工厂。常见做法是用一个简单的策略工厂(或注册 Map)来根据 key 获取策略实例:
import java.util.HashMap;
import java.util.Map;
public class PaymentStrategyFactory {
private static final Map<String, PaymentStrategy> STRATEGY_MAP = new HashMap<>();
static {
STRATEGY_MAP.put("WECHAT", new WeChatPayStrategy());
STRATEGY_MAP.put("ALIPAY", new AliPayStrategy());
STRATEGY_MAP.put("BANKCARD", new BankCardPayStrategy());
}
public static PaymentStrategy getStrategy(String payType) {
PaymentStrategy strategy = STRATEGY_MAP.get(payType);
if (strategy == null) {
throw new IllegalArgumentException("不支持的支付方式:" + payType);
}
return strategy;
}
}
这样调用方只需要 PaymentStrategyFactory.getStrategy(payType).pay(orderId, amount),新增一种支付方式时,只需新增一个策略类并在工厂里注册,完全符合开闭原则。策略模式负责“做什么”,工厂模式负责“谁来干”,两者搭配是消除分支语句的经典组合拳,在 SDK 设计、规则引擎、营销活动配置等场景里非常常见。
实际项目里你可能会用 Spring 容器来进一步简化:给每个策略类打上 @Component("WECHAT") 之类的注解,然后通过 @Autowired Map<String, PaymentStrategy> 让 Spring 自动把所有策略 Bean 注入到 Map 里,连手动注册的步骤都省了。这正是 Spring 生态下策略模式的优雅实现方式,也是日常开发中最推荐的做法。
责任链模式
责任链模式(Chain of Responsibility Pattern)让多个对象都有机会处理同一个请求,把这些对象串成一条链,请求沿着链传递,直到某个对象处理它为止。发送者不需要知道链上的哪个对象最终处理了请求,链上的每个处理者也只需知道“自己处理不了就交给下一个”。
这个模式天然契合审批流程、过滤器链、拦截器栈这类场景。比如请假审批:请假 1 天以内组长能批,3 天以内部门经理能批,7 天以内总监能批,再往上就得 CEO 出马——这就是一条责任链。
// 1. 抽象处理者:定义处理请求的接口,并维护下一个处理者的引用
public abstract class Approver {
protected Approver nextApprover; // 下一个处理者
protected String name;
public Approver(String name) {
this.name = name;
}
// 组装链:返回下一个处理者,方便链式调用
public Approver setNext(Approver next) {
this.nextApprover = next;
return next;
}
// 子类实现具体审批逻辑
public abstract void approve(LeaveRequest request);
}
// 2. 请假请求
public class LeaveRequest {
private String employee;
private int days;
private String reason;
public LeaveRequest(String employee, int days, String reason) {
this.employee = employee;
this.days = days;
this.reason = reason;
}
public String getEmployee() { return employee; }
public int getDays() { return days; }
public String getReason() { return reason; }
}
// 3. 具体处理者:组长(处理 1 天以内)
public class TeamLeader extends Approver {
public TeamLeader(String name) {
super(name);
}
@Override
public void approve(LeaveRequest request) {
if (request.getDays() <= 1) {
System.out.println("组长 " + name + " 批准了 " + request.getEmployee()
+ " 的 " + request.getDays() + " 天请假(" + request.getReason() + ")");
} else if (nextApprover != null) {
System.out.println("组长 " + name + " 权限不足,转交上级...");
nextApprover.approve(request);
}
}
}
// 4. 具体处理者:部门经理(处理 3 天以内)
public class DepartmentManager extends Approver {
public DepartmentManager(String name) {
super(name);
}
@Override
public void approve(LeaveRequest request) {
if (request.getDays() <= 3) {
System.out.println("部门经理 " + name + " 批准了 " + request.getEmployee()
+ " 的 " + request.getDays() + " 天请假(" + request.getReason() + ")");
} else if (nextApprover != null) {
System.out.println("部门经理 " + name + " 权限不足,转交上级...");
nextApprover.approve(request);
}
}
}
// 5. 具体处理者:总监(处理 7 天以内)
public class Director extends Approver {
public Director(String name) {
super(name);
}
@Override
public void approve(LeaveRequest request) {
if (request.getDays() <= 7) {
System.out.println("总监 " + name + " 批准了 " + request.getEmployee()
+ " 的 " + request.getDays() + " 天请假(" + request.getReason() + ")");
} else if (nextApprover != null) {
System.out.println("总监 " + name + " 权限不足,转交上级...");
nextApprover.approve(request);
}
}
}
// 6. 具体处理者:CEO(终极兜底,全权处理)
public class CEO extends Approver {
public CEO(String name) {
super(name);
}
@Override
public void approve(LeaveRequest request) {
System.out.println("CEO " + name + " 批准了 " + request.getEmployee()
+ " 的 " + request.getDays() + " 天请假(" + request.getReason() + ")");
}
}
// 7. 客户端:组装责任链并提交请求
public class ChainOfResponsibilityDemo {
public static void main(String[] args) {
// 组装审批链:组长 -> 部门经理 -> 总监 -> CEO
Approver leader = new TeamLeader("张三");
leader.setNext(new DepartmentManager("李四"))
.setNext(new Director("王五"))
.setNext(new CEO("赵六"));
System.out.println("=== 员工 A 请假 1 天 ===");
leader.approve(new LeaveRequest("员工A", 1, "家里有事"));
System.out.println("\n=== 员工 B 请假 4 天 ===");
leader.approve(new LeaveRequest("员工B", 4, "回老家"));
System.out.println("\n=== 员工 C 请假 15 天 ===");
leader.approve(new LeaveRequest("员工C", 15, "出国旅游"));
}
}
运行结果:
=== 员工 A 请假 1 天 ===
组长 张三 批准了 员工A 的 1 天请假(家里有事)
=== 员工 B 请假 4 天 ===
组长 张三 权限不足,转交上级...
部门经理 李四 权限不足,转交上级...
总监 王五 批准了 员工B 的 4 天请假(回老家)
=== 员工 C 请假 15 天 ===
组长 张三 权限不足,转交上级...
部门经理 李四 权限不足,转交上级...
总监 王五 权限不足,转交上级...
CEO 赵六 批准了 员工C 的 15 天请假(出国旅游)
责任链模式在框架里随处可见:Servlet 的 FilterChain、Spring MVC 的 HandlerInterceptor、Spring Security 的 FilterChainProxy、Netty 的 ChannelPipeline,本质都是把一个个处理器串成链,请求依次流过。理解了这个模式,再去看这些框架的源码会顺畅很多。
观察者模式
观察者模式(Observer Pattern)定义了对象之间的一对多依赖关系:当一个对象(被观察者 / Subject)的状态发生变化时,所有依赖它的对象(观察者 / Observer)都会自动收到通知并更新。这是事件驱动编程的基础。
它的应用场景非常广:GUI 里按钮点击的事件监听、消息队列的广播、Vue/React 的数据响应式、甚至微博“关注了某人后他发博你就能刷到”——本质都是观察者模式。
下面以一个天气数据广播为例:气象站(Subject)采集到温度、湿度、气压数据后,要通知多个展示终端(Observer)——比如当前状况板、天气统计板、简单预报板。
import java.util.ArrayList;
import java.util.List;
// 1. 观察者接口:定义收到通知后的更新行为
public interface Observer {
void update(float temperature, float humidity, float pressure);
}
// 2. 被观察者接口:定义注册、移除、通知的方法
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// 3. 具体被观察者:气象站,持有观察者列表和数据
public class WeatherStation implements Subject {
private final List
<Observer> observers = new ArrayList<>();
private float temperature;
private float humidity;
private float pressure;
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer o : observers) {
o.update(temperature, humidity, pressure);
}
}
// 当数据更新时,调用通知方法
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObservers();
}
}
// 4. 具体观察者 A:当前状况展示板
public class CurrentConditionsDisplay implements Observer {
@Override
public void update(float temperature, float humidity, float pressure) {
System.out.println("【当前状况板】温度:" + temperature + "℃,湿度:" + humidity + "%");
}
}
// 5. 具体观察者 B:天气统计板
public class StatisticsDisplay implements Observer {
@Override
public void update(float temperature, float humidity, float pressure) {
System.out.println("【统计板】气压:" + pressure + " hPa,温度:" + temperature + "℃");
}
}
// 6. 具体观察者 C:简单预报板
public class ForecastDisplay implements Observer {
@Override
public void update(float temperature, float humidity, float pressure) {
if (pressure < 1000) {
System.out.println("【预报板】气压偏低,预计有雨");
} else {
System.out.println("【预报板】气压正常,天气晴好");
}
}
}
// 7. 客户端
public class ObserverDemo {
public static void main(String[] args) {
WeatherStation station = new WeatherStation();
// 三个展示板订阅气象站
Observer currentDisplay = new CurrentConditionsDisplay();
Observer statsDisplay = new StatisticsDisplay();
Observer forecastDisplay = new ForecastDisplay();
station.registerObserver(currentDisplay);
station.registerObserver(statsDisplay);
station.registerObserver(forecastDisplay);
System.out.println("=== 第一次数据更新 ===");
station.setMeasurements(28.5f, 65.0f, 1013.0f);
System.out.println("\n=== 第二次数据更新(气压降低)===");
station.setMeasurements(26.0f, 80.0f, 998.0f);
}
}
运行结果:
=== 第一次数据更新 ===
【当前状况板】温度:28.5℃,湿度:65.0%
【统计板】气压:1013.0 hPa,温度:28.5℃
【预报板】气压正常,天气晴好
=== 第二次数据更新(气压降低)===
【当前状况板】温度:26.0℃,湿度:80.0%
【统计板】气压:998.0 hPa,温度:26.0℃
【预报板】气压偏低,预计有雨
观察者模式有两种数据传递方式:
- 推模式(Push):Subject 主动把数据塞给 Observer,就像上面
update(temp, humidity, pressure)一次性传所有字段。简单,但 Observer 可能收到它用不上的数据。 - 拉模式(Pull):Subject 只通知“数据变了”,Observer 自己按需调用 Subject 的 getter 拉取感兴趣的字段。更灵活,但有方法调用开销。
JDK 内置了对观察者模式的支持:java.util.Observable(已废弃,Java 9 起)和 java.util.EventListener 体系。现代 Java 开发更推荐用 Guava 的 EventBus,或者直接基于 Spring 的 ApplicationEventPublisher + @EventListener 实现事件总线——它们都是观察者模式的生产级落地。
小提示:纯观察者模式的 Subject 和 Observer 是直接引用的松耦合,但如果注意不到线程安全和观察者执行顺序问题,在并发场景下容易踩坑。生产环境一般会引入消息中间件(如 RocketMQ)来做“物理隔离版”的观察者模式——发布者发消息,订阅者消费消息,两者完全解耦,还能跨进程、跨机器。
总结归纳
创建型、结构型、行为型三大类模式,分别回答了软件设计的三个核心问题。
- 创建型模式关注“对象怎么创建”——单例、工厂、建造者、原型,核心是隐藏创建细节、解耦客户端与具体类型,让对象的构造和使用分离。
- 结构型模式关注“对象怎么组装”——代理、适配器、装饰、外观、桥接、组合、享元,核心是把类或对象组合成更大的结构,处理接口适配和职责叠加。
- 行为型模式关注“对象怎么协作”——策略、责任链、观察者、模板方法、状态、命令、迭代器、中介者、备忘录、访问者,核心是分配职责、管理交互、控制算法的调用时机和方式。
实际开发中最常用的设计模式 Top 5(个人向,结合日常工作频次):
- 策略模式:替代长串 if-else 的首选,配合 Spring 容器做策略注入,几乎每个业务系统都会用到。
- 工厂模式:隐藏对象创建细节,Spring 的 BeanFactory 本身就是最大的工厂。
- 单例模式:Spring 的 Bean 默认就是单例,配置类、工具类场景极常见。
- 责任链模式:过滤器、拦截器、审批流、风控规则引擎,框架里到处都是。
- 观察者模式:事件发布订阅、消息广播,是事件驱动架构的基础。
最后几句清醒话:
设计模式不是 silver bullet。它本质是前人沉淀下来的“套路”,是用来解决特定重复问题的经验复用,而不是炫技的资本。滥用模式比不会模式更可怕——一些本来几十行就能写清楚的简单逻辑,硬套上三层抽象、五个模式,反而让代码晦涩难懂、难以维护。评价一段代码的好坏,永远是“是否清晰、是否易改、是否能解决问题”,而不是“用了几个模式”。
初学时建议“先识别、再套用”:多读优秀的开源框架源码,看看大牛是怎么用的,理解每个模式解决的是什么痛点。Spring 框架本身就是一座设计模式宝库——
BeanFactory是工厂,ApplicationContext的 BeanPostProcessor 链是责任链,AOP 底层是动态代理,ApplicationEvent是观察者,TransactionTemplate 是模板方法,JdbcTemplate 也是模板方法,Resource体系是策略,RouterFunction 是责任链。把这些套路在真实代码里看明白,比自己对着书干啃十遍都管用。记住 GOF 那句话:“针对接口编程,而不是针对实现编程;多用组合,少用继承。” 掌握了这两条,你对设计模式就算入了门。剩下的,就是在实战中慢慢打磨品味了。
