# (软件)设计原则有哪些

常⽤的⾯向对象设计原则包括7 个,这些原则并不是孤⽴存在的,它们相互依赖,相互补充。

设计原则英文简单定义
开闭原则Open Closed Principle(OCP)对扩展开放,对修改关闭
单⼀职责原则Single Responsibility Principle(SRP)⼀个类只负责⼀个功能领域中的相应职责
里氏替换原则Liskov Substitution Principle(LSP)所有引用基类的地方,必须能透明地使用其子类的对象
依赖倒置原则Dependency Inversion Principle(DIP)面向抽象 / 接口编程,而非面向具体实现类,让上层不再依赖下层
接口隔离原则Interface Segregation Principle(ISP)类之间的依赖关系应该建立在最小的接口上
合成 / 聚合复用原则Composite/Aggregate Reuse Principle(C/ARP)尽量使用合成 / 聚合,而不是通过继承达到复用的目的
最少知识原则 / 迪米特法则Least Knowledge Principle(LKP)/Law of Demeter(LOD)一个软件实体应当尽可能少的与其他实体发生相互作用

# 设计模式简介

设计模式 (Design pattern) 代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结,是经过广泛接受、验证有效的解决问题的方法。使用设计模式是为了重用代码、让代码更易于理解、保证代码可靠性。毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

# 设计模式的分类

image-20220520174508815

image-20240308140318516

设计模式主要分为 3 大类,24 小种。

  • 创建型(Creational):在创建对象的同时隐藏对象的创建逻辑,不直接使用 new 来实例化对象,程序在判断需要创建哪些对象时更灵活。

    • 单例模式(Singleton):确保一个类只有一个实例,提供全局访问点。例如配置管理、日志管理等。
    • 简单工厂模式由工厂对象来创建实例,根据传入的参数决定创建哪种产品类的实例。
    • 工厂方法模式(Factory Method):定义一个创建对象的抽象工厂,内部声明了产品的生产接口,将生产任务交给不同的派生类工厂,让它们决定实例化哪个类。
    • 抽象工厂模式(Abstract Factory):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
    • 建造者模式(Builder):将一个复杂对象的构建过程与其表示分离,封装对象的构建过程,使同样的构建过程可以创建不同的表示。
    • 原型模式(Prototype):通过复制现有实例来创建新实例。
  • 结构型(Structural):**通过类和接口间的继承和引用** 实现创建复杂结构的对象。

    • 适配器模式(Adapter):将一个类的接口转换成客户希望的另一个接口,适用于接口不兼容的场景。
    • 桥接模式(Bridge):将抽象和对应实现进行解耦,使得二者可以独立地变化
    • 组合模式(Composite):又叫做部分 - 整体模式,使得客户端看来单个对象和对象的组合是同等的。换句话说,某个类型的方法同时也接受自身类型作为参数
    • 装饰器模式(Decorator):动态地给一个对象添加额外的属性 / 功能,而不希望影响其他对象时。
    • 外观模式(Facade):为子系统中的一组接口提供一个统一的高层接口。适用于简化复杂系统的场景。
    • 享元模式(Flyweight):通过共享对象来减少内存占用,适用于大量相似对象的场景。可以共享一部分相同状态的对象以减少内存占用,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建
    • 代理模式(Proxy):为其他对象提供一种代理,以控制对其的访问。适用于权限控制、延迟加载等场景。
  • 行为型(Behavioral):通过类之间的不同通信方式实现不同行为

    • 模板方法方法(Template):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中实现。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。
    • 策略模式(Strategy):定义一系列算法,将每个算法封装起来,并使它们可以互换。例如当一个类有多个行为,而且这些行为在运行时可以相互替代时,可以使用策略模式。
    • 状态模式(State):允许对象在其内部状态发生变化时,改变它的行为。当一个对象的行为取决于它的状态,并且它在运行时可以切换状态时,可以使用状态模式。
    • 观察者模式(Observer):定义对象间一对多的依赖关系,使得每当一个对象状态发生改变时,所有依赖于它的对象都得到通知并自动更新
    • 命令模式(Command):将一个请求 / 操作封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。例如 GUI、操作系统命令调用、需要对行为进行记录、撤销或重做、事务等处理。
    • 责任链模式(Chain of Responsibility):为请求创建一条由多个接收者对象组成的链,并将请求沿着这条链传递,直到有对象处理它为止。当系统中有多个对象可以处理同一请求,但具体哪个对象处理由运行时决定。使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。例如拦截器、过滤器、数据清洗、规则引擎等。
    • 解释器模式(Interpreter):给定一门语言,定义它的一种语法,并定义一个解释器,该解释器使用该语法来解释语言中的句子。例如正则匹配、规则引擎、将字符串解析成对象等。
    • 迭代器模式(Iterator):提供一个一致的方法来顺序访问集合中的各个元素,而该方法与集合的底层具体实现无关。例如异构集合统一遍历方式。
    • 中介者模式(Mediator):用一个中介对象来封装一系列的对象交互,使得其他对象之间不需要显示地相互作用,而且可以独立地改变它们之间的交互。当系统中的多个对象之间存在复杂的相互依赖关系,而且这些关系难以维护时,可以使用中介者模式,例如聊天室、GUI 组件之间的通信等。
    • 备忘录模式(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态快照,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。例如撤销、恢复、历史记录等。
    • 访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。适用于:需要对一个对象结构中的各元素进行不同的操作,但是不希望在该对象的类中添加这些操作时。

# 单例模式

# 定义与特点

单例模式属于创建型模式,单例类在任何情况下都只存在一个实例,特点如下:

  • 私有的构造方法
  • 私有的静态变量:存储实例
  • 公有的静态方法:获取实例

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

单例模式的好处:

  • 全局访问:单例对象可以在应用程序的任何地方被访问,而不需要传递对象的引用。这样可以方便地共享对象的状态和功能,简化了对象之间的通信和协作。

  • 节省资源:对于频繁使用的对象,可以节省重复创建对象所花费的开销,这对于那些重量级对象而言,是非常可观的一笔系统开销;

  • 减轻 GC 压力:由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

# 常见写法

# 饿汉式(线程安全)

类在加载时就直接创建单例实例

优点:线程安全,不用加锁,执行效率高。

基于类加载机制避免了多线程的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。

缺点:易产生垃圾对象,浪费内存空间

代码实现:

/**
 * 饿汉式单例测试
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造⽅法
    private Singleton(){}
    // 2、定义⼀个私有的静态变量,指向自己类型
    private final static Singleton instance = new Singleton();
    // 3、对外提供⼀个公有的静态方法,获取实例
    public static Singleton getInstance() {
        return instance;
    }
}

这种情况的单例模式容易被反射破坏

image-20240309161137415

public class Test {
    // 使用反射破坏单例
    public static void main(String[] args) throws Exception{
        // 获取空参构造方法(通过反射机制可以调用内部的 private 属性 / 方法 / 构造器)
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
        // 将空参构造方法设置为强制访问
        declaredConstructor.setAccessible(true);
        // 创建实例
        Singleton singleton = declaredConstructor.newInstance();
		
        System.out.println("反射创建的实例" + singleton);
		System.out.println("正常创建的实例" + Singleton.getInstance());
        System.out.println("正常创建的实例" + Singleton.getInstance());
    }
}

输入结果如下,可见创建了多个单例实例

反射创建的实例
com.example.spring.demo.single.Singleton@6267c3bb
正常创建的实例
com.example.spring.demo.single.Singleton@533ddba
正常创建的实例
com.example.spring.demo.single.Singleton@533ddba

# 懒汉式(线程不安全)

使用的时候再创建单例实例

优点:懒加载

缺点:线程不安全,在多线程环境是无法保证单例的

代码实现:

/**
 * 懒汉式单例,线程不安全
*
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造⽅法
    private Singleton(){ }
    // 2、定义⼀个私有的静态变量,指向自身类型
    private static Singleton instance;
    // 3、对外提供⼀个公共的⽅法,获取实例
    public static Singleton getInstance() {
        // 判断为 null 的时候再创建对象
        if (instance == null) {
            instance = new Singleton();
        }
        // 判断为 not null 的时候直接返回对象
        return instance;
    }
}

容易被多线程破坏,测试代码如下:

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("多线程创建的单例:" +  Singleton.getInstance());
            }).start();
        }
    }
}

输出结果如下,可见创建了多个单例实例

多线程创建的单例:
com.example.spring.demo.single.Singleton@18396bd5
多线程创建的单例:
com.example.spring.demo.single.Singleton@7f23db98
多线程创建的单例:
com.example.spring.demo.single.Singleton@5000d44

# 懒汉式(线程安全)

# 方式一:synchronized 加锁

通过 synchronized 关键字加锁来保证线程安全,既可以添加在方法上面,也可以添加在代码块上面。

优点:懒加载,线程安全

缺点:每一次调用 getInstance () 获取实例时都需要加锁和释放锁,效率较低

这里演示将 synchronized 关键字添加在方法上面,代码实现如下:

/**
 * 懒汉式单例,⽅法上⾯添加 synchronized 保证线程安全
*
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造⽅法
    private Singleton(){ }
    // 2、定义⼀个私有的静态变量,指向⾃⼰类型
    private static Singleton instance;
    // 3、对外提供⼀个公共的方法,获取实例(添加 synchronized 关键字加锁)
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

# 方式二:双重检查锁(DCL)

double-checked locking

双重检查指的是两次空判断,锁指的仍然是 synchronized 加锁

优点︰懒加载,线程安全,效率较高

缺点︰实现较复杂

代码实现如下:

/**
 * 双重检查锁(DCL, 即 double-checked locking)
*
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 1、私有化构造⽅法
    private Singleton() {}
    // 2、定义⼀个私有的静态变量,指向自己类型(volatile 表明该变量是共享且不稳定的,可以避免线程从自己工作内存的高速缓存中读写变量的值,要求每次读写都需要到主内存中进行,从而保证变量的线程可见性;此外,编译器和处理器会禁止对该 volatile 修饰的变量进行指令重排,从而保证程序在多线程环境下的正确性)
    private volatile static Singleton instance;
    // 3、对外提供⼀个公共的静态方法,获取实例
    public static Singleton getInstance() {
        // 第⼀重检查是否为 null
        if (instance == null) {
            // 使用 synchronized 加锁!!!!!!!
            synchronized (Singleton.class) {
                // 第⼆重检查是否为 null
                if (instance == null) {
                    //new 关键字创建对象不是原子操作
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

第一重空判断:

  • 如果实例已经存在,就直接返回这个实例,不再需要进行同步操作

  • 如果实例还没创建,才会进入同步块去创建实例

    同步块:目的是为了防止有多个线程同时调用导致生成多个实例,使得每次只能有一个线程访问同步块内容。当第一个线程抢到锁的调用获取了实例之后,这个实例就会被创建,之后其他线程的所有调用都不会进入同步块,直接在第一重判断就返回了单例。

第二重空判断:当多个线程一起到达锁位置时,进行锁竞争,其中一个线程获取锁,如果是第一次进入则为 null,则会创建单例对象,然后释放锁。其他线程获取锁后,就会被第二重空判断拦截,从而直接返回已创建的单例对象

其中最关键的一个点就是 volatile 关键字的使用,被它修饰的变量意味着是共享且不稳定的,会拥有两个特性:

  • 线程可见性:可以避免线程从自己本地内存的高速缓存中读写变量副本的值,而是强制到主内存中读写该变量本身的值,从而保证该变量的线程可见性。

    JMM(Java 内存模型)强制在主存中进行读取

  • 禁止指令重排:JVM 为了优化,会在不影响正确性的前提下,调整代码的执行顺序,但在多线程下指令重排会影响正确性!但是,编译器和处理器会禁止对 volatile 修饰的变量进行指令重排,从而保证程序在多线程环境下的正确性

因此就能理解这里为什么要使用 volatile 了,因为 ** new 关键字创建对象的过程不是原子操作**, instance = new Singleton(); 这行代码分为三步执行:

  1. 在堆内存中为 instance 分配内存空间
  2. 调用构造方法,初始化对象 instance
  3. instance 指向分配的内存地址

对应字节码指令如下(17、20、21、24):

image-20240309193545865

由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance () 后在第一重空判断发现 instance 不为空,因此直接返回 instance ,但此时 instance 还未被初始化,这就是著名的 DCL 失效问题

当我们在引用变量上面添加 volatile 关键字以后,会通过在创建对象指令的前后添加内存屏障来禁止指令重排序,就可以避免这个问题。而且对 volatile 修饰的变量的修改对其他任何线程都是可见的

# 方式三:静态内部类

优点:懒加载、线程安全、效率较高、实现简单

代码实现:

/**
 * 静态内部类实现单例
*
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 1、私有化构造⽅法
    private Singleton() {}
    
    // 2、对外提供获取实例的公共静态方法
    public static Singleton getInstance() {
        return InnerClass.INSTANCE; // 返回静态内部类的静态成员变量!
    }
    // 3、定义私有的静态内部类
    private static class InnerClass{
        // 私有的静态变量
        private final static Singleton INSTANCE = new Singleton(); 
    }
}

懒加载如何理解?

首先,了解一下类的生命周期。类是在运行期间第一次使用时才动态加载的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于方法区中。类从被加载到虚拟机内存中开始,到卸载出内存为止,类的生命周期可概括为 7 个阶段,如下图所示。

一个类的完整生命周期

其中,虚拟机加载 Class 文件的过程主要分为三步:加载、链接、初始化。《虚拟机规范》要求有且仅有以下 5 种 “类的主动引用” 情况,必须立即对类进行初始化

  1. 遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时,分别对应以下 Java 代码场景:
    • 使用 new 关键字实例化对象
    • 读取 / 设置一个类的静态字段final 修饰除外,被 final 修饰的静态字段是常量,已在编译期把结果放入常量池)
    • 调用一个类的静态方法
  2. 使用 java.lang.reflect 包方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现其父类还没有初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main () 的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果是 REF_getStatic 、REF_putStatic 、REF_invokeStatic 的方法句柄,则需要先触发这个方法句柄所对应的类的初始化。

除此之外的所有引用都不会立即对类进行初始化,称为类的被动引用

首先了解一下线程共享的 方法区

  • 常量池表JVM 为每个已加载的类维护一个常量池表,存储了类在编译期间生成的符号引用、字面量
    • 符号引用:类、字段、方法、接口等的符号引用
    • 字面量:基本数据类型、String 类型常量、声明为 final 的常量值等
  • 运行时常量池
    • 常量池中的数据会在对应类被加载后放入运行时常量池
    • 类在解析阶段将这些符号引用替换成直接引用
    • 除了在编译期生成的常量,还允许动态生成常量,例如 String 类的 intern ()

img

Java8 内存结构图

因此,Singleton 类不会在加载时就被初始化,那么当 Singleton 类的静态方法 getInstance () 被调用时,它才会被初始化。InnerClass 才会被放入 Singleton 的运行时常量池里,并把符号引用替换为直接引用,这时静态对象 INSTANCE 也真正被创建,然后再被 getInstance () 方法返回出去。


线程安全如何实现?

在《深入理解 JAVA 虚拟机》中,有这么一句话:“虚拟机会保证一个类的() 方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的() 方法,其他线程都需要阻塞等待,直到活动线程执行() 方法完毕。如果在一个类的() 方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行() 方法后,其他线程唤醒之后不会再次进入() 方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。”


从上面的分析可以看出 INSTANCE 在创建过程中是线程安全的,所以说静态内部类形式的单例既可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

# 枚举单例

在 Java 中,枚举类与普通类一样,都能拥有字段与方法,而且枚举类的实例创建是线程安全的,在任何情况下都是一个单例,因此使用枚举类( enum )可以非常方便地实现单例模式:

public enum Singleton { //enum 表示枚举类
    INSTANCE; // 这是枚举类的单个实例
    // 在这里可以添加其他成员变量和方法
    public void doSomething() {
        // 实现单例模式的操作
        System.out.println("Singleton instance is doing something.");
    }
}

在这个例子中, Singleton 是一个枚举类, INSTANCE 是该枚举类的单个实例。由于枚举类的特性,这个实例是在程序启动时被初始化(类似于饿汉式),并且在整个程序生命周期内只有一个

使用时,可以通过 Singleton.INSTANCE 来访问单例对象,并调用其中的方法:

public class Main {
    public static void main(String[] args) {
        // 访问单例实例
        Singleton singleton = Singleton.INSTANCE;
        
        // 调用单例方法
        singleton.doSomething();
    }
}

特点:类似于饿汉式,简单,高效,线程安全可以避免通过反射破坏枚举单例

  • 枚举类天生就是线程安全的,且只会被加载一次
  • 防止反射和反序列化时破坏单例的问题,因为枚举类型不会在反序列化时重新创建新的对象,会报错

枚举类的实例创建为什么是线程安全的?

利用 javap 命令反编译枚举类:

javap Singleton.class

得到如下内容:

Compiled from "Singleton.java"
    public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
        public static final com.spring.demo.singleton.Singleton INSTANCE; // INSTANCE 是 static final
        public static com.spring.demo.singleton.Singleton[] values();
        public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
        public void doSomething(java.lang.String);
        static {};
    }

从枚举类的反编译结果可以看到,INSTANCE 实例被 public static final 修饰(即公有静态常量),所以可以通过类名直接调用。并且对象的实例是在静态代码块中创建的,因为 static 类型的属性会在类被加载之后被初始化,当一个 Java 类第一次被真正使用到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程安全的,所以创建一个 enum 类型是线程安全的


枚举类的实例为什么都是单例?为什么不会被反射破坏枚举单例?

尝试通过反射破坏枚举单例的代码如下:

public class Test {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething("hello enum");
        // 尝试使⽤反射破坏单例
        // 枚举类没有空参构造方法,反编译后可以看到枚举有⼀个两个参数的构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 创建实例,这里会报错,因为无法通过反射来创建枚举的实例
        Singleton enumSingleton =declaredConstructor.newInstance();
        System.out.println (enumSingleton);
    }
}

运行结果报错如下:

Exception in thread "main" 
java.lang.IllegalArgumentException: Cannot reflectively create enum objects 
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492) 
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) 
	at com.spring.demo.singleton.Test.main(Test.java:24)

查看反射创建实例的 newInstance () ⽅法,有如下判断:

image-20240310123838943

所以⽆法通过反射创建枚举的实例。

# 优缺点

优点:

  1. 唯一实例: 单例模式确保一个类只有一个实例,减少了开销,避免了多次实例化,保证了全局唯一性。
  2. 延迟实例化: 单例模式可以实现延迟实例化,即在需要时才创建对象,提高了性能。
  3. 全局访问: 单例模式允许全局访问该唯一实例,方便对该实例进行管理和调用。

缺点:

  1. 可能引起性能问题: 如果单例对象在整个应用程序的生命周期内都存在,并频繁地被访问,可能会引起性能问题。
  2. 全局状态: 单例模式引入了全局状态,可能导致程序的复杂性增加,难以进行单元测试。
  3. ** 难以扩展: ** 没有抽象层,与单一职责原则冲突

# 应用场景

  • ** 资源共享:** 当某个资源只需要被系统中的一个对象共享时,使用单例模式可以确保对象的唯一性,例如数据库连接池、线程池等。
  • ** 配置管理:** 单例模式可以用于管理全局的配置信息,确保系统中使用的配置信息是唯一的。
  • ** 日志记录:** 在记录日志时,为了避免频繁地打开和关闭文件,可以使用单例模式来保持文件的打开状态。

# 工厂模式

工厂模式均属于创建型设计模式。

# 简单工厂模式

目的是将客户程序与具体类解耦,由⼀个工厂类来创建实例,根据传入工厂的不同参数来实例化不同产品类,不需要客户端关注创建逻辑。

image-20240309140922573

简单工厂模式的 UML 类图

优点:简单粗暴,适用于创建对象较少的情况。

缺点:如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则。而且产品多的话会使工厂类比较复杂

举个例子, Calendar 抽象类的 getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象;Spring 中的 BeanFactory 使用简单工厂模式,根据传入一个唯一的标识来获得 Bean 对象。

public class AnimalFactory {
    public static Animal createAnimal(String name) {
        if ("cat".equals(name)) {
            return new Cat();
        } else if ("dog".equals(name)) {
            return new Dog();
        } else if ("cow".equals(name)) {
            return new Dog();
        } else {
            return null;
        }
    }
}

# 工厂方法模式

对简单工厂模式进行抽象,该模式 **定义了一个抽象工厂类,其内部声明了抽象的生产方法。具体要实例化哪个类由派生工厂类决定,它们通过继承该抽象工厂类并重写实现其内部抽象生产方法来实例化不同的产品类**。

image-20240309153832142

工厂方法模式的 UML 类图

优点:不用通过指定产品类型来创建对象了,减轻了工厂类的负担,支持增加新产品,符合开放 - 封闭原则。

// 抽象的动物工厂
public abstract class AnimalFactory {
    public abstract Animal createAnimal(); // 抽象的生产方法(接口)
}
// 具体的工厂实现类
public class CatFactory extends AnimalFactory { // 继承抽象工厂类
    @Override
    public Animal createAnimal() { // 重写抽象工厂方法
        return new Cat();
    }
}
public class DogFactory extends AnimalFactory {
    @Override
    public Animal createAnimal() {
        return new Dog();
    }
}

# 抽象工厂模式

简单工厂模式和工厂方法模式不管怎么拆分 / 抽象工厂,都只是针对一类产品,如果要生成另一种产品,就比较难办了!因此,抽象工厂模式中抽象工厂类中定义了多类产品,而不是一类产品

抽象工厂模式通过 **在抽象工厂类中增加创建产品的抽象方法接口**,并在具体子工厂中实现新加产品的创建,当然前提是子工厂支持生产该产品。否则继承的这个接口可以什么也不干。

image-20240309153609323

# 优缺点

优点:

  1. 封装对象创建: 工厂模式将对象的创建逻辑封装在工厂类中,客户端无需知道对象的具体实现细节,只需通过工厂获取对象。
  2. 对象管理: 工厂模式集中了对象的管理,便于对对象的统一管理、维护和修改。
  3. 解耦合: 工厂模式将客户端代码与具体类的实现解耦,使系统更易于扩展和维护。

缺点:

  1. 类的数量增加: 引入工厂模式会增加类的数量,增加了系统的复杂性。
  2. 不适合简单对象创建: 对于简单的对象创建,使用工厂模式可能会显得过于繁琐,不划算。在这种情况下,直接实例化对象可能更为简便。

# 应用场景

  • ** 对象创建复杂:** 当对象的创建逻辑较为复杂,包含多个步骤或者依赖其他对象时,可以使用工厂模式将对象的创建过程封装起来。
  • ** 避免直接实例化:** 当需要隐藏对象的具体实现,只提供接口时,可以使用工厂模式。客户端通过工厂获取对象,而不直接实例化。

# 小结

  • 简单工厂:用一个工厂对象生产同一等级结构中的任意产品。
  • 工厂方法:提供多个派生工厂,用来创建不同的对象。
  • 抽象工厂:用多个派生工厂对象生产不同产品族的全部产品。

# 适配器模式

# 定义

在我们的应用程序中我们可能需要将两个不同接口的类来进行通信,在不修改这两个的前提下我们可能会需要某个中间件来完成这个衔接的过程,这个中间件就是适配器。所谓的适配器模式是一种结构型设计模式,将一个类的接口,转换成客户端期望的另一个接口,允许接口不兼容的类之间能够相互合作。主要有三种主要形式:类适配器、对象适配器、接口适配器

# 类适配器

适配器(Adapter)继承了一个已有的类(Adaptee),并实现了目标接口(Target)。通过继承关系,适配器(Adapter)同时拥有原始类(Adaptee)的功能和目标接口(Target)的行为。

  • Target:客户端 Client 真正期望的接口
  • Adaptee:需要进行适配的类,其中定义了一个已存在的接口
  • Adapter:对 Adaptee 和 Target 的接口进行适配,保证对 target 中接口的调用可以间接转换为对 Adaptee 中接口进行调用

image-20240310143559846

// 已有的类
class Adaptee {
    public void specificRequest() {
        System.out.println("Specific request from Adaptee.");
    }
}
// 目标接口
interface Target {
    void request();
}
// 类适配器
class Adapter extends Adaptee implements Target {
    // 实现 Target 接口中的方法
    public void request() {
        specificRequest();
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        Target target = new Adapter(); // 接口的多态性
        target.request(); // 实际调用的是 Adapter 中定义的方法
    }
}

image-20240310144118342

# 对象适配器

适配器(Adapter)持有一个已有类(Adaptee)的实例(adaptee)【即:类对象组合】,并实现了目标接口(Target)。适配器(Adapter)通过委托的方式调用已有类(Adaptee)的功能(SpecificRequest ()),实现目标接口(Target)的行为(Request ())。

image-20240310145345382

// 已有的类
class Adaptee {
    public void specificRequest() {
        System.out.println("Specific request from Adaptee.");
    }
}
// 目标接口
interface Target {
    void request();
}
// 对象适配器
class Adapter implements Target {
    private Adaptee adaptee; // 持有一个已有类的实例对象
    public Adapter(Adaptee adaptee) { // 构造方法
        this.adaptee = adaptee;
    }
    public void request() { // 实现 Target 接口的方法
        adaptee.specificRequest(); // 通过委托的方式调用已有类的方法
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee(); // 创建已有类的实例
        Target target = new Adapter(adaptee); // 接口的多态性
        target.request();
    }
}

# 接口适配器

接口适配器是一种特殊情况,其中 **适配器(Adapter)实现了目标接口(Target)的某个方法,但是它的方法体是空的。通过接口适配器,子类可以选择性地覆盖感兴趣的方法**,而不必实现所有的方法。

// 目标接口
interface Target {
    void method1();
    void method2();
}
// 接口适配器
abstract class Adapter implements Target {
    public void method1() {
        // 空实现,子类可以选择性地覆盖
    }
    public void method2() {
        // 空实现,子类可以选择性地覆盖
    }
}
// 具体的适配器类
class ConcreteAdapter extends Adapter {
    public void method1() {
        System.out.println("Method1 implementation in ConcreteAdapter.");
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        Target target = new ConcreteAdapter();
        target.method1();  // 调用适配器的方法 1
    }
}

# 优缺点

优点:

  1. 提高了类的复用
  2. 组合若干关联对象形成对外提供统一服务的接口
  3. 扩展性、灵活性好

缺点:

  1. 过多使用适配模式容易造成代码功能、逻辑意义的混淆
  2. 部分语言对继承的限制,可能至多只能适配一个适配者类,而且目标类必须是抽象类。

# 应用场景

  • ** 旧系统升级:** 当需要将旧系统中的组件集成到新系统中时,可能由于接口不兼容而无法直接使用。适配器模式可以将旧系统的接口适配成新系统期望的接口。
  • ** 第三方组件使用:** 当使用某个第三方组件,但其接口与系统要求的接口不一致时,可以使用适配器模式进行适配。

# 代理模式

# 定义

代理模式是一种结构型设计模式,其本质是一个中间件,目的是解耦合服务提供者和使用者使用者通过代理间接地访问提供者,便于对后者的封装和控制。代理对象(Proxy)通常充当客户端(Client)和目标对象(RealSubject)之间的中介,起到控制、过滤和增强功能的作用。

images
使用代理前
images
使用代理后

主要角色如下:

  • Subject:定义 RealSubject 对外的接口,且必须被 Proxy 实现。这样外部调用 proxy 的接口最终都被转化为对 realsubject 的调用。
  • RealSubject:目标对象类。
  • Proxy:目标对象的代理类,负责控制和管理目标对象,并间接地传递外部对目标对象的访问。
    • Remote Proxy(远程代理):对本地的请求以及参数进行序列化,向远程对象发送请求,并对响应结果进行反序列化,将最终结果反馈给调用者;
    • Virtual Proxy(虚拟代理):当目标对象的创建开销比较大的时候,可以使用延迟或者异步的方式创建目标对象或计算,直到真正需要使用时才创建目标对象。这可以用于优化性能,避免不必要的资源开销。
    • Protection Proxy(保护代理):细化对目标对象访问权限的控制,以便限制客户端直接访问目标对象。这种代理常用于实现权限控制、缓存、日志等功能。

# 静态代理

静态代理:编译时就已经确定代理类(Proxy)和目标类(RealSubject)的关系,通过手动编写代理类的方式实现。静态代理需要为每个目标类编写一个对应的代理类,当目标类发生变化时,代理类也需要相应修改

image-20240310152442937

// 目标接口
interface Subject {
    void request();
}
// 目标类
class RealSubject implements Subject {
    public void request() {
        System.out.println("RealSubject handles the request.");
    }
}
// 代理类
class Proxy implements Subject {
    private RealSubject realSubject;
    public Proxy(RealSubject realSubject) {
        this.realSubject = realSubject;
    }
    public void request() {
        System.out.println("Proxy handles the request before delegating to RealSubject.");
        realSubject.request();
        System.out.println("Proxy handles the request after delegating to RealSubject.");
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        Proxy proxy = new Proxy(realSubject);
        proxy.request();
    }
}

# 动态代理

动态代理是 **运行时生成代理类(Proxy),无需手动编写**。Java 中的 java.lang.reflect.ProxyInvocationHandler 接口提供了实现动态代理的机制。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 目标接口
interface Subject {
    void request();
}
// 目标类
class RealSubject implements Subject {
    public void request() {
        System.out.println("RealSubject handles the request.");
    }
}
// 通过 InvocationHandler 接口实现动态代理
class DynamicProxy implements InvocationHandler {
    private Object target;
    public DynamicProxy(Object target) {
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("DynamicProxy handles the request before delegating to RealSubject.");
        Object result = method.invoke(target, args);
        System.out.println("DynamicProxy handles the request after delegating to RealSubject.");
        return result;
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        InvocationHandler handler = new DynamicProxy(realSubject); // 接口的多态性
        Subject proxy = (Subject) Proxy.newProxyInstance(
                Subject.class.getClassLoader(),
                new Class[]{Subject.class},
                handler
        );
        proxy.request();
    }
}

# 二者区别

  1. 灵活性:

    • 静态代理中接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!

    • 动态代理更加灵活,不是必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类

  2. JVM 层面︰

    • 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际 的 class 文件
    • 动态代理是在运行时才动态生成类字节码,并加载到 JVM 中的

# 优缺点

优点:

  1. ** 控制访问:** 代理模式可以控制对目标对象的访问,提供了更灵活的权限控制。
  2. ** 增强功能:** 代理模式可以在目标对象的基础上增加额外的功能,如日志记录、性能监控、缓存等。
  3. 代理类型灵活:可以根据需要选择使用不同类型的代理,包括静态代理、动态代理、虚拟代理等。

缺点:

  1. ** 增加复杂性:** 引入代理类可能会增加系统的复杂性,特别是在静态代理中需要为每个目标类编写一个对应的代理类
  2. 运行时开销:动态代理在运行时生成代理类,可能会带来一些运行时的性能开销。

# 应用场景

  • ** 远程代理:** 当需要在不同地址空间中访问对象时,可以使用远程代理,使得客户端感觉像是在本地访问远程对象。
  • ** 虚拟代理:** 当创建一个对象实例的开销很大时,可以使用虚拟代理来延迟对象的创建,只有在真正需要时才进行实例化。
  • ** 安全控制:** 代理模式可以用于控制对真实对象的访问权限,实现安全控制。
  • ** 日志记录:** 可以使用代理模式在调用真实对象的方法前后进行日志记录、性能监控等操作。

# 装饰器模式

好难啊!

# 定义

装饰器模式是一种结构型设计模式,它允许 **通过将类对象包装在装饰器类的实例中,以期望在不改变类对象及其类定义的情况下,动态地为类对象添加额外的功能 / 属性。简单来说就是,当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个 Decorator 套在原有代码外面。该过程是通过调用被包裹之后的对象完成功能添加的,而不是直接修改现有对象的行为,相当于增加了中间层通过创建一系列相互关联的装饰器类,逐步地添加新的功能**,以达到对类的功能进行动态扩展的目的。

主要角色:

  1. ** 组件(Component):** 一个抽象接口,定义了具体组件 / 被装饰对象和装饰器类的共同接口。
  2. ** 具体组件 / 被装饰对象(ConcreteComponent):** 实现组件接口的具体类,是被装饰的对象
  3. 装饰器(Decorator): 一个抽象类包含一个指向组件对象的引用,并实现与组件接口一致的方法接口,以便可以替代组件。装饰器也可以包含其他装饰器,形成一条装饰链。
  4. 具体装饰器(ConcreteDecorator):继承装饰器类,并重写所有内部方法,还可以额外添加具体的功能

image-20240310163316821

代码例子:

// 组件接口
interface Coffee {
    String getDescription();
    double cost();
}
// 具体组件(被修饰的)
class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "Simple Coffee";
    }
    public double cost() {
        return 2.0;
    }
}
// 装饰器(抽象类!)
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee; // 指向组件接口的对象
    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }
    public String getDescription() { // 这是从 Coffee 接口实现的方法,而非抽象方法!
        return decoratedCoffee.getDescription();
    }
    public double cost() { // 这是从 Coffee 接口实现的方法,而非抽象方法!
        return decoratedCoffee.cost();
    }
}
// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
    public String getDescription() {
        return super.getDescription() + ", with Milk";
    }
    public double cost() {
        return super.cost() + 1.0;
    }
}
// 具体装饰器
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
    public String getDescription() {
        return super.getDescription() + ", with Sugar";
    }
    public double cost() {
        return super.cost() + 0.5;
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        // 创建一个简单的咖啡
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println("Simple Coffee - Cost: $" + simpleCoffee.cost());
        // 添加牛奶装饰器
        Coffee milkCoffee = new MilkDecorator(simpleCoffee); // 包装
        System.out.println("Milk Coffee - Cost: $" + milkCoffee.cost());
        // 再添加糖装饰器
        Coffee milkSugarCoffee = new SugarDecorator(milkCoffee);
        System.out.println("Milk and Sugar Coffee - Cost: $" + milkSugarCoffee.cost());
    }
}

# 优缺点

优点:

  1. 灵活性高: 装饰模式允许在运行时动态地为对象添加新的功能,而无需修改其代码。
  2. 简化代码: 比继承更灵活,避免了通过子类继承来扩展功能的复杂性。
  3. 易于扩展: 可以通过组合多个装饰器实现复杂的功能组合。

缺点:

  1. ** 可能引入许多小对象:** 当装饰器过多时,可能引入大量小型对象,增加系统的复杂性。
  2. ** 理解难度:** 对于初学者来说,理解装饰器模式可能需要一些时间,特别是在有多个装饰器的情况下。

# 应用场景

如果希望在无需修改代码的情况下即可使用对象,且希望在运行时为对象新增额外的行为,可以使用装饰模式。

装饰能将业务逻辑组织为层次结构,可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象

如果用继承来扩展对象行为的方案难以实现或者根本不可行,你可以使用该模式。

许多编程语言使用 final 最终关键字来限制对某个类的进一步扩展。复用 final 类已有行为的唯一方法是使用装饰模式:用封装器对其进行封装

# 通俗解释

当我们使用装饰模式时,可以把它类比成给一杯咖啡加配料的过程。首先,有一杯简单的咖啡,这就是我们的基础组件。然后,我们可以为这杯咖啡加入不同的配料,比如牛奶和糖,而这些配料就是装饰器。

  1. ** 基础咖啡(ConcreteComponent):** 就像最开始的一杯普通咖啡,没有加任何配料。这是我们要装饰的基础对象。

  2. ** 牛奶装饰器(ConcreteDecorator):** 现在我们想要在咖啡中加入牛奶,于是我们创建了一个 “牛奶装饰器” 来包装原始的咖啡。这个装饰器知道如何计算咖啡价格,并在描述中加上 “with Milk”。

  3. ** 糖装饰器(ConcreteDecorator):** 如果我们再想加入糖,我们可以再创建一个 “糖装饰器”,它会包装已经有牛奶的咖啡。这个装饰器也知道如何计算价格,并在描述中加上 “with Sugar”。

这样,我们就可以动态地组合咖啡和各种配料,得到不同种类的咖啡,而不需要修改咖啡本身的类。这就是装饰模式的核心思想:通过将对象包装在装饰器中,逐步地添加新的功能,实现对对象功能的动态扩展。

# 观察者模式

# 定义

观察者模式是一种行为型设计模式,主要 **用于处理对象间的一对多的依赖关系**,使得当一个对象的状态发生变化时,其所有关注者(依赖者)都会收到通知,以进行相应的处理

观察者模式主要涉及两个角色:Subject(目标)和 Observer(观察者)。Subject 负责维护一组观察者,而观察者则订阅 Subject,以便在 Subject 的状态发生变化时接收通知并执行相应的操作

主要组成部分:

  1. **Subject(目标 / 被观察者):** 一个接口 / 抽象类,用于添加、删除和通知观察者。具体的目标对象维护了一组观察者,并在状态发生改变时通知观察者。
  2. **ConcreteSubject(具体目标):** 实现了 Subject 接口并重写了其中的方法,负责维护和管理观察者,并在状态变化时通知观察者。
  3. **Observer(观察者):** 一个接口 / 抽象类,用于接收 Subject 的通知。观察者中通常包含一个更新方法,用于在接收到通知时执行相应的操作。
  4. **ConcreteObserver(具体观察者):** 实现了 Observer 接口,它注册到具体目标,接收目标的通知,并在状态变化时执行具体的业务逻辑。

下面是 GoF 介绍的典型的类观察者模式的 UML 类图:

image-20240310160236309

import java.util.ArrayList;
import java.util.List;
// Subject(目标)
interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}
// ConcreteSubject(具体目标)
class ConcreteSubject implements Subject {
    private int state;
    private List<Observer> observers = new ArrayList<>();
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(state);
        }
    }
    public void setState(int state) {
        this.state = state;
        notifyObservers(); // 状态变化时通知观察者
    }
}
// Observer(观察者)
interface Observer {
    void update(int state);
}
// ConcreteObserver(具体观察者)
class ConcreteObserver implements Observer {
    private String name;
    public ConcreteObserver(String name) {
        this.name = name;
    }
    public void update(int state) {
        System.out.println(name + " received update. New state: " + state);
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();
        ConcreteObserver observer1 = new ConcreteObserver("Observer 1");
        ConcreteObserver observer2 = new ConcreteObserver("Observer 2");
        subject.addObserver(observer1);
        subject.addObserver(observer2);
        subject.setState(10); // 观察者将收到通知并更新
    }
}

# 优缺点

优点:

  1. ** 解耦性强:** 将目标和观察者解耦,使得它们可以独立变化,增加了系统的灵活性。
  2. ** 扩展性好:** 可以轻松地添加、删除观察者,使系统更容易扩展。
  3. 支持广播通信:目标状态变化时,所有观察者都能收到通知,实现了一对多的通信机制

缺点:

  1. ** 可能导致性能问题:** 如果通知过于频繁 / 通知链路过长,可能会导致性能问题。因此,需要仔细设计,避免不必要的通知。
  2. ** 可能引起循环依赖:** 在设计中要注意避免循环依赖,以免触发无限循环的通知

# 应用场景

在支付场景下,用户购买一件商品,当支付成功之后会回调自身,在这个时候系统可能会有很多需要执行的观察者逻辑(如:更新订单状态,发送邮件通知,赠送礼品...),这些逻辑之间并没有强耦合,因此天然适合使用观察者模式去实现这些功能。当有更多的操作时,只需要添加新的观察者,完美实现了对修改关闭,对扩展开放的开闭原则

# 责任链模式

# 定义

责任链模式是一种行为型设计模式,对于一个请求,允许创建一条对象链,上面的每个对象都包含处理该请求的一部分逻辑,并且该请求会沿着链传递,直到某个对象处理它为止。这样就可以动态地组织对象链,而不需要改变其内部结构。此外,一个请求可以被多个对象处理

责任链模式非常简单、异常好理解,相信我它比单例模式还简单易懂,其应用也几乎无所不在,甚至可以这么说,从你敲代码的第一天起你就不知不觉用过了它最原始的裸体结构︰ switch-case 语句

主要角色:

  1. 处理者(Handler): 一个处理请求的接口,通常包含一个处理请求的方法。具体的处理者实现该接口,负责处理请求,也可以把请求传递给链中的下一个处理者。
  2. 具体处理者(ConcreteHandler): 实现处理者接口,负责处理特定类型的请求。如果它不能处理请求,可以将请求传递给链中的下一个处理者
  3. 客户端(Client): 创建请求并将其发送到链的第一个处理者。

img

让我们以一个报销审批的场景为例,其中有一系列处理者,每个处理者负责审批不同金额范围的报销请求。

// 处理者接口
interface Approver {
    void processRequest(Expense expense);
}
// 具体处理者
class Manager implements Approver {
    private static final double MAX_APPROVAL_AMOUNT = 1000;
    private Approver nextApprover;
    public void setNextApprover(Approver nextApprover) {
        this.nextApprover = nextApprover;
    }
    public void processRequest(Expense expense) {
        if (expense.getAmount() <= MAX_APPROVAL_AMOUNT) {
            System.out.println("Manager approves the expense request of $" + expense.getAmount());
        } else if (nextApprover != null) {
            nextApprover.processRequest(expense);
        }
    }
}
class Director implements Approver {
    private static final double MAX_APPROVAL_AMOUNT = 5000;
    private Approver nextApprover;
    public void setNextApprover(Approver nextApprover) {
        this.nextApprover = nextApprover;
    }
    public void processRequest(Expense expense) {
        if (expense.getAmount() <= MAX_APPROVAL_AMOUNT) {
            System.out.println("Director approves the expense request of $" + expense.getAmount());
        } else if (nextApprover != null) {
            nextApprover.processRequest(expense);
        }
    }
}
class VicePresident implements Approver {
    private static final double MAX_APPROVAL_AMOUNT = 10000;
    private Approver nextApprover;
    public void setNextApprover(Approver nextApprover) {
        this.nextApprover = nextApprover;
    }
    public void processRequest(Expense expense) {
        if (expense.getAmount() <= MAX_APPROVAL_AMOUNT) {
            System.out.println("Vice President approves the expense request of $" + expense.getAmount());
        } else if (nextApprover != null) {
            nextApprover.processRequest(expense);
        }
    }
}
class President implements Approver {
    private static final double MAX_APPROVAL_AMOUNT = 50000;
    public void processRequest(Expense expense) {
        if (expense.getAmount() <= MAX_APPROVAL_AMOUNT) {
            System.out.println("President approves the expense request of $" + expense.getAmount());
        } else {
            System.out.println("Expense request of $" + expense.getAmount() + " is rejected.");
        }
    }
}
// 报销请求类
class Expense {
    private double amount;
    public Expense(double amount) {
        this.amount = amount;
    }
    public double getAmount() {
        return amount;
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        // 创建处理者
        Approver manager = new Manager();
        Approver director = new Director();
        Approver vicePresident = new VicePresident();
        Approver president = new President();
        // 设置处理链
        manager.setNextApprover(director);
        director.setNextApprover(vicePresident);
        vicePresident.setNextApprover(president);
        // 创建报销请求
        Expense expense1 = new Expense(800);
        Expense expense2 = new Expense(2500);
        Expense expense3 = new Expense(12000);
        Expense expense4 = new Expense(60000);
        // 提交报销请求
        manager.processRequest(expense1);
        manager.processRequest(expense2);
        manager.processRequest(expense3);
        manager.processRequest(expense4);
    }
}

# 优缺点

优点:

  1. 解耦责任链的发送者和接收者: 发送者无需知道具体的处理者,而处理者也无需知道请求的发送者,解耦了系统的组件。
  2. ** 可动态调整责任链:** 可以灵活地增加、删除或调整责任链中的处理者,而不影响其他部分的代码。
  3. ** 符合开闭原则:** 可以通过增加新的处理者来扩展系统,而无需修改现有代码。

缺点:

  1. 请求可能无法得到处理: 如果责任链没有得到妥善设计,可能会出现请求无法被处理的情况,或者因为责任链太长而导致性能问题

# 应用场景

  • 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。该模式能将多个处理者连接成一条链。接收到请求后,它会 “询问” 每个处理者是否能够对其进行处理。这样所有处理者都有机会来处理请求。

  • 当必须按顺序执行多个处理者时,可以使用该模式。无论你以何种顺序将处理者连接成一条链,所有请求都会严格按照顺序通过链上的处理者。

# 策略模式

# 定义

策略模式是一种行为型设计模式,其用意是 **针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换**。策略模式使得算法可以在不影响到客户端的情况下发生变化。其主要目的是通过定义相似的算法,替换 if else 语句写法,并且可以随时相互替换。

主要角色:

  1. 策略接口(Strategy): 一个通用策略接口
  2. 具体策略类(ConcreteStrategy): 实现了策略接口,包含了具体的算法实现。
  3. 上下文类(Context): 持有一个策略接口的对象引用,用于调用具体的策略。客户端通过上下文类来使用策略。

代码实现:

// 策略接口
interface DiscountStrategy {
    double applyDiscount(double amount);
}
// 具体策略类 - 无折扣
class NoDiscountStrategy implements DiscountStrategy {
    public double applyDiscount(double amount) {
        return amount;
    }
}
// 具体策略类 - 打九折
class TenPercentDiscountStrategy implements DiscountStrategy {
    public double applyDiscount(double amount) {
        return amount * 0.9;
    }
}
// 具体策略类 - 满减
class CashBackDiscountStrategy implements DiscountStrategy {
    private double threshold;
    private double cashBack;
    public CashBackDiscountStrategy(double threshold, double cashBack) {
        this.threshold = threshold;
        this.cashBack = cashBack;
    }
    public double applyDiscount(double amount) {
        return amount >= threshold ? amount - cashBack : amount;
    }
}
// 上下文类
class ShoppingCart {
    private DiscountStrategy discountStrategy;
    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }
    public double checkout(double amount) {
        return discountStrategy.applyDiscount(amount);
    }
}
// 客户端使用
public class Client {
    public static void main(String[] args) {
        // 定义上下文类的对象
        ShoppingCart cart = new ShoppingCart();
        // 选择无折扣策略
        cart.setDiscountStrategy(new NoDiscountStrategy());
        double amount1 = cart.checkout(100);
        System.out.println("Total after no discount: $" + amount1);
        // 选择打九折策略
        cart.setDiscountStrategy(new TenPercentDiscountStrategy());
        double amount2 = cart.checkout(100);
        System.out.println("Total after 10% discount: $" + amount2);
        // 选择满减策略
        cart.setDiscountStrategy(new CashBackDiscountStrategy(50, 10));
        double amount3 = cart.checkout(100);
        System.out.println("Total after cashback discount: $" + amount3);
    }
}

# 优缺点

优点:

  1. ** 算法独立性:** 策略模式使得算法独立于客户端使用而变化,客户端可以灵活地选择策略,甚至在运行时动态切换。
  2. ** 易于扩展:** 新的策略可以很容易地添加到系统中,扩展性好。
  3. ** 避免条件语句:** 使用策略模式可以避免大量的条件语句,提高代码可读性和可维护性。

缺点:

  1. 类数量增加:每个具体策略都需要一个独立的类,可能会导致类的数量增加,增加系统复杂性。

# 应用场景(好处)

  1. 算法经常变化: 当系统中的算法经常发生变化,并且客户端需要灵活地选择不同算法的时候,策略模式就可以发挥作用。例如,不同的排序算法、不同的数据压缩算法等。
  2. 需要在运行时动态选择算法: 如果需要在运行时根据不同的情况选择不同的算法,而且希望将算法的使用与算法的实现解耦,策略模式是一个很好的选择。
  3. 避免使用条件语句: 当一个类有多个相关的条件语句,并且这些条件语句在不同的场景下产生不同的行为,可以考虑使用策略模式。这样可以避免大量的条件语句,提高代码的可维护性。
  4. 需要一系列相关的算法: 如果一个系统中有一系列相似的算法,可以将每个算法封装成一个策略类,通过策略模式来管理和调用这些算法,提高代码的可读性和可维护性。
  5. 算法具有复杂性: 当某个算法的实现比较复杂,且包含许多条件语句和分支时,使用策略模式可以将算法的实现拆分成多个策略类,使代码更加清晰和可理解。

举例来说,一个电商系统中的购物车结算功能就是一个常见的应用场景。不同的用户或不同的时期可能享受不同的优惠策略,这时可以使用策略模式来管理这些优惠策略,客户端可以根据用户或时期的不同来选择合适的策略进行结算。

举例: Java.util.List 定义了一个增(add) 、删(remove) 、改 (set) 、查(indexOf)策略,至于实现了这个策略的 ArrayListLinkedList 等不同类,它们只是在具体实现时采用了不同的算法,但是策略是一样的,不考虑速度的情况下,使用时完全可以互相替换使用。

# Spring 中涉及的设计模式

  • 工厂设计模式 : Spring 使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 模板方法模式 : Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 :Spring AOP 的增强或通知 (Advice) 使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller
  • 装饰器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • ……

# 控制反转(IoC)

**IoC(Inversion of Control,控制反转)** 是 Spring 中一个非常重要的概念,它是一种解耦的设计思想,有多种实现方式,其核心思想是:

  • 将 **对象的创建权** 交给第三方容器
  • 将 **对象之间关系的管理维护权** 交给第三方容器

第三方容器指的是 Spring 中的 IoC 容器,它能管理对象,从而解耦具有依赖关系的对象,开发人员只管使用 IoC 容器即可,降低了代码之间的耦合度。

Spring IoC = XML 解析 + 工厂模式 + 反射机制

image-20240311164331487

IoC 可以认为是一种全新的设计模式,但是理论和时间成熟相对较晚,并没有包含在 GoF 的 23 种设计模式中。

image-20240310193136124

IoC 以及各种实现方式

类似于工厂设计模式,Spring IoC 容器就像是一个工厂,当我们需要创建对象时,只需配置好配置文件 / 注解即可,完全不用考虑对象是如何被创建出来的。IoC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁

在实际项目中一个 Service 类如果有几百甚至上千个类作为它的底层,我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性,且降低了开发难度

关于 Spring IOC 的理解,推荐看这一下知乎的一个回答:https://www.zhihu.com/question/23277575/answer/169698662open in new window ,非常不错。

通俗理解:控制反转(IoC)就像是一家餐馆,你在餐馆里吃饭时,不需要亲自去厨房做菜,而是把控制权(决定菜单、烹饪方式等)交给了餐馆。在编程中,IoC 是一种思想,你不再需要自己去创建和管理对象,而是把这个任务交给了框架或容器。在 Spring 中,IoC 意味着 **你不用自己负责 Bean(对象)的创建和管理,而是把这些任务交给了 Spring 容器**。

举个例子:" 对象 a 依赖了对象 b,当对象 a 需要使用对象 b 的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象 a 和对象 b 之间就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b 的时候,可以指定 IOC 容器去创建一个对象 b 注入到对象 a 中"。对于对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权反转,这就是控制反转名字的由来。

# 依赖注入(DI)

前文提到的 IoC 是一种思想,有多种实现方式,其中最常见的实现方式就是 **DI(Dependency Inject,依赖注入)**,就是将实例变量传入到一个对象中去

  • 依赖:A 对象和 B 对象的关系。
  • 注入:是一种手段,通过这种手段,可以让 A 对象和 B 对象产生关系。
  • 依赖注入:对象 A 和对象 B 之间的关系,靠注入的手段来维护。

依赖注入包括两种方式:

  • set 注入:发生在对象实例化后,通过反射机制调用 set 方法 来给属性赋值,让两个对象之间产生关系。
  • 构造注入:发生在对象实例化中,通过调用 构造方法 来给属性赋值。

通俗理解:依赖注入就像是餐馆把菜单上的菜送到你面前,而不是你去主动取。在编程中,DI 是一种实现 IoC 的方式,它让框架或容器负责将需要的对象传递给你的代码,而不是你去创建或查找这些对象。在 Spring 中,DI 意味着 **你不需要亲自创建对象的实例,而是通过配置或注解告诉 Spring IoC 容器,框架将会在需要的时候将对象注入到你的代码中**。

image-20240312150320154

# 简单工厂

Spring 的 BeanFactory 接口是 IOC 容器的顶级接口,充当 Bean 工厂,根据指定的类名或 ID 创建 Bean 实例。

# 工厂方法

Spring 的 FactoryBean 接口允许开发人员在 Spring 容器中注册一个自定义的 Bean 工厂,自定义 Bean 的创建逻辑,然后在配置文件中配置该工厂,Spring 容器将使用该工厂创建 Bean 实例。

# 工厂设计模式

前文有介绍工厂设计模式

Spring 使用工厂模式可以通过 BeanFactory 接口或 ApplicationContext 接口创建 bean 对象。

  • BeanFactory 接口:
    • 是 IOC 容器的顶级接口
    • 能够产生 Bean 对象的一个工厂对象
    • 延迟注入(使用到某个 bean 的时候才会注入)
    • 相比于 ApplicationContext 来说会占用更少的内存,程序启动速度更快
  • ApplicationContext 接口:
    • 容器启动时一次性创建所有 bean,不管你用没用到
    • BeanFactory 仅提供了最基本的依赖注入支持, ApplicationContext 继承扩展了 BeanFactory ,所以一般开发人员使用 ApplicationContext 会更多

ApplicationContext 接口的三个实现类:

  1. ClassPathXmlApplication :把上下文文件当成类路径资源。
  2. FileSystemXmlApplication :从文件系统中的 XML 文件载入上下文定义信息。
  3. XmlWebApplicationContext :从 Web 系统中的 XML 文件载入上下文定义信息。

image-20240312153233963

Example:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class App {
  public static void main(String[] args) {
    ApplicationContext context = new FileSystemXmlApplicationContext(
        "C:/work/IOC Containers/springframework.applicationcontext/src/main/resources/bean-factory-config.xml");
    HelloApplicationContext obj = (HelloApplicationContext) context.getBean("helloApplicationContext");
    obj.getMsg();
  }
}

# 单例设计模式

前文有介绍单例设计模式

**Spring 中 bean 实例的默认作用域就是 singleton (单例) 的,确保在容器中只有一个共享的实例,在创建上下文的时候初始化。** 此外,Spring 中 bean 还有下面几种作用域:

  • prototype(原型) : 每调用一次 getBean () 都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean

Spring 通过 ConcurrentHashMap 实现单例注册表,核心代码如下:

// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            // 检查缓存中是否存在实例
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //... 省略了很多代码
                try {
                    singletonObject = singletonFactory.getObject();
                }
                //... 省略了很多代码
                // 如果实例对象在不存在,我们注册到单例注册表中。
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }
    // 将对象添加到单例注册表
    protected void addSingleton(String beanName, Object singletonObject) {
            synchronized (this.singletonObjects) {
                this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
            }
        }
}

单例 Bean 存在线程安全问题吗?

大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。常见的有两种解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量。
  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

# 代理设计模式

前文有介绍代理设计模式

# 面向切面编程(AOP)

动态代理

**AOP(Aspect-Oriented Programming,面向切面编程)** 是一种“模块组件”的编程思想,是对 OOP 的延伸。其中,切面指的是那些与核心业务逻辑无关的通用逻辑代码,专业术语为 “交叉业务”,例如日志管理、安全模块、事务处理和权限控制等。AOP 将与核心业务无关的代码独立的抽取出来,形成一个独立的组件 (切面),然后以横向交叉的方式应用到业务流程当中。如下图所示,纵向的是业务逻辑,横向的是交叉业务。通过将交叉业务封装为切面,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

image-20240312174811414

Spring AOP 的底层实现是:JDK 动态代理 + CGLIB 动态代理,Spring 在这两种动态代理中灵活切换,也可以强制通过一些配置让 Spring 只使用 CGLIB 。

  • JDK 动态代理基于接口。如果目标对象实现了某个接口,那么默认使用 JDK Proxy 去创建代理对象
  • CGLIB 动态代理基于父类。如果目标对象没有实现任何接口,就会使用 CGLIB 生成一个被代理对象的子类来作为代理

在这里插入图片描述

基于动态代理实现 Spring AOP

基于动态代理实现 Spring AOP

当然,也可以使用 AspectJ 来实现 Spring AOP,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等场景都用到了 AOP 。

# Spring AOP 和 AspectJ 的区别

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理 (Proxying),而 AspectJ 基于字节码操作 (Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单。

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多

# 模板方法设计模式

模板方法设计模式是一种行为型设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。

public abstract class Template {
    // 这是我们的模板方法
    public final void TemplateMethod(){
        PrimitiveOperation1();
        PrimitiveOperation2();
        PrimitiveOperation3();
    }
    protected void  PrimitiveOperation1(){
        // 当前类实现
    }
    // 被子类实现的方法
    protected abstract void PrimitiveOperation2();
    protected abstract void PrimitiveOperation3();
}
public class TemplateImpl extends Template {
    @Override
    public void PrimitiveOperation2() {
        // 当前类实现
    }
    @Override
    public void PrimitiveOperation3() {
        // 当前类实现
    }
}

Spring 的许多模块和外部扩展都采用模板方法设计模式,例如 JdbcTemplateHibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用 Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。

在 Spring 中, JdbcTemplateHibernateTemplate 都是用于简化数据库访问的模板类。它们通过模板方法设计模式提供了一种标准的方式来执行数据库操作,同时隐藏了底层数据库访问的细节,使得开发者能够专注于业务逻辑的实现而不必关注底层数据库操作的细节。

模板方法设计模式的核心思想是定义一个算法的骨架,将算法的一些步骤延迟到子类中实现。在 JdbcTemplateHibernateTemplate 中,这个算法的骨架就是数据库访问的过程,而延迟到子类中实现的部分则是具体的数据库操作

下面简要介绍一下两者如何体现模板方法设计模式:

  1. JdbcTemplate
    JdbcTemplate 是 Spring 提供的用于执行 SQL 查询、更新等操作的模板类。它封装了 JDBC 的调用,简化了 JDBC 的使用。 JdbcTemplate 中的模板方法就是 execute() 方法,通过该方法可以执行任意 SQL 查询或更新操作,而具体的 SQL 语句和参数则由用户提供。例如:

    jdbcTemplate.execute(connection -> {
        PreparedStatement ps = connection.prepareStatement("SELECT * FROM users WHERE username = ?");
        ps.setString(1, "john_doe");
        ResultSet rs = ps.executeQuery();
        // 处理结果集
        return null;
    });

    在这个例子中, execute() 方法就是模板方法,它提供了数据库连接的获取、SQL 语句的执行等步骤的骨架,而具体的 SQL 查询逻辑则由用户提供

  2. HibernateTemplate
    HibernateTemplate 是 Spring 对 Hibernate 框架的封装,提供了一种简化 Hibernate 操作的方式。在 HibernateTemplate 中,模板方法就是 execute() 方法,它用于执行 Hibernate 操作,比如加载、保存、更新、删除对象等。例如:

    hibernateTemplate.execute(session -> {
        User user = session.get(User.class, 1L);
        // 修改用户信息
        session.update(user);
        return null;
    });

    在这个例子中, execute() 方法提供了 Hibernate 操作的骨架,包括获取 Hibernate Session、执行操作、事务管理等,而具体的业务逻辑则由用户提供。

通过模板方法设计模式, JdbcTemplateHibernateTemplate 封装了底层的数据库操作细节,提供了一种统一的、标准的方式来执行数据库操作,使得代码更加简洁、易读,并且降低了开发者使用底层框架的学习成本。

# 观察者设计模式

前文有介绍观察者模式

观察者模式是一种对象行为型模式。它表示的是对象与对象之间具有一对多依赖关系,当一个对象发生改变的时候,依赖这个对象的所有对象也会做出反应

**Spring 事件监听机制** 允许组件监听和响应特定类型的事件,实现了松耦合的组件通信,就是观察者模式很经典的一个应用。在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

# Spring 事件监听机制的三种角色

# 事件

img

ApplicationEvent ( org.springframework.context 包下)充当事件的角色,这是一个抽象类,它继承了 java.util.EventObject 并实现了 java.io.Serializable 接口。

public abstract class ApplicationEvent extends EventObject {
   /** use serialVersionUID from Spring 1.2 for interoperability */
   private static final long serialVersionUID = 7099057708183571937L;
   /** System time when the event happened */
   private final long timestamp;
   public ApplicationEvent(Object source) {
      super(source);
      this.timestamp = System.currentTimeMillis();
   }
   public final long getTimestamp() {
      return this.timestamp;
   }
}

Spring 中默认存在以下事件,他们都是对 ApplicationContextEvent 的实现 (继承自 ApplicationContextEvent ):

  • ContextStartedEventApplicationContext 启动后触发的事件;
  • ContextStoppedEventApplicationContext 停止后触发的事件;
  • ContextRefreshedEventApplicationContext 初始化或刷新完成后触发的事件;
  • ContextClosedEventApplicationContext 关闭后触发的事件。
public abstract class ApplicationContextEvent extends ApplicationEvent {
   public ApplicationContextEvent(ApplicationContext source) {
      super(source);
   }
   public final ApplicationContext getApplicationContext() {
      return (ApplicationContext) getSource();
   }
}

# 事件监听者

ApplicationListener 接口充当了事件监听者角色,里面只定义了一个 onApplicationEvent() 方法来处理 ApplicationEvent 。源码如下,可以看出接口中的事件只要继承了 ApplicationEvent 就可以了。所以,在 Spring 中我们只要实现 ApplicationListener 接口的 onApplicationEvent() 方法即可完成监听事件。

package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}

# 事件发布者(事件源)

ApplicationEventPublisher 接口充当了事件的发布者,源码如下:

@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }
    void publishEvent(Object var1);
}

其中的 publishEvent() 这个方法在 AbstractApplicationContext 类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过 ApplicationEventMulticaster 来广播出去的。具体内容过多,就不在这里分析了。

# Spring 的事件流程小结

  1. 定义一个事件:实现一个继承自 ApplicationEvent 的类,并且写相应的构造函数;

  2. 定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;

  3. 使用事件发布者发布消息:可以通过 ApplicationEventPublisherpublishEvent() 方法发布消息。

// 定义一个事件,继承自 ApplicationEvent 并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
    private static final long serialVersionUID = 1L;
    private String message;
    public DemoEvent(Object source,String message){
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}
// 定义一个事件监听者,实现 ApplicationListener 接口,重写 onApplicationEvent () 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{
    // 使用 onApplicationEvent 接收消息
    @Override
    public void onApplicationEvent(DemoEvent event) {
        String msg = event.getMessage();
        System.out.println("接收到的信息是:"+msg);
    }
}
// 发布事件,可以通过 ApplicationEventPublisher  的 publishEvent () 方法发布消息。
@Component
public class DemoPublisher {
    @Autowired
    ApplicationContext applicationContext;
    public void publish(String message){
        // 发布事件
        applicationContext.publishEvent(new DemoEvent(this, message));
    }
}

当调用 DemoPublisherpublish() 方法的时候,比如 demoPublisher.publish("你好") ,控制台就会打印出: 接收到的信息是:你好

# 适配器设计模式

前文有介绍适配器模式

适配器模式 (Adapter Pattern) 将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作

# Spring AOP 中

我们知道 Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是  AdvisorAdapter

通知(Advice)常用的类型有:

  • 前置通知:使用 @Before 注解标识,在被代理的目标方法前执行
  • 返回通知:使用 @AfterReturning 注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
  • 异常通知:使用 @AfterThrowing 注解标识,在被代理的目标方法异常结束后执行(死于非命)
  • 后置通知:使用 @After 注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
  • 环绕通知:使用 @Around 注解标识,使用 try...catch...finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

每个类型 Advice 都有对应的拦截器: MethodBeforeAdviceInterceptorAfterReturningAdviceInterceptorThrowsAdviceInterceptor 等等。

Spring 预定义的 Advice 要通过对应的适配器,适配成 MethodInterceptor 接口 (方法拦截器) 类型的对象(如: MethodBeforeAdviceAdapter 通过调用 getInterceptor 方法,将 MethodBeforeAdvice 适配成 MethodBeforeAdviceInterceptor )。

# Spring MVC 中

在 Spring MVC 中, DispatcherServlet 根据请求信息调用 HandlerMapping ,解析请求对应的 Handler 。解析到对应的 Handler (也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。 HandlerAdapter 作为适配器(期望接口),有不同的实现类来处理不同类型的处理器(需要适配的类)

image-20240313164642619

为什么要在 Spring MVC 中使用适配器模式?

Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话, DispatcherServlet 直接获取对应类型的 Controller ,需要自行来判断,像下面这段代码一样:

if(mappedHandler.getHandler() instanceof MultiActionController){
   ((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
    ...
}else if(...){
   ...
}

假如我们再增加一个 Controller 类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 —— 对扩展开放,对修改关闭。

# 装饰器设计模式

前文有介绍装饰器模式

装饰器模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰器模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个 Decorator 套在原有代码外面

其实在 JDK 中就有很多地方用到了装饰器模式,比如 InputStream 家族, InputStream 类下有 FileInputStream (读取文件)、 BufferedInputStream (增加缓存,使读取文件速度大大提升) 等子类都在不修改 InputStream 代码的情况下扩展了它的功能。

装饰器模式 示意图

装饰器模式 示意图

Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰器模式 (这一点我自己还没太理解具体原理)。

Spring 中用到的包装器模式在类名上含有 Wrapper 或者 Decorator ,这些类基本上都是动态地给一个对象添加一些额外的职责。例如 BeanWrapper 允许在不修改原始 Bean 类的情况下添加额外的功能

# 策略模式

前文有介绍策略模式

Spring 允许使用策略模式来定义包扫描时的过滤策略,例如在 @ComponentScan 注解中使用的 excludeFilters 和 includeFilters 。

# 责任链设计模式

Spring AOP 通过责任链模式实现通知(Advice)的调用,确保通知按照顺序执行。

# JDK 中涉及的设计模式

# 桥接模式

这个模式将抽象和对应实现进行解耦,使得二者可以独立地变化

GOF 在提出桥梁模式的时候指出,桥梁模式的用意是 “将抽象化 (Abstraction) 与实现化 (Implementation) 脱耦,使得二者可以独立地变化”。这句话有三个关键词,也就是抽象化、实现化和脱耦。

在 Java 应用中,对于桥接模式有一个非常典型的例子,就是 **应用程序使用 JDBC 驱动程序进行开发的方式**。所谓驱动程序,指的是按照预先约定好的接口来操作计算机系统或者是外围设备的程序。

# 适配器模式

用来把一个接口转化成另一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

java.util.Arrays#asList()
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)

# 组合模式

又叫做部分 - 整体模式,使得客户端看来单个对象和对象的组合是同等的。换句话说,某个类型的方法同时也接受自身类型作为参数。

java.util.Map#putAll(Map)
java.util.List#addAll(Collection)
java.util.Set#addAll(Collection)

# 装饰器模式

动态地给一个对象附加额外的功能 / 属性,这也是子类的一种替代方式。可以看到,在创建一个类型的时候,同时也传入同一类型的对象。这在 JDK 里随处可见,你会发现它无处不在,所以下面这个列表只是一小部分。

java.io.FileInputStream(InputStream)
java.io.BufferedInputStream(InputStream)
java.io.DataInputStream(InputStream)

java.io.BufferedOutputStream(OutputStream)
java.util.zip.ZipOutputStream(OutputStream)

java.util.Collections#checkedList|Map|Set|SortedSet|SortedMap

比如 InputStream 类下有 FileInputStream (读取文件)、 BufferedInputStream (增加缓存,使读取文件速度大大提升) 等子类都在不修改 InputStream 代码的情况下扩展了它的功能。

# 享元模式

使用缓存来加速大量小对象的访问时间

java.lang.Integer#valueOf(int)
java.lang.Boolean#valueOf(boolean)
java.lang.Byte#valueOf(byte)
java.lang.Character#valueOf(char)

# 代理模式

java.lang.reflect.Proxy
RMI

# 工厂方法模式

一个返回具体对象的方法

java.lang.Proxy#newProxyInstance()
java.lang.Object#toString()
java.lang.Class#newInstance()
java.lang.reflect.Array#newInstance()
java.lang.reflect.Constructor#newInstance()
java.lang.Boolean#valueOf(String)
java.lang.Class#forName()

# 抽象工厂模式

抽象工厂模式提供了一个协议来生成一系列的相关或者独立的对象,而不用指定具体对象的类型。它使得应用程序能够和使用的框架的具体实现进行解耦。

这在 JDK 或者许多框架比如 Spring 中都随处可见。它们也很容易识别,一个创建新对象的方法,返回的却是接口或者抽象类的,就是抽象工厂模式了

java.util.Calendar#getInstance()
java.util.Arrays#asList()
java.util.ResourceBundle#getBundle()
java.sql.DriverManager#getConnection()
java.sql.Connection#createStatement()
java.sql.Statement#executeQuery()
java.text.NumberFormat#getInstance()
javax.xml.transform.TransformerFactory#newInstance()

# 建造者模式

定义了一个新类来构建另一个类的实例,以简化复杂对象的创建。建造模式通常也使用方法链接来实现。

java.lang.StringBuilder#append()
java.lang.StringBuffer#append()
java.sql.PreparedStatement
javax.swing.GroupLayout.Group#addComponent(

# 原型模式

如果创建一个实例非常复杂耗时,可以通过复制现有实例来创建新实例

java.lang.Object#clone()
java.lang.Cloneable

# 单例模式

用来确保类只有一个实例。Joshua Bloch 在 Effetive Java 中建议到,还有一方法就是使用枚举类

java.lang.Runtime#getRuntime()
java.awt.Toolkit#getDefaultToolkit()
java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()
java.awt.Desktop#getDesktop()

# 责任链模式

通过把请求从一个对象传递到链条中下一个对象的方式,直到请求被处理完毕,以实现对象间的解耦。

java.util.logging.Logger#log()
javax.servlet.Filter#doFilter()

# 命令模式

将操作封装到对象内,以便存储,传递和返回

java.lang.Runnable
javax.swing.Action

# 解释器模式

定义了⼀个语⾔的语法,然后解析相应语法的语句

java.util.Pattern
java.text.Normalizer
java.text.Format

# 迭代器模式

提供一个一致的方法来顺序访问集合中的对象,而该方法与集合的底层具体实现无关

java.util.Iterator
java.util.Enumeration

# 中介者模式

通过使用一个中间对象来进行消息分发以及减少类之间的直接依赖

java.util.Timer
java.util.concurrent.Executor#execute()
java.util.concurrent.ExecutorService#submit()
java.lang.reflect.Method#invoke()

# 备忘录模式

生成对象状态的一个快照,以便对象可以恢复原始状态而不用暴露自身的内容。Date 对象通过自身内部的一个 long 值来实现备忘录模式。

java.util.Date
java.io.Serializable

# 空对象模式

通过一个无意义的对象来代替没有对象的状态,使得你不需要额外处理空对象

java.util.Collections#emptyList()
java.util.Collections#emptyMap()
java.util.Collections#emptySet()

# 观察者模式

使得一个对象可以灵活地将消息发送给感兴趣的对象

java.util.EventListener
javax.servlet.http.HttpSessionBindingListener
javax.servlet.http.HttpSessionAttributeListener
javax.faces.event.PhaseListener

# 状态模式

通过改变对象内部的状态,使得可以在运行时动态改变一个对象的行为

java.util.Iterator
javax.faces.lifecycle.LifeCycle#execute()

# 策略模式

将一组算法封装成一系列对象,通过传递这些对象可以灵活的改变程序的功能

java.util.Comparator#compare()
javax.servlet.http.HttpServlet
javax.servlet.Filter#doFilter()

# 模板方法模式

让子类可以重写方法的一部分,而不是整个重写,你可以控制子类需要重写那些操作。

java.util.Collections#sort()
java.io.InputStream#skip()
java.io.InputStream#read()
java.util.AbstractList#indexOf()

# 访问者模式

提供一个方便的可维护的方式来操作一组对象。它使得你在不改变操作的对象前提下,可以修改或者扩展对象的行为。

javax.lang.model.element.Element and 
javax.lang.model.element.ElementVisitor
javax.lang.model.type.TypeMirror and 
javax.lang.model.type.TypeVisitor