设计模式(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 适配成了字符流 Readerjava.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(个人向,结合日常工作频次):

  1. 策略模式:替代长串 if-else 的首选,配合 Spring 容器做策略注入,几乎每个业务系统都会用到。
  2. 工厂模式:隐藏对象创建细节,Spring 的 BeanFactory 本身就是最大的工厂。
  3. 单例模式:Spring 的 Bean 默认就是单例,配置类、工具类场景极常见。
  4. 责任链模式:过滤器、拦截器、审批流、风控规则引擎,框架里到处都是。
  5. 观察者模式:事件发布订阅、消息广播,是事件驱动架构的基础。

最后几句清醒话:

设计模式不是 silver bullet。它本质是前人沉淀下来的“套路”,是用来解决特定重复问题的经验复用,而不是炫技的资本。滥用模式比不会模式更可怕——一些本来几十行就能写清楚的简单逻辑,硬套上三层抽象、五个模式,反而让代码晦涩难懂、难以维护。评价一段代码的好坏,永远是“是否清晰、是否易改、是否能解决问题”,而不是“用了几个模式”。

初学时建议“先识别、再套用”:多读优秀的开源框架源码,看看大牛是怎么用的,理解每个模式解决的是什么痛点。Spring 框架本身就是一座设计模式宝库——BeanFactory 是工厂,ApplicationContext 的 BeanPostProcessor 链是责任链,AOP 底层是动态代理,ApplicationEvent 是观察者,TransactionTemplate 是模板方法,JdbcTemplate 也是模板方法,Resource 体系是策略,RouterFunction 是责任链。把这些套路在真实代码里看明白,比自己对着书干啃十遍都管用。

记住 GOF 那句话:“针对接口编程,而不是针对实现编程;多用组合,少用继承。” 掌握了这两条,你对设计模式就算入了门。剩下的,就是在实战中慢慢打磨品味了。

By admin

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注