Fork me on GitHub

架构师 - 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 判断类型)每新增产品都要修改工厂类,违反开闭原则;工厂方法用"增加实现类"替代"修改既有代码"。
  • 客户端只依赖 FactoryProduct 两个抽象,新增支付方式时调用方代码零改动。
  • 产品种类多时会产生"一个产品配一个工厂"的类膨胀,此时应评估改用抽象工厂或反射注册。

📖 创建型 - 工厂方法模式

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

 维度一(形状)                  维度二(颜色)
   圆形 ─┐                          ┌─ 红色
   方形 ─┼───── 自由组合 ───────────┼─ 蓝色
   三角形 ─┘                        └─ 绿色

以"形状 × 颜色"为例:用继承需要 RedCircleBlueSquare… 共 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 不暴露内部结构地遍历集合 Iteratorfor-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 FilterHandlerInterceptor、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 的空实现)让子类决定是否执行某一步,避免强制重写。
  • 既有实例:JdbcTemplateHttpServlet.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(装饰器)。


posted @ 2026-09-21 21:26  秋夜雨巷  阅读(9)  评论(0)    收藏  举报