一篇文章带你搞懂Java“设计模式”! - - 超长文(涵盖23种)万字总结!【汇总篇】

一篇文章带你搞懂Java"设计模式"! - - 超长文(涵盖23种)万字总结!【汇总篇】

设计模式的七大原则

设计模式是为了让程序具有更好的:

代码可重用性、可读性、可扩展性、可靠性、呈现高内聚,低耦合。

单一职责原则

对类来说,一个类应该只负责一项职责。如果类A负责两个不同的职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。

注意事项

复制代码
- 降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性,可维护性。
- 降低变更引起的风险。
- 通常情况下,应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别保持单一职责原则。
接口隔离原则

类不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上


(把接口从左图一个叫接口,拆分成右图三个接口)

依赖倒转(倒置)原则

高层模块不应该依赖底层模块,二者都应该依赖其抽象。

抽象不应该依赖细节,细节应该依赖抽象。

依赖倒转的中心思想是面向接口编程

依赖关系传递的三种方式:

复制代码
- 接口传递
- 构造方法传递
- setter方法传递

注意事项

复制代码
- 底层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
- 变量的声明类型尽量是抽象类或接口,这样变量引用和实习对象间,就存在一个缓冲层,利于程序的扩展和优化
- 继承时遵循里氏替换原则
里氏替换原则

所有引用基类的地方必须能透明地使用其子类的对象

在使用继承时,遵循里氏替换原则,则在子类中尽量不要重写父类的方法

里氏替换原则说明,继承实际上让两个类耦合性增强,在适当情况下,可以通过集合,组合,依赖来解决问题

开闭原则

开闭原则是最基础,也是最重要的设计原则,是设计模式的目的

一个软件实体(类、模块和函数等)应该对扩展开放 (对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。

当软件需要变化时,尽量通过扩展 软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

即要让代码遵循OCP原则

迪米特法则(最少知道原则)

一个对象应该对其他对象保持最少的了解,即不属于我的部分,你要自己实现。

迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。即对于被依赖的类不管多么复杂,尽量将逻辑封装在自己类的内部。对外除了提供的public方法,不泄漏任何信息

迪米特法则还有个更简单的定义:只与直接的朋友通信

直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部

迪米特法则的核心是降低类之间的耦合,但注意,由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖。

合成复用原则

原则是尽量使用合成/聚合的方式,而不是使用继承。


单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法

单例模式有八种方式:

饿汉式(静态常量)、饿汉式(静态代码块)、懒汉式(线程不安全)、懒汉式(线程安全,同步方法)、懒汉式(线程安全,同步代码块)、双重检查、静态内部类、枚举

饿汉式(静态常量)

实现步骤如下:

  • 构造器私有化(防止被new)
  • 类的内部创建对象
  • 向外部暴露一个静态的公共方法(getInstance)
java 复制代码
class Singleton {
    // 1.构造器私有化
    private Singleton() {
        // ...
    }
    // 2.在本类内部创建对象实例
    private final static Singleton instance = new Singleton();
    // 3.对外提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

**优点:**写法简单,在类装载的时候完成实例化,避免了线程同步问题

**缺点:**在类装载的时候完成实例化,没有懒加载的效果。如果未曾使用过这个类,会造成内存的浪费

饿汉式(静态代码块)
java 复制代码
class Singleton {
    // 1.构造器私有化
    private Singleton() {
        // ...
    }
    // 2.在本类内部声明静态对象属性
    private static Singleton instance;
    // 3.使用静态代码块创建对象
    static {
        instance = new Singleton();
    }
    // 4.对外提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

**优缺点:**与静态常量一样,无非就是把实例化过程放在了静态代码块

懒汉式(线程不安全)
java 复制代码
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    // 提供一个静态的公有方法,当使用到该方法的时,才去创建实例
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点:

  • 起到了懒加载的效果,但是只能在单线程下使用。
  • 如果在多线程下使用,由于静态获取方法未进行线程安全处理,可能会导致产生多个实例
  • 因此实际开发中,不建议使用
懒汉式(线程安全,同步方法)
java 复制代码
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    // 提供一个静态的公有方法,当使用到该方法的时,才去创建实例
    // 加入了synchronized的同步处理机制,解决线程安全问题
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点:

  • 解决了线程不安全问题
  • 但是由于同步处理,导致效率低,其实获取方法只要执行一次就够了
  • 因此实际开发中,也不建议使用
懒汉式(线程安全,同步代码块)
java 复制代码
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    // 提供一个静态的公有方法,当使用到该方法的时,才去创建实例
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

优缺点:

  • 这种方法本意为了改进前一种的效率问题
  • 但是反而起不到线程同步作用,依旧可能有多个线程进到if语句,会产生多个实例
  • 因此实际开发中,不能使用
双重检查
java 复制代码
class Singleton {
    // 使用轻量级volatile
    private static volatile Singleton instance;
    
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次检查,防止重复创建实例
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优缺点:

  • 双重检查有效保证了线程安全
  • 同时也避免反复进行方法同步,提高效率
  • 满足线程安全,延迟加载,效率高,实际开发中,推荐使用
静态内部类
java 复制代码
class Singleton {
    private Singleton() {}

    // 创建一个静态内部类
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

优缺点:

  • 这种方法采用了类装载的机制来保证初始化实例只有一个线程
  • 静态内部类方式在Singleton类被装载时并不会立即实例化 ,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,因为在类进行初始化时,是线程安全的(别的线程无法进入)。
  • 因此避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。实际开发中,推荐使用。
枚举
java 复制代码
enum Singleton {
    INSTACNE;
    public void method() {
        // ...
    }
}

优缺点:

  • 通过枚举来实现单例模式,不仅能避免多线程同步问题
  • 而且还能防止反序列化重新创建新的对象
  • 推荐使用
使用单例模式的注意事项
  1. 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是直接使用new
  3. 单例模式的使用场景:
    1. 需要频繁创建和销毁对象
    2. 创建对象时耗时过多或耗费资源过多(大对象)但又要经常用到的对象
    3. 工具类对象
    4. 频繁访问数据库或文件的对象(比如数据源、session工厂等)

工厂模式

简单工厂模式(静态工厂模式)
  1. 简单工厂模式是属于创建型模式,是工厂模式的一种。++简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例++。简单工厂模式是工厂模式家族中最简单实用的模式。
  2. 简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为(代码)
  3. 在软件开发中,当我们会用到大量的创建某种、某类或者某批对象时,就会用到工厂模式

实际案例:

  • 需求:实现一个计算器,输入两个数和一个符号(+/-)可以得到结果。
java 复制代码
// 接口
public interface CalApi {
    /**
    *根据传入的两个数字进行计算
    */
    int cal(int num1, int num2);
}

// 实现类1
public class AddCalApi implements CalApi {
    // 加法运算
    @Override
    public int cal(int number1, int number2) {
        return number1 + number2;
    }
}

// 实现类2
public class SubCalApi implements CalApi {
    // 减法运算
    @Override
    public int cal(int number1, int number2) {
        return number1 - number2;
    }
}

// 工厂类
public class Factory {
    public static CalApi getCalApi(String operator) {
        switch (operator) {
            case "+": {
                return new AddCalApi();
            }
            case "-": {
                return new SubCalApi();
            }
            default: return null;
        }
    }
}

// 实际客户端使用
public class Client {
    public static void main(String[] args) {
        CalApi calApi1 = Factory.getCalApi("+");
        int res1 = calApi1.cal(6, 8);
        System.out.println(res1);
        CalApi calApi2 = Factory.getCalApi("-");
        int res2 = calApi2.cal(15, 10);
        System.out.println(res2);
    }
}

这么做的优点:

  • 将实际使用的客户端与接口具体的实现类进行了解耦,即使用者不需要知道具体的产品类名,只需要知道工厂类和产品的抽象接口。
  • 将对象的创建集中到一个位置,便于管理和维护
  • 易于扩展,增加新的产品类时,只需在工厂类中新增相应的创建逻辑即可,对其他类的修改较小。

但是,简单工厂模式也由局限性:

  • 工厂类的职责相对比较重,若种类非常多,工厂方法会很复杂
  • 当增加新产品时,需要修改工厂类,这违反了开闭原则
工厂方法模式
  1. 定义了一个创建对象的抽象方法,由子类决定要实例化的类。
  2. 工厂方法模式将对象的实例化推迟到子类。

这么做的优缺点:

  • 符合开闭原则,新增产品类型只需添加新工厂类,无需修改已有代码。
  • 可扩展和更复杂的层次结构,但是随着业务增加,可能会造成类的个数成倍增加,类膨胀。
抽象工厂模式
  1. 抽象工厂模式:定义了一个interface用于创建相关或有依赖关系的对象簇,而无需指明具体的类
  2. 抽象工厂模式可以将简单工厂模式工厂方法模式 进行整合
  3. 从设计层面看,抽象工厂模式就是对简单工厂模式的改进(或称为进一步的抽象),类似将工厂分组。
  4. 将工厂抽象成两层,AbsFactory(抽象工厂)具体实现的工厂子类 。程序员可以根据创建对象类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂簇,更利于代码的维护和扩展。

原型模式

  1. 原型模式(Prototype模式)是指:用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。
  2. 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
  3. 工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即对象.clone()

Prototype:原型类,声明一个克隆自己的接口

ConcretePrototype:具体的原型类,实现一个克隆自己的操

Client:让一个原型对象克隆自己,从而创建一个新的对象(属性一样)

java 复制代码
class Sheep implements Cloneable {
   
   private String id;
    
   public String getId() {
      return id;
   }
   
   public void setId(String id) {
      this.id = id;
   }

    // 重写clone方法来实现原型模式
   public Object clone() {
      Sheep clone = null;
      try {
          // super.clone()方法会将父类的所以属性直接返回
         clone = (Sheep) super.clone();
      } catch (CloneNotSupportedException e) {
         e.printStackTrace();
      }
      return clone;
   }
}

:::color2

Spring框架的原型Bean创建就使用到了原型模式

这其中的scope="prototype"就代表创建一个bean使用的是原型模式

:::

浅拷贝和深拷贝问题
浅拷贝
  1. 对于数据类型是基本数据类型的成员变量 ,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
  2. 对于数据类型是引用数据类型的成员变量 ,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递 ,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象 。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
  3. 浅拷贝是使用默认的clone()方法来实现,例如sheep = (Sheep) super.clone()
深拷贝
  1. 复制对象的所有基本数据类型的成员变量
  2. 为所有引用数据类型的成员变量申请存储空间 ,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象进行拷贝
  3. 深拷贝实现方式1:重写clone方法来实现深拷贝
  4. 深拷贝实现方式2:通过对象序列化实现深拷贝(推荐)
java 复制代码
class MyClass implements Cloneable {
    private String field1;
    private NestedClass nestedObject;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        MyClass cloned = (MyClass) super.clone();
        // 对引用类型的属性进行单独处理
        cloned.nestedObject = (NestedClass) nestedObject.clone(); // 深拷贝内部的引用对象
        return cloned;
    }
}

class NestedClass implements Cloneable {
    private int nestedField;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
java 复制代码
import java.io.*;

class MyClass implements Serializable {
    private String field1;
    private NestedClass nestedObject;

    public MyClass deepCopy() {
        try {	
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();
            oos.close();

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (MyClass) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

class NestedClass implements Serializable {
    private int nestedField;
}
原型模式的注意事项
  1. 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率
  2. 不用重新初始化对象,而是动态地获得对象运行时的状态
  3. 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码
  4. 在实现深克隆的时候可能需要比较复杂的代码
  5. **缺点:**需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,会违背了ocp原则。

建造者模式

  1. 建造者模式又叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),是这个抽象过程的不同实现方法可以构造出不同表现(属性)对象。
  2. 建造者模式是一步一步创建一个复杂的对象 ,它允许用户只通过指定复杂对象的类型和内容 就可以构建它们,用户不需要知道内部的具体构建细节
建造者模式的四个角色
  1. Product(产品角色):一个具体的产品对象。
  2. Builder(抽象建造者):创建一个Product对象的各个部件指定的接口。
  3. ConcreteBuilder(具体建造者):实现接口,构建和装配各个部件。
  4. Director(指挥者):构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。
建造者模式的注意事项
  1. 客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  2. 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象
  3. 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程
  4. 增加新的具体建造者无须修改原有类库的代码 ,指挥者类针对抽象建造者类编程,系统扩展方便,符合"开闭原则"
  5. 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
  6. 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,因此在这种情况下,要考虑是否选择建造者模式。
抽象工厂模式vs建造者模式

抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。

:::info

建造者模式:例如生产一辆车,车具有很多零件,而Builder就好比是制作这辆车的蓝图,ConcreteBuilder就好比是根据这些蓝图制造这些零件的生产商,Director就是负责最后将这些零件组装起来的。不同的ConcreteBuilder就好比是生产不同车型零件的生产商,一个ConcreteBuilder生产一种车型的零件。而Director可以根据不同的ConcreteBuilder动态的组装不同的车,最后Client只需要调用Director来获得最终的产品。

:::

优缺点
  • 在建造者模式中,客户端不必知道产品内部组成的细节
  • 将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  • 将复杂产品的创建步骤分解在不同的方法中
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,符合开闭原则
  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似
  • 如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制

适配器模式

  1. 适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示 ,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper)
  2. 适配器模式属于结构型模式
  3. 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式
工作原理
  1. 适配器模式:将一个类的接口转换成另一种接口。让原本接口不兼容的类可以兼容
  2. 从用户的角度看不到被适配者,是解耦的
  3. 用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法


类适配器
  • 通过继承的方式进行适配。适配器类继承源类,并实现目标接口。
  • 适用于源接口和目标接口具有相似的结构,适配器类可以通过继承来复用源类的方法。
对象适配器
  • 通过组合(委托)而非继承来实现适配。适配器类持有源类的对象作为成员变量,并通过这个对象来调用源类的方法。
  • 对象适配器更加灵活,适用于Java这样的单继承语言中,它避免了继承带来的限制。
接口适配器
  • 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求
  • 适用于一个接口不想使用其所有的方法的情况。

:::info

SpringMVC中的HandlerAdapter就使用了适配器模式

:::

适配器模式总结
  1. 三种命名方式,是根据src是以怎样的形式给到Adapter(在Adapter里的形式)来命名的。
    1. 类适配器:以类给到,在Adapter里,就是将src当做类
    2. 继承对象适配器:以对象给到,在Adapter里,将src作为一个对象,持有
    3. 接口适配器:以接口给到,在Adapter里,将src作为一个接口,实现
  2. Adapter模式最大的作用还是将原本不兼容的接口融合在一起工作。
适配器模式优缺点
优点:
  1. 解耦合:通过适配器,客户端与源接口之间不再直接交互,降低了客户端与源接口的耦合度。这样即使源接口发生变化,客户端也可以通过适配器进行适配,减少修改的影响。
  2. 扩展性强:可以根据需要创建多个适配器,使得多个不同的源接口可以适配到目标接口,从而提供更多的灵活性。
  3. 复用性高:适配器可以使已有类的接口得到复用,而不需要改变源类的代码。例如,一个不兼容的库可以通过适配器进行适配,不需要修改原有库的代码。
  4. 提高灵活性:适配器模式让我们可以动态地适配不同的接口,灵活性非常高。特别是在不修改源代码的情况下,使得系统能够适应新的接口或外部库。
缺点:
  1. 增加类的数量:每一个适配器类都需要单独创建和维护,可能导致代码数量的增加,从而使系统变得复杂。
  2. 难度较大:如果源接口和目标接口差异很大,适配器的实现可能会比较复杂。
  3. 不适合多个接口差异过大的情况:如果源接口和目标接口差异很大,适配器可能需要实现复杂的转换逻辑,这样就会增加代码的复杂度。

桥接模式

  1. 桥接模式(Bridge模式)是指:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
  2. 是一种结构型设计模式
  3. Bridge模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。
  4. 它的主要特点是把抽象(Abstraction)与行为实现(Implementation)分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展

Client类:桥接模式的调用者

抽象类(Abstraction):维护了 Implementor/即它的实现类ConcretelmplementorA...二者是聚合关系,Abstraction 充当桥接类

RefinedAbstraction:是Abstraction 抽象类的子类

Implementor:行为实现类的接口

ConcretelmplementorA/B:行为的具体实现类

从UML图:这里的抽象类和接口是聚合的关系,其实调用和被调用关系

实际场景举例:

:::info

假如你正在开发一个东西,这个东西有很多种实现方案,还有很多种类型,而且每种方案下都有这些类型。如果不使用桥接模式,可能会发生如下情况:

每一种实现方案都要去写出所有的类型,这可能造成类爆炸等情况出现。

因此使用桥接模式可以将抽象部分和实现部分独立出来,这样每增加一种实现方案类,无需再把该实现方案类下的所有类型类都写出来,只需通过桥接者来得到其所有类型即可。

:::

桥接模式注意事项
  1. 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。
  2. 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成
  3. 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
  4. 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
  5. 桥接模式要求正确识别出系统中两个独立变化的维度(抽象和实现) ,因此其使用范围有一定的局限性,即需要有这样的应用场景。
桥接模式优点
  • 分离抽象部分及其具体实现部分
  • 提高了系统的可扩展性
  • 符合开闭原则
  • 符合合成复用原则

装饰者模式

  1. 装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则(ocp)。
原理
  • 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
  • 此模式为了使多个装饰器可以批次堆叠,每次向覆盖的方法添加新功能。
  • 装饰者和原始类对象共享一组公共功能,无论是装饰版本还是未装饰版本都是可以使用公共方法(图中:methodA()和methodB())方法。
  • 装饰特征(eg:方法、属性或其他成员)通常由装饰器和装饰对象共享的接口。
  • 装饰者模式是子类的替代方案,子类会在编译时添加行为,并改变原始类的所有实例;装饰可以在运行时为所选对象提供新的行为。
案例
  1. 进行咖啡售卖,咖啡可以添加各种调料
  1. 在JDK源码中IO结构,其中FilterInputStream就是一个装饰者
装饰者模式要点
  • 组合和委托可用于在运行时动态扩展我们行为
  • 装饰者模式意味着: 利用一群装饰者类来包装具体组件
  • 装饰者可以在被装饰者的行为前或后添加自己的行为,将被装饰者的行为整个取代掉达到特定目的
  • 装饰者会导致设计中出现许多小对象,如果过度使用使得程序变得复杂

组合模式

  1. 组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示"整体-部分"的层次关系。
  2. 组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
  3. 这种类型的设计模式属于结构型模式。
  4. 组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象
组合模式原理图
  • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Component</font>**是一个抽象类或接口,它定义了所有元素(叶子节点和组合节点)必须实现的共同接口。常见的接口方法包括:
    • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">add(Component component)</font>**:允许添加子组件(组合节点)。
    • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">remove(Component component)</font>**:允许移除子组件(组合节点)。
    • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">display()</font>**:显示当前节点的信息。
  • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Leaf</font>**类表示树中的叶子节点。叶子节点没有子节点,它只负责显示自己的信息,并实现<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Component</font>接口,但没有实现<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">add()</font><font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">remove()</font>方法。
  • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Composite</font>**类表示树中的组合节点,它是由多个子组件(可能是叶子节点或者其他组合节点)组成的节点。组合节点实现了<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Component</font>接口,并且提供了<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">add()</font><font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">remove()</font>方法来管理它的子节点。
  • **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Client</font>**通过统一的<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Component</font>接口来操作单一对象或组合对象。客户端不关心该对象是否为叶子节点,还是组合节点。
组合模式解决的问题

组合模式解决这样的问题,当我们的要处理的对象可以生成一颗树形结构,而我们要对树上的节点和叶子进行操作时,它能够提供一致的方式,而不用考虑它是节点还是叶子

组合模式的优缺点
  1. 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
  2. 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动.
  3. 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
  4. 需要遍历组织机构,或者处理的对象具有树形结构时,非常适合使用组合模式
  5. 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
组合模式的使用场景
  1. 树形结构数据:当你需要表示树形结构的数据时,比如文件系统、公司组织结构等。
  2. 统一接口访问:当你希望客户端通过统一接口处理单个对象和组合对象时。
  3. 递归结构:当你需要递归处理组件及其子组件时,组合模式提供了一种非常自然的方式。

外观模式

  1. 外观模式(Facade),也叫"过程模式:外观模式为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用
  2. 外观模式通过定义一个一致的接口,用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节
外观模式原理图
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Facade</font>:外观类角色,作用是为多个子系统提供一个统一接口,在这里面统一调度安排子系统。
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">SubSystem1、SubSystem2、SubSystem3</font>:子系统角色,以及内部实现的功能
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Client</font>:客户端,通过外观类对子系统集合中的功能进行访问

:::info

外观模式在MyBatis中的Configuration去创建MetaObject对象的时候使用到

:::

外观模式优缺点
  1. 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
  2. 外观模式对客户端与子系统的耦合关系,让子系统内部的模块更易维护和扩展
  3. 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
  4. 当系统需要进行分层设计时,可以考虑使用Facade模式
  5. 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口,让新系统与Facade类交互,提高复用性
  6. 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的。

享元模式

  1. 享元模式(Flyweight Pattern)也叫蝇量模式:运用共享技术有效地支持大量细粒度的对象
    • 通过<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">共享已经存在的对象</font>来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率
  2. 常用于系统底层开发,解决系统的性能问题,像数据库连接池,里面都是创建好的连接对象,在连接对象中又我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
  3. 享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
  4. 享元模式经典的应用场景就是池技术了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式
享元模式的原理

享元模式的主要角色:

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象

享元模式的两种状态:

  • 内部状态,即不会随着环境的改变而改变的可共享部分
  • 外部状态,指随环境改变而改变的不可以共享的部分

:::info
Integer类中使用了享元模式

  • 可以看到 **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Integer</font>**默认先创建并缓存 **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">-128 ~ 127</font>**之间数的 **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Integer</font>**对象
  • 当调用 **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">valueOf</font>**时如果参数在 **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">-128 ~ 127</font>**之间则计算下标并从缓存中返回,否则创建一个新的 **<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Integer</font>**对象

:::

享元模式要点
  1. 在享元模式这样理解,"享"就表示共享,"元"表示对象
  2. 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
  3. 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable存储
  4. 享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率
  5. 享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方
  6. 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
  7. 享元模式经典的应用场景是需要缓冲池的场景,比如String常量池、数据库连接池

代理模式

  1. 代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
  2. 被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象
  3. 代理模式有不同的形式,主要有三种静态代理动态代理 (JDK代理、接口代理)和Cglib代理(可以在内存动态的创建对象,而不需要实现接口,它是属于动态代理的范畴)。
静态代理
  • 静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类
  • 优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展
  • 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类一旦接口增加方法,目标对象与代理对象都要维护
动态代理
  • 代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
  • 代理对象的生成,是利用JDK的API(JDK的反射机制),动态的在内存中构建代理对象
  • 动态代理也叫做:JDK代理、接口代理
Cglib代理
  • 静态代理和JDK代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理-这就是Cglib代理
  • Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展,有些书也将Cglib代理归属到动态代理
  • Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如SpringAOP,实现方法拦截
  • 在AOP编程中如何选择代理模式:
    • 目标对象需要实现接口,用JDK代理
    • 目标对象不需要实现接口,用Cglib代理
  • Cglib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类
代理对象的变种
  • 防火墙代理
    • 内网通过代理穿透防火墙,实现对公网的访问。
  • 缓存代理
    • 比如:当请求图片文件等资源时,先到缓存代理取,如果取到资源则ok,如果取不到资源,再到公网或者数据库取,然后缓存。
  • 远程代理
    • 远程对象的本地代表,通过它可以把远程对象当本地对象来调用。远程代理通过网络和真正的远程对象沟通信息。
  • 同步代理:主要使用在多线程编程中,完成多线程间同步工作

模板模式

  1. 模板方法模式(Template Method Pattern),又叫模板模式(Template Pattern),在一个抽象类公开定义了执行它的方法的模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行
  2. 简单说,模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构,就可以重定义该算法的某些特定步骤
  3. 这种类型的设计模式属于行为型模式。
模板方法模式中的钩子方法

在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为"钩子"。

:::info

模板方法模式在Spring框架中的Spring IOC容器初始化时就运用到了模板方法模式

:::

模板模式的特点
  • 模板方式的优点:
    • 【1】封装了不变的部分,扩展可变部分。将不变部分的算法封装到父类中实现,而把可变部分的算法由各子类实现。便于子类继续扩展。
    • 【2】在父类中提取了公共的部分代码,便于代码复用。
    • 【3】部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
  • 模板方式的缺点:
    • 【1】对每个不同的实现都需要定义一个子类,这个导致类的个数增加,系统更加庞大,设计也更加抽象。
    • 【2】父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,提高了代码的阅读难度。
  • 使用场景:
    • 1**】**当多个子类具有公用的方法,却执行流程逻辑相同时。
    • 【2】重要的、复杂的方法,可以考虑作为模板方法。
  • **注意事项:**为了防止恶意操作,一般模板方法上都加有 final 关键字

命令模式

  1. 命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计
  2. 命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦。
  3. 在命名模式中,会将一个请求封装为一个对象 ,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作
  4. 通俗易懂的理解:将军发布命令,士兵去执行。其中有几个角色:将军(命令发布者)、士兵(命令的具体执行者)、命令(连接将军和士兵)。
    1. Invoker是调用者(将军),Receiver是被调用者(士兵),MyCommand是命令,实现了Command接口,持有接收对象
命令模式工作原理

核心角色:

  • **Command(命令接口)**‌:声明执行操作的接口,通常包含一个execute()方法。‌
  • ‌**ConcreteCommand(具体命令)‌:实现命令接口,它持有‌ Receiver(接收者)**‌的引用,并在execute()方法中调用接收者的具体操作方法来完成请求。‌
  • ‌**Receiver(接收者)**‌:知道如何实施与执行请求相关的实际操作,是命令的真正执行者。‌
  • ‌**Invoker(调用者/请求者)**‌:持有命令对象,负责触发命令执行(调用execute()),但不关心命令如何被执行。‌
  • ‌**Client(客户端)**‌:创建具体的命令对象,并设置其接收者,然后将命令对象传递给调用者。‌

在Spring框架中的JdbcTemplate中使用到了命令模式

命令模式的要点
  1. 将发起请求的对象与执行请求的对象解耦。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁、是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:"请求发起者"和"请求执行者"之间的解耦是通过命令对象实现的,命令对象起到了纽带桥梁的作用
  2. 容易设计一个命令队列。只要把命令对象放到列队,就可以多线程的执行命令
  3. 容易实现对请求的撤销和重做
  4. 命令模式不足:可能导致某些系统有过多的具体命令类,增加了系统的复杂度,这点在在使用的时候要注意
  5. 空命令也是一种设计模式,它为我们省去了判空的操作。在上面的实例中,如果没有用空命令,我们每按下一个按键都要判空,这给我们编码带来一定的麻烦。
  6. 命令模式经典的应用场景:界面的一个按钮都是一条命令、模拟CMD(DOS命令)订单的撤销/恢复、触发-反馈机制
命令模式的应用场景
  • 需要解耦请求发送者与执行者‌:例如,图形界面中的菜单项、按钮(发送者)与背后的业务逻辑(执行者)分离。‌‌
  • 需要支持撤销、重做或事务操作‌:如文本编辑器的撤销功能、交易系统的事务回滚。‌‌
  • 需要将请求排队、记录日志或延迟执行‌:例如任务调度系统、操作审计。‌‌
  • 需要参数化对象并动态指定其行为‌:例如遥控器可以配置不同的按钮对应不同的设备操作。‌‌

访问者模式

  1. 访问者模式(Visitor Pattern),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
  2. 主要将数据结构与数据操作分离,解决数据结构操作耦合性问题
  3. 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
  4. 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决
访问者模式的原理

主要角色:

  • **Visitor(访问者接口):**定义了对每种元素对象的访问操作,通常会定义一组访问方法,每个方法对应一种元素类(即将要访问的类)。
  • ConcreteVisitor(具体访问者): 实现了访问者接口,定义了具体的访问操作。每个 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">ConcreteVisitor</font> 都可以为一组元素类型提供不同的操作。
  • Element(元素接口): 定义了接受访问者的接口,通常包含一个 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">accept()</font> 方法,这个方法接收一个 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Visitor</font> 对象,并调用访问者中的相应方法。
  • ConcreteElement(具体元素): 实现了 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Element</font> 接口,定义了接受访问者的行为,即实现 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">accept()</font> 方法来接受访问者的访问。
  • ObjectStructure(对象结构): 容纳多个 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Element</font> 对象,并可以遍历这些对象,通常会定义一个 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">accept()</font> 方法,遍历每一个元素并传入访问者。
访问者模式优缺点

优点:

  • 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
  • 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统

缺点:

  • 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的,这样造成了具体元素变更比较困难
  • 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
  • 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的

迭代器模式

  1. 迭代器模式(Iterator Pattern)是常用的设计模式,属于行为型模式
  2. 如果我们的集合元素是用不同的方式实现的,有数组,还有java的集合类,或者还有其他方式,当客户端要遍历这些集合元素的时候就要使用多种遍历方式,而且还会暴露元素的内部结构,可以考虑使用迭代器模式解决。
  3. 迭代器模式,提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即:不暴露其内部的结构
迭代器模式原理
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Aggregate</font>:抽象聚合接口,定义对聚合对象的一些操作和创建迭代器对象的接口
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Iterator</font>:抽象迭代器接口,定义访问和遍历聚合元素的接口
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Aggregate1</font>:具体聚合实现,实现抽象聚合接口,返回一个具体迭代器实例对象
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Iterator1</font>:具体迭代器实现,实现抽象迭代器接口中所定义的方法
迭代器模式优缺点

优点:

  • 提供一个统一的方法遍历对象 ,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象了。
  • 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的具体组成。
  • 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(叫做单一责任原则)。在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。而如果遍历方式改变的话,只影响到了迭代器。
  • 当要展示一组相似对象,或者遍历一组相同对象时使用,适合使用迭代器模式

缺点:

  • 每个聚合对象都要一个迭代器,会生成多个迭代器不好管理类

观察者模式

  1. 观察者模式是一种行为型设计模式,又被称为"发布-订阅"模式,它定义了对象之间的一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会收到通知并自动更新。
  2. 观察者模式的关注点是对象之间的通信以及被观察对象的状态。

核心成员:

  • 被观察者(Subject):被观察的对象,它的内部包含了观察者对象的集合,并提供了添加、通知和删除观察者对象的统一接口。
  • 观察者(Observer):接收Subject通知的对象,它订阅了Subject的状态,并提供了更新操作的统一接口。
  • 具体的被观察者(ConcreteSubject):包含Subject类接口的具体实现,维护了观察者的列表,自身状态发生变化时通知所有的观察者。
  • 具体的观察者(ConcreteObserver):包含Observer类接口的具体实现,提供了更新操作的具体实现细节,一旦收到Subject的通知便进行更新操作。

观察者模式在JDK中的Observable类中有使用到

观察者模式工作流程
  1. 被观察者维护一个观察者的列表 ,并提供了管理和通知观察者的方法。
  2. 观察者与被观察者绑定(attach),并将自己添加到观察者列表中。
  3. 当被观察者的状态发生变化时,开始通知观察者,通知的方式一般是遍历观察者列表 ,遍历时会调用每个观察者的更新方法
  4. 观察者完成具体的更新操作。
观察者模式优缺点

观察者模式的优点:

  • 符合"开闭原则"的要求。
  • 支持广播的通信方式。
  • 支持事件驱动编程。
  • 可以动态添加观察者,代码扩展性好。

观察者模式的缺点:

  • 每次状态变化都要遍历所有观察者,性能开销大。
  • 每次状态变化都要通知所有的观察者,通信时间变长。
  • 观察者数量过多会使代码的可读性变差。
  • 当有多个客户端操作观察者的删除时,会带来数据安全问题。

中介者模式

  1. 中介者模式(MediatorPattern),用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
  2. 中介者模式属于行为型模式,使代码易于维护
  3. 比如MVC模式,C(Controller控制器)是M(Model模型)和V(View视图)的中介者,在前后端交互时起到了中间人的作用
工作原理

主要包含以下角色:

  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Mediator</font>:抽象中介者,是中介者的接口/抽象类
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">ConcreteMeditor</font>:中介者的具体实现,实现中介者接口,定义一个List来管理<font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Colleague</font>对象
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Colleague</font>:抽象同事类,定义同事类的接口/抽象类,保存中介者对象,实现同事类的公共方法
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">ConcreteColleague1、ConcreteColleague2</font>:具体同事类,实现抽象同事类。通过中介者间接完成具体同事类之间的通信交互
要点
  1. 多个类相互耦合,会形成网状结构,使用中介者模式将网状结构分离为星型结构,进行解藕
  2. 减少类间依赖,降低了耦合,符合迪米特原则
  3. 中介者承担了较多的责任,一旦中介者出现了问题,整个系统就会受到影响如果设计不当,中介者对象本身变得过于复杂,这点在实际使用时,要特别注意
适用范围
  1. 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解
  2. 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象
  3. 需要通过一个中间类来封装多个类中的行为,但又不想生成太多的子类

备忘录模式

  1. 备忘录模式(Memento Pattern)在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态 。这样以后就可将该对象恢复到原先保存的状态
  2. 可以这里理解备忘录模式:现实生活中的备忘录是用来记录某些要去做的事情,或者是记录已经达成的共同意见的事情,以防忘记了。而在软件层面,备忘录模式有着相同的含义,备忘录对象主要用来记录一个对象的某种状态,或者某些数据,当要做回退时,可以从备忘录对象里获取原来的数据进行恢复操作
  3. 备忘录模式属于行为型模式
原理

主要角色

  • 发起人(Originator):负责创建一个备忘录对象,用于保存自身状态,并可以使用备忘录对象来恢复自身状态。
  • 备忘录(Memento):用于存储发起人对象的内部状态,可以包含多个状态属性。
  • 管理者(Caretaker):负责保存备忘录对象,但不能对备忘录对象进行修改或检查。使用集合管理,提高效率

实现步骤

  • 在发起人类中,定义一个内部类作为备忘录类,该类用于保存发起人对象的状态。
  • 在发起人类中,提供创建备忘录对象、恢复状态的方法。
  • 在管理者类中,保存备忘录对象,并提供对外的保存和获取备忘录对象的方法。
  • 在客户端中,通过发起人类和管理者类来实现对对象状态的保存和恢复操作。
要点
  1. 给用户提供了一种可以恢复状态的机制 ,可以使用户能够比较方便地回到某个历史的状态
  2. 实现了信息的封装,使得用户不需要关心状态的保存细节如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存,这个需要注意
  3. 适用的应用场景:1、后悔药。2、打游戏时的存档。3、Windows里的ctri+z。4、IE中的后退。4、数据库的事务管理
  4. 为了节约内存,备忘录模式可以和原型模式配合使用

解释器模式

  1. 在编译原理中,一个算术表达式通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器 构建语法分析树,最终形成一颗抽象的语法分析树。这里的词法分析器和语法分析器都可以看做是解释器
  2. 解释器模式(Interpreter Pattern):是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)
  3. 应用场景:
    1. 应用可以将一个需要解释执行的语言中的句子表示为一个抽象语法树些
    2. 重复出现的问题可以用一种简单的语言来表达
    3. 一个简单语法需要解释的场景
  4. 这样的例子还有,比如编译器、运算表达式计算、正则表达式、机器人等
原理

主要角色:

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

工作流程:

  1. 定义语法规则:首先,需要定义语言或表达式的文法规则,并将每个规则(或语法)表示为类。这些规则通常是递归的,定义了基本语法和复杂语法的关系。
  2. 构建抽象语法树:通过客户端创建一棵抽象语法树(Abstract Syntax Tree, AST),树的每个节点代表一个表达式或者操作符。叶子节点(终结符)通常是字面量,非叶子节点(非终结符)是更复杂的表达式。
  3. 解释表达式 :调用 <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">interpret()</font> 方法,解释器将根据上下文解析表达式。每个表达式(无论是终结符还是非终结符)都会递归地调用其子表达式,直到最终得到结果。
要点
  1. 当有一个语言需要解释执行,可将该语言中的句子表示为一个抽象语法树,就可以考虑使用解释器模式,让程序具有良好的扩展性
  2. 应用场景:编译器、运算表达式计算、正则表达式、机器人等
  3. 使用解释器可能带来的问题:解释器模式会引起类膨胀、解释器模式采用递归调用方法,将会导致调试非常复杂、效率可能降低.

状态模式

  1. 状态模式(State Pattern):它主要用来解决对象在多种状态转换时,需要对外输出不同的为的问题。状态和行为是一 一对应的,状态之间可以相互转换
  2. 当一个对象的内在状态改变时,允许改变其行为,这个对象看起来像是改变了其类
工作原理
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">State</font>:抽象状态类,提供一个方法封装上下文对象的状态
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">ConcreteState1、ConcreteState2</font>:具体状态类,继承抽象状态类,实现状态下的行为
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Context</font>:上下文类,负责对具体状态进行切换
  • <font style="color:rgb(10, 191, 91);background-color:rgb(243, 245, 249);">Client</font>:客户端,调用具体状态和上下文
状态转换机制

状态转换通常有两种实现方式:

  • 由环境类统一负责‌:环境类在业务方法中根据属性值判断并切换状态。
  • 由具体状态类负责‌:具体状态类在处理行为时,根据条件为环境类设置新的状态对象。
应用场景

状态模式的应用比较广泛,比如游戏中角色状态的转换、公文审批中的流转等等。

以下情况可以考虑使用状态模式:

  • 对象的行为依赖于它的某些属性值(状态),而且状态的改变将导致行为的变化
  • 代码中包含大量与对象状态有关的条件语句(if-else),这些条件语句的出现会导致代码的可维护性和灵活性变差。
状态模式要点
  1. 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中
  2. 方便维护。将容易产生问题的if-else语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产出很多if-else语句,而且容易出错
  3. 符合"开闭原则"。容易增删状态
  4. 会产生很多类。每个状态都要一个对应的类,当状态过多时会产生很多类。
  5. 当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式

策略模式

  1. 策略模式(Strategy Pattern)中,定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户
  2. 这算法体现了几个设计原则,第一、把变化的代码从不变的代码中分离出来;第二、针对接口编程而不是具体类(定义了策略接口);第三、多用组合/聚合,少用继承(客户通过组合方式使用策略)
原理

核心成员:

  • 环境类(Context):环境类内部持有一个具体策略类的实例,这个实例就是当前的策略,可以供客户端使用
  • 抽象策略类(Strategy):抽象策略类声明具体策略类需要实现的接口,这个接口同时也是提供给客户端调用的接口
  • 具体策略类(Concrete Strategy):具体策略类实现抽象策略类声明的接口,每个具体策略类都有自己独有的实现方式,即代表不同策略

在JDK中的Arrays工具类中的Comparator中就使用到了策略模式

策略模式要点
  1. 策略模式的关键是:分析项目中变化部分与不变部分
  2. 策略模式的核心思想是:多用组合/聚合少用继承;用行为类组合,而不是行为的继承。更有弹性
  3. 体现了"对修改关闭,对扩展开放"原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if...elseif...else)
  4. 提供了可以替换继承关系的办法:策略模式将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展
  5. 需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大
策略模式优缺点

优点

  • 策略模式遵循开闭原则,用户可以在不修改原有系统的前提下选择和更换算法
  • 避免使用多重条件判断
  • 可以灵活地增加新的算法或行为
  • 提高算法和策略的安全性:可以封装策略的具体实现,调用者只需要知道不同策略之间的区别就可以

缺点

  • 客户端必须知道当前所有的具体策略类,而且需要自行决定使用哪一个策略类
  • 如果可选的方案过多,会导致策略类数量激增。

责任链模式

  1. 职责链模式(Chain of Responsibility Pattern)又叫责任链模式,为请求创建了一个接收者对象的链(简单示意图)。这种模式对请求的发送者和接收者进行解耦。
  2. 职责链模式通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
  3. 这种类型的设计模式属于行为型模式
  4. 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止.
原理

核心成员:

  1. **抽象处理者(Handler)**‌:定义处理请求的接口,并持有对下一个处理者的引用(即设置后继者的方法)。‌‌
  2. ‌**具体处理者(Concrete Handler)**‌:实现抽象处理者的接口,包含具体的处理逻辑。它判断自己能否处理当前请求,如果能则处理,否则将请求传递给链中的下一个处理者。‌
  3. ‌**客户端(Client)**‌:负责创建并组装处理链,然后将请求发送给链的第一个处理者。‌‌
责任链模式要点
  1. 将请求和处理分开,实现解耦,提高系统的灵活性
  2. 简化了对象,使对象不需要知道链的结构
  3. 性能会受到影响,特别是在链比较长的时候,因此需控制链中最大节点数量,一般通过在Handler中设置一个最大节点数量,在setNext()方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地破坏系统性能
  4. 调试不方便。采用了类似递归的方式,调试时逻辑可能比较复杂
  5. 最佳应用场景:有多个对象可以处理同一个请求时,比如:多级请求、请假/加薪等审批流程、Java Web中Tomcat对Encoding的处理、拦截器
相关推荐
良逍Ai出海1 小时前
OpenClaw 新手最该先搞懂的 2 套命令
android·java·数据库
6+h1 小时前
【Spring】深度剖析IoC
java·后端·spring
程序员JerrySUN2 小时前
别再把 HTTPS 和 OTA 看成两回事:一篇讲透 HTTPS 协议、安全通信机制与 Mender 升级加密链路的完整文章
android·java·开发语言·深度学习·流程图
j_xxx404_2 小时前
C++算法:一维/二维前缀和算法模板题
开发语言·数据结构·c++·算法
郝学胜-神的一滴2 小时前
系统设计与面向对象设计:两大设计思想的深度剖析
java·前端·c++·ue5·软件工程
蓝天智能2 小时前
QT实战:Qt6 字符编码避坑指南
开发语言·qt
xier_ran2 小时前
【第一周】关键词解释:倒数排名融合(Reciprocal Rank Fusion, RRF)算法
开发语言·python·算法
HelloWorld__来都来了2 小时前
如何用python爬取上市公司信息
开发语言·python
myloveasuka2 小时前
[Java]子类到底能继承父类中的哪些东西?继承中成员变量/方法访问特点---就近原则
java·开发语言