0%

深入设计模式

设计模式是软件工程中经过反复实践分类编目经验总结通用解决方案。它们不是现成的代码库或框架,而是一种在特定情境下解决特定问题思维框架和指导方针。简单来说,设计模式就是“前辈们”在解决软件设计问题时发现的最佳实践,能够帮助我们写出更优雅、更灵活、更易于维护和扩展的代码。设计模式主要分为三类:创建型、结构型、行为型

1. 创建型模式

这类模式主要关注对象的创建过程,使得系统在创建对象时更具弹性。它们将对象的创建与使用分离,避免了直接创建对象可能带来的复杂性或紧耦合。

单例模式

确保一个类只有一个实例,并提供一个全局访问点。应用场景:线程池、缓存、日志对象、配置对象等。例子: 数据库连接池、Spring IoC 容器中的 Bean 默认就是单例。以下说说单例的实现方式:

饿汉式

饿汉式是在类加载的时候就立即创建单例实例。由于实例在类加载时就已创建,所以是天然线程安全的。但是,无论是否需要,实例都会在类加载时创建。如果单例对象创建开销较大且不一定会被使用,这会造成资源浪费。

1
2
3
4
5
6
7
8
9
10
11
public class SingletonEager {
private static SingletonEager instance = new SingletonEager(); // 类加载时立即创建实例,static 关键字使得 instance 变量成为类变量,而不是实例变量。这个 instance 变量都只存在一份,并且被所有该类的对象(如果存在)共享。

private SingletonEager() {
// 私有构造函数,防止外部实例化
}

public static SingletonEager getInstance() { //类方法可以直接通过类名调用,而不需要先创建类的实例。
return instance;
}
}

懒汉式 (Lazy Initialization)

懒汉式在第一次调用 getInstance() 方法时才创建实例。只有在真正需要时才创建实例,节省资源。

  • 非线程安全版本:在多线程环境下,有两个或更多线程几乎同时首次调用 getInstance() 方法,如果多个线程同时进入到 if (instance == null) 代码块,那就会创建多个实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SingletonLazy {
private static SingletonLazy instance; // 延迟到首次使用时创建

private SingletonLazy() {
// 私有构造函数
}

public static SingletonLazy getInstance() {
if (instance == null) { // 第一次访问时才创建
instance = new SingletonLazy();
}
return instance;
}
}
  • 线程安全版本(双重锁校验):以上是由于竞态条件:多个线程在没有适当同步的情况下,对共享资源(这里的共享资源就是 instance 变量)进行读写操作,导致最终结果取决于线程执行的时序,从而产生不可预测或错误的结果引入同步机制来保证在任何时刻,需要确保instance = new SingletonLazy() 这行代码只会被一个线程执行,才能确保实例的唯一性。instance = new SingletonLazy()这行代码是由三个非原子步骤构成:(1)分配内存(为 SingletonDCL 实例分配内存空间)、(2)初始化对象(调用 SingletonDCL 的构造函数,初始化对象的字段)、(3)设置引用(将 instance 引用指向刚分配的内存地址),JVM/CPU 为了优化性能,可能会对指令进行重排序,将其执行顺序变为 (1) -> (3) -> (2),线程 A 先执行了步骤 (1) (分配内存) 和 (3) (设置引用)。此时,instance 变量已经不再是 null,它指向了一块内存区域。但是,这个内存区域中的对象内部字段可能还没有被完全初始化(因为步骤 (2) 还没执行);线程 A 还没有来得及执行步骤 (2) (初始化对象),就暂时失去了 CPU 时间片。当线程 B 尝试使用这个 instance 对象时,就会因为访问到尚未初始化的字段而导致空指针异常 (NullPointerException) 或其他运行时错误,所以必须使用 volatile 关键字来防止指令重排序问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SingletonDCL {
// 使用 volatile 关键字,确保多线程环境下 instance 变量的可见性
// 并防止指令重排序导致的问题(例如,在对象完全初始化前就返回了引用)
private static volatile SingletonDCL instance;

private SingletonDCL() {
// 私有构造函数
}

public static SingletonDCL getInstance() {
if (instance == null) { // 第一次检查:避免不必要的同步
synchronized (SingletonDCL.class) { // 加锁,确保只有一个线程创建实例
if (instance == null) { // 第二次检查:防止在同步块内被其他线程创建
instance = new SingletonDCL();
}
}
}
return instance;
}
}

静态内部类

静态内部类是实现单例模式结合了懒加载和线程安全的优点,JVM 保证类的初始化是线程安全的,只有在 getInstance() 方法被首次调用时,SingletonHolder 类才会被加载,从而创建单例实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SingletonStaticInnerClass {
private SingletonStaticInnerClass() {
// 私有构造函数
}

// 静态内部类,只有在首次调用 getInstance() 时才会被加载
private static class SingletonHolder {
private static final SingletonStaticInnerClass INSTANCE = new SingletonStaticInnerClass();
}

public static SingletonStaticInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}

枚举

实例在枚举类加载时就被创建,与饿汉式类似,无法实现懒加载

1
2
3
public enum SingletonEnum {
INSTANCE; // 唯一的实例
}

工厂方法模式 (Factory Method Pattern)

定义一个创建对象的接口,但让子类决定实例化哪一个类。工厂方法将类的实例化延迟到子类。应用场景: 当一个类不知道它所需要的对象的类名时;当一个类希望其子类来指定它所创建的对象时, 例子: 不同类型的汽车工厂生产不同品牌的汽车。

抽象工厂模式 (Abstract Factory Pattern)

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。应用场景:当需要创建一组相关或相互依赖的对象家族时(如 UI 库中的按钮、文本框、菜单等都属于某个风格)。例子:生产不同操作系统(Windows/Linux/Mac)下的一系列 UI 组件。

建造者模式 (Builder Pattern)

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。应用场景:创建一个复杂对象,其构造过程包含多个步骤且顺序通常是固定的,但可以有不同的组合。例子:StringBuilder、Java 8 Stream API 的构建、生成复杂报表。
当你要创建一个对象,而这个对象的构造过程非常复杂,包含了很多可选的参数或构建步骤时,直接使用构造函数或者 Setter 方法可能会导致以下问题:构造函数参数过多 (Telescoping Constructor Anti-Pattern): 如果对象有多个可选参数,你需要定义多个构造函数来适应不同的参数组合,这会导致构造函数爆炸式增长,难以维护。

原型模式 (Prototype Pattern)

用原型实例指定创建对象的种类,并通过复制这些原型创建新对象。应用场景:当对象创建成本较高,或者需要动态地创建对象时。例子:缓存大量相似对象、通过 clone() 方法创建新对象。


2. 结构型模式 (Structural Patterns)

这类模式关注如何将类和对象组合成更大的结构,以形成新的功能,同时保持结构的灵活性和效率。

适配器模式 (Adapter Pattern)

将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。应用场景:遗留系统与新系统集成、统一不同来源的数据格式。例子:将 220V 电源适配为 110V、电脑的 USB 转网线接口。

装饰器模式 (Decorator Pattern)

动态地给一个对象添加一些额外的职责。相比较于继承,装饰器更灵活。应用场景:** 在不改变原有类结构的情况下,为对象添加新功能。例子:Java IO 流(BufferedReader, DataInputStream 等)、Spring Security 中为方法添加权限校验。

代理模式 (Proxy Pattern)

为其他对象提供一种代理以控制对这个对象的访问。应用场景:远程代理、虚拟代理(延迟加载)、安全代理(权限控制)、智能引用。例子:Spring AOP 的底层实现、RPC 框架中的客户端代理、MyBatis 的 Mapper 接口代理。

组合模式 (Composite Pattern)

将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。应用场景:目录和文件结构、菜单系统、组织架构图。例子:GUI 中的容器和组件。

外观模式 (Facade Pattern)

为子系统中的一组接口提供一个统一的接口。外观模式定义了一个高层接口,这个接口使得子系统更容易使用。应用场景:简化复杂系统的使用、将客户端与子系统解耦。例子:电脑启动按钮(它隐藏了启动 CPU、内存、硬盘等复杂过程)、Spring 对 JDBC 的封装。

享元模式 (Flyweight Pattern)

运用共享技术有效地支持大量细粒度的对象。应用场景:当系统中存在大量相同或相似的对象,且这些对象的存储开销很大时。例子:Java 的 String 字符串常量池、数据库连接池。

桥接模式 (Bridge Pattern)

将抽象与实现分离,使它们可以独立变化。应用场景:当一个类有多个维度的变化,且这些维度需要独立扩展时。例子:抽象画(画抽象)与具体画法(用画笔画)分离、不同操作系统下的图形渲染。


3. 行为型模式 (Behavioral Patterns)

这类模式关注对象之间的职责分配和交互方式,以增强系统的灵活性和可维护性。

策略模式 (Strategy Pattern)

定义一系列的算法,并将每一个算法封装起来,使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化。应用场景:依据不同条件执行不同算法、运行时选择算法。例子:支付方式(支付宝、微信支付、银联)、排序算法的选择。

模板方法模式 (Template Method Pattern)

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义该算法的某些特定步骤。应用场景:多个子类有共同的操作步骤,但个别步骤有差异。例子:制作咖啡和茶的流程、Servlet 的生命周期方法。

观察者模式 (Observer Pattern)

定义对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。应用场景:事件处理机制、消息通知、GUI 事件。例子:Java Swing/AWT 的事件监听、Spring 事件发布与监听、Kafka/RabbitMQ 消息订阅。

迭代器模式 (Iterator Pattern)

提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。应用场景:遍历集合对象。例子:Java Collection Framework 中的 Iterator 接口。

责任链模式 (Chain of Responsibility Pattern)

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。应用场景:流程审批、过滤器链、日志记录级别。例子:Servlet Filter 链、Spring Security 的过滤器链。

命令模式 (Command Pattern)

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。应用场景:菜单操作、宏命令、日志回滚、事务。例子:队列任务、Swing 中的 Action

忘录模式 (Memento Pattern)

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。应用场景:撤销操作、游戏存档。例子:编辑器中的“撤销”功能。

状态模式 (State Pattern)

当一个对象的内在状态改变时允许其改变行为,这个对象看起来像是改变了它的类。应用场景:对象的行为依赖于其状态,且状态之间有明确的转换。例子:订单状态(待支付、已支付、已发货等)、ATM 机不同状态下的操作。

访问者模式 (Visitor Pattern)

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。应用场景:对一组不同类型的对象执行不同的操作,且操作逻辑需要经常变化。例子:编译器中的语法树遍历、对不同文件类型进行不同处理。

中介者模式 (Mediator Pattern)

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

解释器模式 (Interpreter Pattern)

给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。