【有趣的设计模式】23 种设计模式详解和场景分析

前言

七大设计原则

1、单一原则:一个类只负责一个职责

2、开闭原则:对修改关闭,对扩展开放

3、里氏替换原则:不要破坏继承关系

4、接口隔离原则:暴露最小接口,避免接口过于臃肿

5、依赖倒置原则:面向抽象编程

6、迪米特 法则:尽量不跟陌生人讲话

7、合成复用原则:多使用组合、聚合、少用继承

一、创建型模式

1. 单例模式(Singleton Pattern)⭐

单例模式(Singleton Pattern)是 Java中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。


1.1 饿汉式

特点: 类加载时就初始化,线程安全

java 复制代码
// 构造方法私有化
private Singleton() {

}

// 饿汉式创建单例对象
private static Singleton singleton = new Singleton();

public static Singleton getInstance() {
	return singleton;
}

1.2 懒汉式

特点: 第一次调用才初始化,避免内存浪费。

java 复制代码
/*
 * 懒汉式创建单例模式 由于懒汉式是非线程安全, 所以加上线程锁保证线程安全
 */
private static Singleton singleton;

public static synchronized Singleton getInstance() {
	if (singleton == null) {
		singleton = new Singleton();
	}
	return singleton;
}

1.3 双重检验锁(double check lock)(DCL)

特点: 安全且在多线程情况下能保持高性能

java 复制代码
private volatile static Singleton singleton;
// 构造方法私有化
private Singleton (){}

public static Singleton getInstance() {
    if (singleton == null) {
        synchronized (Singleton.class) {
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

1.4 静态内部类

特点:效果类似DCL, 只适用于静态域

java 复制代码
private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
}
// 构造方法私有化
private Singleton (){}

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

1.5 枚举

特点:自动支持序列化机制,绝对防止多次实例化

java 复制代码
public enum Singleton {
    INSTANCE;
}

1.6 乐观锁机制CAS

cas是一项乐观锁技术,当多个线程尝试使用cas同时更新一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知竞争失败,并可以再次尝试。

优点:本质是基于忙等待算法,依赖底层硬件的实现。没有线程切换和阻塞的额外消耗。

缺点:一直执行不成功,对cpu造成较大的开销。

java 复制代码
private static final AtomicReference<CasSingleton> instance = new AtomicReference<>();

private CasSingleton(){}

public static CasSingleton getInstance() {
    while (instance.get() == null) {
        instance.compareAndSet(null,new CasSingleton());
    }
    return instance.get();
}

1.7 容器式单例

当程序中的单例对象非常多的时候,则可以使用容器对所有单例对象进行管理,如下:

java 复制代码
public class ContainerSingleton {
    private ContainerSingleton() {}
    private static Map<String, Object> singletonMap = new ConcurrentHashMap<>();
    public static Object getInstance(Class clazz) throws Exception {
        String className = clazz.getName();
        // 当容器中不存在目标对象时则先生成对象再返回该对象
        if (!singletonMap.containsKey(className)) {
            Object instance = Class.forName(className).newInstance();
            singletonMap.put(className, instance);
            return instance;
        }
        // 否则就直接返回容器里的对象
        return singletonMap.get(className);
    }
    public static void main(String[] args) throws Exception {
        SafetyDangerLibrary instance1 = (SafetyDangerLibrary)ContainerSingleton.getInstance(SafetyDangerLibrary.class);
        SafetyDangerLibrary instance2 = (SafetyDangerLibrary)ContainerSingleton.getInstance(SafetyDangerLibrary.class);
        System.out.println(instance1 == instance2); // true
    }
}

1.8 ThreadLocal单例

不保证整个应用全局唯一,但保证线程内部全局唯一,以空间换时间,且线程安全。

java 复制代码
public class ThreadLocalSingleton {

    private ThreadLocalSingleton(){}
    
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = ThreadLocal.withInitial(() -> new ThreadLocalSingleton());
    
    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
    
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "-----" + ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() + "-----" + ThreadLocalSingleton.getInstance());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "-----" + ThreadLocalSingleton.getInstance());
            System.out.println(Thread.currentThread().getName() + "-----" + ThreadLocalSingleton.getInstance());
        }).start();
//        Thread-0-----com.ruoyi.library.domain.vo.ThreadLocalSingleton@53ac93b3
//        Thread-1-----com.ruoyi.library.domain.vo.ThreadLocalSingleton@7fe11afc
//        Thread-0-----com.ruoyi.library.domain.vo.ThreadLocalSingleton@53ac93b3
//        Thread-1-----com.ruoyi.library.domain.vo.ThreadLocalSingleton@7fe11afc
    }
}

可以看到上面线程0和1他们的对象是不一样的,但是线程内部,他们的对象是一样的,这就是线程内部保证唯一。

ThreadLocal将多有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

1.9 破坏单例的几种方式与解决方法

1.9.1 反序列化

java 复制代码
Singleton singleton = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/test.txt"));
oos.writeObject(singleton);
oos.flush();
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/test.txt"));
Singleton singleton1 = (Singleton)ois.readObject();
ois.close();
System.out.println(singleton);//com.ruoyi.base.mapper.Singleton@50134894
System.out.println(singleton1);//com.ruoyi.base.mapper.Singleton@5ccd43c2

可以看到反序列化后,两个对象的地址不一样了,那么这就是违背了单例模式的原则了,解决方法只需要在单例类里加上一个readResolve()方法即可,原因就是在反序列化的过程中,会检测readResolve()方法是否存在,如果存在的话就会反射调用readResolve()这个方法

java 复制代码
private Object readResolve() {
    return singleton;
}
//com.ruoyi.base.mapper.Singleton@50134894
//com.ruoyi.base.mapper.Singleton@50134894

1.9.2 反射

java 复制代码
Singleton singleton = Singleton.getInstance();
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1 = constructor.newInstance();
System.out.println(singleton);//com.ruoyi.base.mapper.Singleton@32a1bec0
System.out.println(singleton1);//com.ruoyi.base.mapper.Singleton@22927a81

同样可以看到,两个对象的地址不一样,这同样是违背了单例模式的原则,解决办法为使用一个布尔类型的标记变量标记一下即可,代码如下:

饿汉式

java 复制代码
// 饿汉式创建单例对象
private static Singleton singleton = new Singleton();

//第一要素必须私有构造函数,才不能被随意New
private Singleton(){
     if(instance != null) {
		throw new IllegalArgumentException("单例已经创建");
     }
}

public static Singleton getInstance() {
	return singleton;
}

懒汉式同样方法无解

但是这种方法假如使用了反编译,获得了这个变量,同样可以破坏单例,代码如下:

java 复制代码
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton = constructor.newInstance();
System.out.println(singleton); // com.ruoyi.base.mapper.Singleton@32a1bec0

Field singletonFlag = singletonClass.getDeclaredField("singleton");
singletonFlag.setAccessible(true);
singletonFlag.set(singleton, null);
Singleton singleton1 = constructor.newInstance();
System.out.println(singleton1); // com.ruoyi.base.mapper.Singleton@5e8c92f4

如果想使单例不被破坏,那么应该使用枚举的方式去实现单例模式,枚举是不可以被反射破坏单例的。

1.10 总结

适用场景:

  • 需要确保在任何情况下绝对只需要一个实例。如:ServletContext,ServletConfig,ApplicationContext,DBPool,ThreadPool等。

优点:

  • 在内存中只有一个实例,减少了内存开销。
  • 可以避免对资源的多重占用。
  • 设置全局访问点,严格控制访问。

缺点:

  • 没有接口,扩展困难。
  • 如果要扩展单例对象,只有修改代码,没有其它途径。

2.工厂方法模式(Factory Method)⭐

2.1 简单工厂模式

简单工厂模式不是23种设计模式之一,他可以理解为工厂模式的一种简单的特殊实现。

2.1.1 基础版

java 复制代码
// 工厂类
public class CoffeeFactory {
   public Coffee create(String type) {
       if ("americano".equals(type)) {
           return new Americano();
       }
       if ("mocha".equals(type)) {
           return new Mocha();
       }
       if ("cappuccino".equals(type)) {
           return new Cappuccino();
       }
       return null;
   }
}



// 产品基类
public interface Coffee {
}

// 产品具体类,实现产品基类接口
public class Cappuccino implements Coffee {
}

基础版是最基本的简单工厂的写法,传一个参数过来,判断是什么类型的产品,就返回对应的产品类型。但是这里有一个问题,就是参数是字符串的形式,这就很容易会写错,比如少写一个字母,或者小写写成了大写,就会无法得到自己想要的产品类了,同时如果新加了产品,还得在工厂类的创建方法中继续加if,于是就有了升级版的写法。

2.1.2 升级版

java 复制代码
// 使用反射创建对象
// 加一个static变为静态工厂
public static Coffee create(Class<? extends Coffee> clazz) throws Exception {
    if (clazz != null) {
        return clazz.newInstance();
    }
    return null;
}

升级版就很好的解决基础版的问题,在创建的时候在传参的时候不仅会有代码提示,保证不会写错,同时在新增产品的时候只需要新增产品类即可,也不需要再在工厂类的方法里面新增代码了。

当然也可以使用枚举的方式

2.1.3 总结

适用场景:

  • 工厂类负责创建的对象较少。
  • 客户端只需要传入工厂类的参数,对于如何创建的对象的逻辑不需要关心。

优点:

  • 只需要传入一个正确的参数,就可以获取你所需要的对象,无须知道创建的细节。

缺点:

  • 工厂类的职责相对过重,增加新的产品类型的时需要修改工厂类的判断逻辑,违背了开闭原则。
  • 不易于扩展过于复杂的产品结构。

2.2 工厂方法模式

工厂方法模式是指定义一个创建对象的接口,让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到子类中进行。

工厂方法模式主要有以下几种角色:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它和具体工厂之间一一对应。

2.2.1 代码实现

java 复制代码
// 抽象工厂
public interface CoffeeFactory {
    Coffee create();
}
// 具体工厂
public class CappuccinoFactory implements CoffeeFactory {
    @Override
    public Coffee create() {
        return new Cappuccino();
    }
}
// 抽象产品
public interface Coffee {
}
// 具体产品
public class Cappuccino implements Coffee {
}

2.2.2 总结

适用场景:

  • 创建对象需要大量的重复代码。
  • 客户端(应用层)不依赖于产品类实例如何被创建和实现等细节。
  • 一个类通过其子类来指定创建哪个对象。

优点:

  • 用户只需要关系所需产品对应的工厂,无须关心创建细节。
  • 加入新产品符合开闭原则,提高了系统的可扩展性。

缺点:

  • 类的数量容易过多,增加了代码结构的复杂度。
  • 增加了系统的抽象性和理解难度。

3.抽象工厂模式(Abstract Factory)⭐

抽象工厂模式是指提供一个创建一系列相关或相互依赖对象的接口,无须指定他们具体的类。

工厂方法模式中考虑的是一类产品的生产,如电脑厂只生产电脑,电话厂只生产电话,这种工厂只生产同种类的产品,同种类产品称为同等级产品,也就是说,工厂方法模式只考虑生产同等级的产品,但是现实生活中许多工厂都是综合型工厂,能生产多等级(种类)的产品,如上面说的电脑和电话,本质上他们都属于电器,那么他们就能在电器厂里生产出来,而抽象工厂模式就将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,如上图所示纵轴是产品等级,也就是同一类产品;横轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。

3.1 代码实现

java 复制代码
// 咖啡店 抽象工厂
public interface CoffeeShopFactory {

   // 咖啡类
   Coffee createCoffee();

   // 甜点类
   Dessert createDessert();
}
// 美式风格工厂
public class AmericanFactory implements CoffeeShopFactory {
   @Override
   public Coffee createCoffee() {
       return new Americano();
   }

   @Override
   public Dessert createDessert() {
       return new Cheesecake();
   }
}
// 意式风格工厂
public class ItalyFactory implements CoffeeShopFactory {
   @Override
   public Coffee createCoffee() {
       return new Cappuccino();
   }

   @Override
   public Dessert createDessert() {
       return new Tiramisu();
   }
}

UML图

3.2 总结

产品族:一系列相关的产品,整合到一起有关联性

产品等级:同一个继承体系

适用场景:

  • 客户端(应用层)不依赖于产品类实例如何被创建和实现等细节。
  • 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量重复的代码。
  • 提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

优点:

  • 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点:

  • 当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

4.原型模式(Prototype)

原型模式是指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。调用者不需要知道任何创建细节,不调用构造函数。

原型模式包含如下角色:

  • 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Cloneable {
    private String name;
    private String sex;
    private Integer age;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws Exception{
        Student stu1 = new Student("张三", "男", 18);
        Student stu2 = (Student)stu1.clone();
        stu2.setName("李四");
        System.out.println(stu1);// Student(name=张三, sex=男, age=18)
        System.out.println(stu2);// Student(name=李四, sex=男, age=18)
    }
}

可以看到,把一个学生复制过来,只是改了姓名而已,其他属性完全一样没有改变,需要注意的是,一定要在被拷贝的对象上实现Cloneable接口,否则会抛出CloneNotSupportedException异常。

4.1 浅克隆

创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

java 复制代码
@Data
public class Clazz implements Cloneable {
    private String name;
    private Student student;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
    private String name;
    private String sex;
    private Integer age;
}

public static void main(String[] args) throws Exception{
    Clazz clazz1 = new Clazz();
    clazz1.setName("高三一班");
    Student stu1 = new Student("张三", "男", 18);
    clazz1.setStudent(stu1);
    System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=张三, sex=男, age=18))
    Clazz clazz2 = (Clazz)clazz1.clone();
    Student stu2 = clazz2.getStudent();
    stu2.setName("李四");
    System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=李四, sex=男, age=18))
    System.out.println(clazz2); // Clazz(name=高三一班, student=Student(name=李四, sex=男, age=18))
}

可以看到,当修改了stu2的姓名时,stu1的姓名同样也被修改了,这说明stu1和stu2是同一个对象,这就是浅克隆的特点,对具体原型类中的引用类型的属性进行引用的复制。同时,这也可能是浅克隆所带来的弊端,因为结合该例子的原意,显然是想在班级中新增一名叫李四的学生,而非让所有的学生都改名叫李四,于是我们这里就要使用深克隆。

4.2 深克隆

创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

java 复制代码
@Data
public class Clazz implements Cloneable, Serializable {
    private String name;
    private Student student;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    protected Object deepClone() throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(this);
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        return ois.readObject();
    }
}

public static void main(String[] args) throws Exception{
    Clazz clazz1 = new Clazz();
    clazz1.setName("高三一班");
    Student stu1 = new Student("张三", "男", 18);
    clazz1.setStudent(stu1);
    Clazz clazz3 = (Clazz)clazz1.deepClone();
    Student stu3 = clazz3.getStudent();
    stu3.setName("王五");
    System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=张三, sex=男, age=18))
    System.out.println(clazz3); // Clazz(name=高三一班, student=Student(name=王五, sex=男, age=18))
}

可以看到,当修改了stu3的姓名时,stu1的姓名并没有被修改了,这说明stu3和stu1已经是不同的对象了,说明Clazz中的Student也被克隆了,不再指向原有对象地址,这就是深克隆。这里需要注意的是,Clazz类和Student类都需要实现Serializable接口,否则会抛出NotSerializableException异常。

4.3 克隆破坏单例与解决办法

PS:上面例子有的代码,这里便不重复写了,可以在上面的代码基础上添加以下代码

java 复制代码
// Clazz类
private static Clazz clazz = new Clazz();
private Clazz(){}
public static Clazz getInstance() {return clazz;}

// 测试
public static void main(String[] args) throws Exception{
    Clazz clazz1 = Clazz.getInstance();
    Clazz clazz2 = (Clazz)clazz1.clone();
    System.out.println(clazz1 == clazz2); // false
}

可以看到clazz1和clazz2并不相等,也就是说他们并不是同一个对象,也就是单例被破坏了。

解决办法也很简单,首先第一个就是不实现Cloneable接口即可,但是不实现Cloneable接口进行clone则会抛出CloneNotSupportedException异常。第二个方法就是重写clone()方法即可,如下:

java 复制代码
@Override
protected Object clone() throws CloneNotSupportedException {
    return clazz;
}
// 测试输出
System.out.println(clazz1 == clazz2) // true

可以看到,上面clazz1和clazz2是相等的,即单例没有被破坏。

另外我们知道,单例就是只有一个实例对象,如果重写了clone()方法保证单例的话,那么通过克隆出来的对象则不可以重新修改里面的属性,因为修改以后就会连同克隆对象一起被修改,所以是需要单例还是克隆,在实际应用中需要好好衡量。

4.4 总结

适用场景:

  • 类初始化消耗资源较多。
  • new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。
  • 构造函数比较复杂。
  • 循环体中生产大量对象时。

优点:

  • 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建的过程。

缺点:

  • 必须配备克隆(或者可拷贝)方法。
  • 当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
  • 深克隆、浅克隆需要运用得当。

5.建造者模式(Builder)⭐

建造者模式是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。用户只需指定需要建造的类型就可以获得对象,建造过程及细节不需要了解。

建造者(Builder)模式包含如下角色:

  • 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
  • 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
  • 产品类(Product):要创建的复杂对象。
  • 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。

5.1 常规写法

java 复制代码
//产品类 电脑
@Data
public class Computer {
   private String motherboard;
   private String cpu;
   private String memory;
   private String disk;
   private String gpu;
   private String power;
   private String heatSink;
   private String chassis;
}
// 抽象 builder类(接口) 组装电脑
public interface ComputerBuilder { 
   Computer computer = new Computer();
   void buildMotherboard();
   void buildCpu();
   void buildMemory();
   void buildDisk();
   void buildGpu();
   void buildHeatSink();
   void buildPower();
   void buildChassis();
   Computer build();
}
// 具体 builder类 华硕ROG全家桶电脑(手动狗头)
public class AsusComputerBuilder implements ComputerBuilder {
   @Override 
   public void buildMotherboard() {
       computer.setMotherboard("Extreme主板");
   }
   @Override
   public void buildCpu() {
       computer.setCpu("Inter 12900KS");
   }
   @Override
   public void buildMemory() {
       computer.setMemory("芝奇幻峰戟 16G*2");
   }
   @Override
   public void buildDisk() {
       computer.setDisk("三星980Pro 2T");
   }
   @Override
   public void buildGpu() {
       computer.setGpu("华硕3090Ti 水猛禽");
   }
   @Override
   public void buildHeatSink() {
       computer.setHeatSink("龙神二代一体式水冷");
   }
   @Override
   public void buildPower() {
       computer.setPower("雷神二代1200W");
   }
   @Override
   public void buildChassis() {
       computer.setChassis("太阳神机箱");
   }
   @Override
   public Computer build() {
       return computer;
   }
}

// 指挥者类 指挥该组装什么电脑
@AllArgsConstructor
public class ComputerDirector {
   private ComputerBuilder computerBuilder;
   public Computer construct() {
       computerBuilder.buildMotherboard();
       computerBuilder.buildCpu();
       computerBuilder.buildMemory();
       computerBuilder.buildDisk();
       computerBuilder.buildGpu();
       computerBuilder.buildHeatSink();
       computerBuilder.buildPower();
       computerBuilder.buildChassis();
       return computerBuilder.build();
   }
}

// 测试
 public static void main(String[] args) {
     ComputerDirector computerDirector = new ComputerDirector(new AsusComputerBuilder());
     // Computer(motherboard=Extreme主板, cpu=Inter 12900KS, memory=芝奇幻峰戟 16G*2, disk=三星980Pro 2T, gpu=华硕3090Ti 水猛禽, power=雷神二代1200W, heatSink=龙神二代一体式水冷, chassis=太阳神机箱)
     System.out.println(computerDirector.construct());
 }

上面示例是建造者模式的常规用法,指挥者类ComputerDirector在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合,于是就有了下面的简化写法。

5.2 简化写法

java 复制代码
// 把指挥者类和抽象建造者合在一起的简化建造者类
public class SimpleComputerBuilder {
    private Computer computer = new Computer();
    public void buildMotherBoard(String motherBoard){
        computer.setMotherboard(motherBoard);
    }
    public void buildCpu(String cpu){
        computer.setCpu(cpu);
    }
    public void buildMemory(String memory){
        computer.setMemory(memory);
    }
    public void buildDisk(String disk){
        computer.setDisk(disk);
    }
    public void buildGpu(String gpu){
        computer.setGpu(gpu);
    }
    public void buildPower(String power){
        computer.setPower(power);
    }
    public void buildHeatSink(String heatSink){
        computer.setHeatSink(heatSink);
    }
    public void buildChassis(String chassis){
        computer.setChassis(chassis);
    }
    public Computer build(){
        return computer;
    }
}

// 测试
public static void main(String[] args) {
    SimpleComputerBuilder simpleComputerBuilder = new SimpleComputerBuilder();
    simpleComputerBuilder.buildMotherBoard("Extreme主板");
    simpleComputerBuilder.buildCpu("Inter 12900K");
    simpleComputerBuilder.buildMemory("芝奇幻峰戟 16G*2");
    simpleComputerBuilder.buildDisk("三星980Pro 2T");
    simpleComputerBuilder.buildGpu("华硕3090Ti 水猛禽");
    simpleComputerBuilder.buildPower("雷神二代1200W");
    simpleComputerBuilder.buildHeatSink("龙神二代一体式水冷");
    simpleComputerBuilder.buildChassis("太阳神机箱");
    // Computer(motherboard=Extreme主板, cpu=Inter 12900K, memory=芝奇幻峰戟 16G*2, disk=三星980Pro 2T, gpu=华硕3090Ti 水猛禽, power=雷神二代1200W, heatSink=龙神二代一体式水冷, chassis=太阳神机箱)
    System.out.println(simpleComputerBuilder.build());
}

可以看到,对比常规写法,这样写确实简化了系统结构,但同时也加重了建造者类的职责,也不是太符合单一职责原则,如果construct()

过于复杂,建议还是封装到 Director 中。

5.3 链式写法

java 复制代码
// 链式写法建造者类
public class SimpleComputerBuilder {
    private Computer computer = new Computer();
    public SimpleComputerBuilder buildMotherBoard(String motherBoard){
        computer.setMotherboard(motherBoard);
        return this;
    }
    public SimpleComputerBuilder buildCpu(String cpu){
        computer.setCpu(cpu);
        return this;
    }
    public SimpleComputerBuilder buildMemory(String memory){
        computer.setMemory(memory);
        return this;
    }
    public SimpleComputerBuilder buildDisk(String disk){
        computer.setDisk(disk);
        return this;
    }
    public SimpleComputerBuilder buildGpu(String gpu){
        computer.setGpu(gpu);
        return this;
    }
    public SimpleComputerBuilder buildPower(String power){
        computer.setPower(power);
        return this;
    }
    public SimpleComputerBuilder buildHeatSink(String heatSink){
        computer.setHeatSink(heatSink);
        return this;
    }
    public SimpleComputerBuilder buildChassis(String chassis){
        computer.setChassis(chassis);
        return this;
    }
    public Computer build(){
        return computer;
    }
}

// 测试
public static void main(String[] args) {
    Computer asusComputer = new SimpleComputerBuilder().buildMotherBoard("Extreme主板")
        .buildCpu("Inter 12900K")
        .buildMemory("芝奇幻峰戟 16G*2")
        .buildDisk("三星980Pro 2T")
        .buildGpu("华硕3090Ti 水猛禽")
        .buildPower("雷神二代1200W")
        .buildHeatSink("龙神二代一体式水冷")
        .buildChassis("太阳神机箱").build();
    System.out.println(asusComputer);
}

可以看到,其实链式写法与普通写法的区别并不大,只是在建造者类组装部件的时候,同时将建造者类返回即可,使用链式写法使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。

比较常见的mybatis-plus中的条件构造器就是使用的这种链式写法。

5.4 总结

适用场景:

  • 适用于创建对象需要很多步骤,但是步骤顺序不一定固定。
  • 如果一个对象有非常复杂的内部结构(属性),把复杂对象的创建和使用进行分离。

优点:

  • 封装性好,创建和使用分离。
  • 扩展性好,建造类之间独立、一定程度上解耦。

缺点:

  • 产生多余的Builder对象。
  • 产品内部发生变化,建造者都要修改,成本较大。

与工厂模式的区别:

  • 建造者模式更注重方法的调用顺序,工厂模式更注重创建对象。
  • 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样。
  • 关注点不同,工厂模式只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。
  • 建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。

与抽象工厂模式的区别:

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

二、 结构型模式

1.代理模式(Proxy Pattern)⭐

代理模式是指为其他对象提供一种代理,以控制对这个对象的访问。代理对象在访问对象和目标对象之间起到中介作用。

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

代理(Proxy)模式分为三种角色:

  • 抽象角色(Subject): 通过接口或抽象类声明真实角色和代理对象实现的业务方法。
  • 真实角色(Real Subject): 实现了抽象角色中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理角色(Proxy) : 提供了与真实角色相同的接口,其内部含有对真实角色的引用,它可以访问、控制或扩展真实角色的功能。

1.1 静态代理

静态代理就是指我们在给一个类扩展功能的时候,我们需要去书写一个静态的类,相当于在之前的类上套了一层,这样我们就可以在不改变之前的类的前提下去对原有功能进行扩展,静态代理需要代理对象和目标对象实现一样的接口。

java 复制代码
// 火车站接口,有卖票功能
public interface TrainStation {
    void sellTickets();
}
// 广州火车站卖票
public class GuangzhouTrainStation implements TrainStation {
    @Override
    public void sellTickets() {
        System.out.println("广州火车站卖票啦");
    }
}
// 代售点卖票(代理类)
public class ProxyPoint implements TrainStation {
    // 目标对象(代理火车站售票)
    private TrainStation station = null;

	public ProxyPoint(TrainStation station){
		this.station  = station;
	}

    @Override
    public void sellTickets() {
        System.out.println("代售加收5%手续费");
        station.sellTickets();
    }
}

// 测试
public static void main(String[] args) {
	TrainStation station = new GuangzhouTrainStation()
    ProxyPoint proxyPoint = new ProxyPoint(station);
    // 代售加收5%手续费
    // 火车站卖票啦
    proxyPoint.sellTickets();
}

可以从上面代码看到,我们访问的是ProxyPoint对象,也就是说ProxyPoint是作为访问对象和目标对象的中介的,同时也对sellTickets方法进行了增强(代理点收取加收5%手续费)。

静态代理的优点是实现简单,容易理解,只要确保目标对象和代理对象实现共同的接口或继承相同的父类就可以在不修改目标对象的前提下进行扩展。

而缺点也比较明显,那就是代理类和目标类必须有共同接口(父类) ,并且需要为每一个目标类维护一个代理类 ,当需要代理的类很多时会创建出大量代理类。一旦接口或父类的方法有变动,目标对象和代理对象都需要作出调整

1.2 动态代理

代理类在代码运行时创建的代理称之为动态代理。动态代理中代理类并不是预先在Java代码中定义好的,而是运行时由JVM动态生成,并且可以代理多个目标对象。

1.2.1 jdk动态代理

JDK动态代理是Java JDK自带的一个动态代理实现, 位于java.lang.reflect包下。

  1. 代理类实现InvocationHandler接口
  2. 实现invok()方法, 对目标方法进行增强
  3. 使用Proxy.newProxyInstance(classLoader, interfaces[], invocationHandler)
java 复制代码
// 火车站接口,有卖票功能
public interface TrainStation {
    void sellTickets();
}

// 广州火车站卖票
public class GuangzhouTrainStation implements TrainStation {
    @Override
    public void sellTickets() {
        System.out.println("广州火车站卖票啦");
    }
}
// 深圳火车站卖票
public class ShenzhenTrainStation implements TrainStation {
    @Override
    public void sellTickets() {
        System.out.println("深圳火车站卖票啦");
    }
}
// 代售点卖票(代理类)
public class ProxyPoint implements InvocationHandler {

    private TrainStation trainStation;
    
    public TrainStation getProxyObject(TrainStation trainStation) {
        this.trainStation = trainStation;
        Class<? extends TrainStation> clazz = trainStation.getClass();
        return (TrainStation) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代售火车票收取5%手续费");
        return method.invoke(this.trainStation, args);
    }
}

// 测试
public static void main(String[] args) {
    ProxyPoint proxy = new ProxyPoint();
    
    TrainStation guangzhouTrainStation = proxy.getProxyObject(new GuangzhouTrainStation());
    guangzhouTrainStation.sellTickets();
    // 代售火车票收取5%手续费
    // 广州火车站卖票啦

    TrainStation shenzhenTrainStation = proxy.getProxyObject(new ShenzhenTrainStation());
    shenzhenTrainStation.sellTickets();
    // 代售火车票收取5%手续费
    // 深圳火车站卖票啦
}

优点:

  • 使用简单、维护成本低。
  • Java原生支持,不需要任何依赖。
  • 解决了静态代理存在的多数问题。

缺点:

  • 由于使用反射,性能会比较差。
  • 只支持接口实现,不支持继承, 不满足所有业务场景。

1.2.2 CGLIB动态代理

CGLIB是一个强大的、高性能的代码生成库。它可以在运行期扩展Java类和接口,其被广泛应用于AOP框架中(Spring、dynaop)中,

用以提供方法拦截。CGLIB比JDK动态代理更强的地方在于它不仅可以接管Java接口, 还可以接管普通类的方法

java 复制代码
<!-- 先引入cglib包 -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>${cglib-version}</version>
</dependency>
  1. 代理类实现MethodInterceptor方法拦截器
  2. 通过Enhancer设置要代理的目标对象 以及方法拦截器
  3. Enhancer.create()生成代理对象
java 复制代码
// 代售点卖票(代理类)
public class ProxyPoint implements MethodInterceptor {
    public TrainStation getProxyObject(Class<? extends TrainStation> trainStation) {
        //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
        Enhancer enhancer =new Enhancer();
        //设置父类的字节码对象
        enhancer.setSuperclass(trainStation);
        //设置回调函数
        enhancer.setCallback(this);
        //创建代理对象并返回
        return (TrainStation) enhancer.create();
    }
    
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("代售火车票收取5%手续费");
        return methodProxy.invokeSuper(o, objects);
    }
}

// 测试
public static void main(String[] args) {
    ProxyPoint proxy = new ProxyPoint();
    
    TrainStation guangzhouTrainStation = proxy.getProxyObject(GuangzhouTrainStation.class);
    guangzhouTrainStation.sellTickets();
    // 代售火车票收取5%手续费
    // 广州火车站卖票啦

    TrainStation shenzhenTrainStation = proxy.getProxyObject(ShenzhenTrainStation.class);
    shenzhenTrainStation.sellTickets();
    // 代售火车票收取5%手续费
    // 深圳火车站卖票啦
}

1.3 总结

应用场景:

  • 保护目标对象。
  • 增强目标对象。

优点:

  • 代理模式能将代理对象与真实被调用的目标对象分离。
  • 一定程度上降低了系统的耦合程度,易于扩展。
  • 代理可以起到保护目标对象的作用。
  • 增强目标对象的职责。

缺点:

  • 代理模式会造成系统设计中类的数目增加。
  • 在客户端和目标对象之间增加了一个代理对象,请求处理速度变慢。
  • 增加了系统的复杂度。

两种动态代理的对比:

  • JDK动态代理的特点:
    • 需要实现InvocationHandler接口, 并实现invoke方法。
    • 被代理类需要实现接口, 它不支持继承。
    • JDK 动态代理类不需要事先定义好, 而是在运行期间动态生成。
    • JDK 动态代理不需要实现和被代理类一样的接口, 所以可以绑定多个被代理类。
    • 主要实现原理为反射, 它通过反射在运行期间动态生成代理类, 并且通过反射调用被代理类的实际业务方法。
  • cglib的特点:
    • cglib动态代理中使用的是FastClass机制。
    • cglib生成字节码的底层原理是使用ASM字节码框架。
    • cglib动态代理需创建3份字节码,所以在第一次使用时会比较耗性能,但是后续使用较JDK动态代理方式更高效,适合单例bean场景。
    • cglib由于是采用动态创建子类的方法,对于final方法,无法进行代理。

2.适配器模式(Adapter Class/Object)⭐

适配器模式,它的功能是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作。适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

适配器模式(Adapter)包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)接口:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

2.1 类适配器

类适配器是通过定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件来实现的,类图如下:

类图

java 复制代码
// 适配者 220V电压
public class AC220 {
    public int output() {
        System.out.println("输出220V交流电");
        return 220;
    }
}

// 目标 5V
public interface DC5 {
    public int output5();
}

// 适配器类(电源适配器)
public class PowerAdapter extends AC220 implements DC5 {
    @Override
    public int output5() {
        int output220 = super.output();
        int output5 = output220 / 44;
        System.out.println(output220 + "V适配转换成" + output5 + "V");
        return output5;
    }
}

// 测试
public static void main(String[] args) {
    PowerAdapter powerAdapter = new PowerAdapter();
    // 输出220V交流电
    powerAdapter.output();
    // 输出220V交流电
    // 220V适配转换成5V
    powerAdapter.output5();
}

通过上面代码例子可以看出,类适配器有一个很明显的缺点,就是违背了合成复用原则。结合上面的例子,假如我不是220V的电压了,是380V电压呢?那就要多建一个380V电压的适配器了。同理,由于Java是单继承的原因,如果不断的新增适配者,那么就要无限的新增适配器,于是就有了对象适配器。

2.2 对象适配器

对象适配器的实现方式是通过现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

java 复制代码
// 电源接口
public interface Power {
    int output();
}

// 适配者 220V电压
public class AC220 implements Power {
    @Override
    public int output() {
        System.out.println("输出220V交流电");
        return 220;
    }
}

// 目标 5V
public interface DC5 {
    public int output5();
}

@AllArgsConstructor
public class PowerAdapter implements DC5 {

    // 适配者
    private Power power;

    @Override
    public int output5() {
        int output220 = power.output();
        int output5 = output220 / 44;
        System.out.println(output220 + "V适配转换成" + output5 + "V");
        return output5;
    }
}

// 测试
public static void main(String[] args) {
    DC5 powerAdapter = new PowerAdapter(new AC220());
    // 输出220V交流电
    // 220V适配转换成5V
    powerAdapter.output5();
}

可以看到,上面代码中,只实现了目标接口,并没有继承适配者,而是将适配者类实现适配者接口,在适配器中引入适配者接口,当我们需要使用不同的适配者通过适配器进行转换时,就无需再新建适配器类了,如上面例子,假如我需要380V的电源转换成5V的,那么客户端只需要调用适配器时传入380V电源的类即可,就无需再新建一个380V电源的适配器了(PS:上述逻辑代码中output220

/ 44请忽略,可以根据实际情况编写实际的通用逻辑代码)。

2.3 接口适配器

接口适配器主要是解决类臃肿的问题,我们可以把所有相近的适配模式的方法都放到同一个接口里面,去实现所有方法,当客户端需要哪个方法直接调用哪个方法即可。如上面例子所示,我们只是转换成了5V电压,那假如我要转换成12V,24V,30V...呢?那按照上面的写法就需要新建12V,24V,30V...的接口,这样就会导致类过于多了。那么我们就可以把5V,12V,24V,30V...这些转换方法,通通都写到一个接口里去,这样当我们需要转换哪种就直接调用哪种即可。

java 复制代码
// 这里例子 输出不同直流电接口
public interface DC {
    int output5();
    int output12();
    int output24();
    int output30();
}

// 适配器类(电源适配器)
@AllArgsConstructor
public class PowerAdapter implements DC {
    private Power power;
    @Override
    public int output5() {
        // 具体实现逻辑
        return 5;
    }
    @Override
    public int output12() {
        // 具体实现逻辑
        return 12;
    }
    @Override
    public int output24() {
        // 具体实现逻辑
        return 24;
    }
    @Override
    public int output30() {
        // 具体实现逻辑
        return 30;
    }
}

2.4 总结

适用场景:

  • 已经存在的类,它的方法和需求不匹配(方法结构相同或相似)的情况。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

优点:

  • 能提高类的透明性和复用,现有的类复用但不需要改变。
  • 目标类和适配器类解耦,提高程序的扩展性。
  • 在很多业务场景中符合开闭原则。

缺点:

  • 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
  • 增加代码阅读难度,降级代码可读性,过多使用适配器会使系统代码变得凌乱。

3.装饰模式(Decorator Pattern)⭐

装饰模式,是指在不改变原有对象的基础上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能)

装饰(Decorator)模式中的角色:

  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

3.1 继承方式(原始人方式)

举一个简单的例子,假如现在有一碟炒饭,每个人的口味不一样,有些人喜欢加鸡蛋,有些人喜欢加鸡蛋火腿,有些人喜欢加鸡蛋火腿胡萝卜等,那么就会发现,如果采用继承的方式去实现这个例子,那么每加一个配料,都需要创建新的配料类去继承上一个旧的配料类,那么久而久之,就会产生很多类了,而且还不利于扩展,代码如下:

java 复制代码
// 炒饭类
public class FriedRice {
   String getDesc() {
       return "炒饭";
   }
   Integer getPrice() {
       return 5;
   }
}

// 炒饭加鸡蛋类
public class FriedRiceAddEgg extends FriedRice{
   String getDesc() {
       return super.getDesc() + "+鸡蛋";
   }
   Integer getPrice() {
       return super.getPrice() + 2;
   }
}

// 炒饭加鸡蛋加火腿类
public class FriedRiceAddEggAndHam extends FriedRiceAddEgg {
   String getDesc() {
       return super.getDesc() + "+火腿";
   }
   Integer getPrice() {
       return super.getPrice() + 3;
   }
}

// 测试方法
public static void main(String[] args) {
   FriedRice friedRice = new FriedRice();
   System.out.println(friedRice.getDesc() + friedRice.getPrice() + "元");// 炒饭5元
   
   FriedRice friedRiceAddEgg = new FriedRiceAddEgg();
   System.out.println(friedRiceAddEgg.getDesc() + friedRiceAddEgg.getPrice() + "元"); // 炒饭+鸡蛋7元
   
   FriedRice friedRiceAddEggAndHam = new FriedRiceAddEggAndHam();
   System.out.println(friedRiceAddEggAndHam.getDesc() + friedRiceAddEggAndHam.getPrice() + "元");// 炒饭+鸡蛋+火腿10元
}

可以从上面看到,如果我们只需要炒饭加火腿,那么我们还需要创建一个FriedRiceAddHam类去继承FriedRice类,所以继承的方式扩展性非常不好,且需要定义非常多的子类,下面就可以用装饰器模式去改进它。

3.2 装饰器模式方式

java 复制代码
// 炒饭类
public class FriedRice {
    String getDesc() {
        return "炒饭";
    }
    Integer getPrice() {
        return 5;
    }
}

// 配料表
public abstract class Ingredients extends FriedRice {

    private FriedRice friedRice;
    
    public Ingredients(FriedRice friedRice) {
        this.friedRice = friedRice;
    }
    
    String getDesc() {
        return this.friedRice.getDesc();
    }
    
    Integer getPrice() {
        return this.friedRice.getPrice();
    }
}

// 鸡蛋配料
public class Egg extends Ingredients {

    public Egg(FriedRice friedRice) {
        super(friedRice);
    }
    
    String getDesc() {
        return super.getDesc() + "+鸡蛋";
    }
    
    Integer getPrice() {
        return super.getPrice() + 2;
    }
}

// 火腿配料
public class Ham extends Ingredients {
    public Ham(FriedRice friedRice){
        super(friedRice);
    }
    String getDesc() {
        return super.getDesc() + "+火腿";
    }
    Integer getPrice() {
        return super.getPrice() + 3;
    }
}

// 测试方法
public static void main(String[] args) {
    FriedRice friedRice = new FriedRice();
    System.out.println(friedRice.getDesc() + friedRice.getPrice() + "元"); // 炒饭5元
    
    friedRice = new Egg(friedRice);
    System.out.println(friedRice.getDesc() + friedRice.getPrice() + "元"); // 炒饭+鸡蛋7元
    
    friedRice = new Egg(friedRice);
    System.out.println(friedRice.getDesc() + friedRice.getPrice() + "元");// 炒饭+鸡蛋+鸡蛋9元
    
    friedRice = new Ham(friedRice);
    System.out.println(friedRice.getDesc() + friedRice.getPrice() + "元");// 炒饭+鸡蛋+鸡蛋+火腿12元
}

可以看到,使用装饰器模式的方法实现,与普通的继承方法实现,最大的区别就是一种配料只有一个类,而且在加配料的时候,也可以直接想加多少就加多少,不需要说一个鸡蛋一个类,两个鸡蛋也要创建一个类,这样可以带来比继承更加灵活的扩展功能,使用也更加方便。

3.3 总结

装饰器模式与静态代理模式对比:

  • 装饰器模式就是一种特殊的代理模式。
  • 装饰器模式强调自身的功能扩展,用自己说了算的透明扩展,可动态定制的扩展;代理模式强调代理过程的控制。
  • 获取目标对象构建的地方不同,装饰者是从外界传递进来的,可以通过构造方法传递;静态代理是在代理类内部创建,以此来隐藏目标对象。

适用场景:

  • 用于扩展一个类的功能或者给一个类添加附加职责。
  • 动态的给一个对象添加功能,这些功能同样也可以再动态的撤销。

优点:

  • 装饰器是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象扩展功能,即插即用。
  • 通过使用不同装饰类以及这些装饰类的排列组合,可实现不同效果。
  • 装饰器完全遵守开闭原则。

缺点:

  • 会出现更多的代码,更多的类,增加程序的复杂性。
  • 动态装饰时,多层装饰会更复杂。

4.桥接模式(Bridge Pattern)

桥接模式也称为桥梁模式、接口模式或者柄体(Handle andBody)模式,是将抽象部分与他的具体实现部分分离,使它们都可以独立地变化,通过组合的方式建立两个类之间的联系,而不是继承。

桥接(Bridge)模式包含以下主要角色:

  • 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
  • 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。

4.1 代码实现

下面以一个多系统多视频格式文件播放为例子:

java 复制代码
// 视频接口
public interface Video {
    void decode(String fileName);
}

// MP4格式类
public class Mp4 implements Video{
    @Override
    public void decode(String fileName) {
        System.out.println("MP4视频文件:"+ fileName);
    }
}
// RMVB格式类
public class Rmvb implements Video{
    @Override
    public void decode(String fileName) {
        System.out.println("rmvb文件:" + fileName);
    }
}
// 操作系统抽象类
@AllArgsConstructor
public abstract class OperatingSystem {
    Video video;
    public abstract void play(String fileName);

}
// iOS系统
public class Ios extends OperatingSystem {
    public Ios(Video video){
        super(video);
    }
    @Override
    public void play(String fileName) {
        video.decode(fileName);
    }
}
// windows系统
public class Windows extends OperatingSystem {
    public Windows(Video video){
        super(video);
    }
    @Override
    public void play(String fileName) {
        video.decode(fileName);
    }
}

类关系图

可以通过类图看到,视频类和操作系统类之间通过OperatingSystem类桥接关联起来。

4.2 总结

适用场景:

  • 在抽象和具体实现之间需要增加更多的灵活性的场景。
  • 一个类存在两个(或多个)独立变化的维度,而这两个(或多个)维度都需要独立进行扩展。
  • 不希望使用继承,或因为多层继承导致系统类的个数剧增。

优点:

  • 分离抽象部分及其具体实现部分。
  • 提高了系统的扩展性。
  • 符合开闭原型。
  • 符合合成复用原则。

缺点:

  • 增加了系统的理解与设计难度。
  • 需要正确地识别系统中两个独立变化的维度。

5.外观模式(Facade)

外观模式又称门面模式,提供了一个统一的接口,用来访问子系统中的一群接口。

特征:门面模式定义了一个高层接口,让子系统更容易使用。

外观(Facade)模式包含以下主要角色:

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

5.1 代码实现

下面以一个智能音箱实现起床睡觉一键操作电器的场景,通过代码模拟一下这个场景:

java 复制代码
public class Light {
    public void on() {
        System.out.println("开灯");
    }
    public void off() {
        System.out.println("关灯");
    }
}
public class Tv {
    public void on() {
        System.out.println("开电视");
    }
    public void off() {
        System.out.println("关电视");
    }
}
public class Fan {
    public void on() {
        System.out.println("开风扇");
    }
    public void off() {
        System.out.println("关风扇");
    }
}
public class SmartSpeaker {
    private Light light;
    private Tv tv;
    private Fan fan;
    public SmartSpeaker() {
        light = new Light();
        tv = new Tv();
        fan = new Fan();
    }
    public void say(String order) {
        if (order.contains("起床")) {
            getUp();
        } else if (order.contains("睡觉")) {
            sleep();
        } else {
            System.out.println("我还听不懂你说的啥!");
        }
    }
    public void getUp() {
        System.out.println("起床");
        light.on();
        tv.on();
        fan.off();
    }
    public void sleep() {
        System.out.println("睡觉");
        light.off();
        tv.off();
        fan.on();
    }
}

public static void main(String[] args) {
    SmartSpeaker smartSpeaker = new SmartSpeaker();
    //睡觉
    //关灯
    //关电视
    //开风扇
    smartSpeaker.say("我要睡觉了!");
    //起床
    //开灯
    //开电视
    //关风扇
    smartSpeaker.say("我起床了!");
    //我还听不懂你说的啥!
    smartSpeaker.say("Emmm");
}

5.2 总结

适用场景:

  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

优点:

  • 简化了调用过程,无需深入了解子系统,以防给子系统带来风险。
  • 减少系统依赖、松散耦合。
  • 更好地划分访问层次,提高了安全性。
  • 遵循迪米特法则,即最少知道原则。

缺点:

  • 当增加子系统和扩展子系统行为时,可能容易带来未知风险。
  • 不符合开闭原则。
  • 某些情况下可能违背单一职责原则。

6.组合模式(Composite Pattern)

组合模式也称为整体-部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子结点)和组合对象(树枝节点)用相同的接口进行表示。

作用:使客户端对单个对象和组合对象保持一致的方式处理。

组合模式主要包含三种角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。

  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。

  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

6.1 代码实现

下面以一个添加菜单的例子通过代码实现:

java 复制代码
// 菜单组件
public abstract class MenuComponent {
    String name;
    Integer level;
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException("不支持添加操作!");
    }
    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException("不支持删除操作!");
    }
    public MenuComponent getChild(Integer i) {
        throw new UnsupportedOperationException("不支持获取子菜单操作!");
    }
    public String getName() {
        throw new UnsupportedOperationException("不支持获取名字操作!");
    }
    public void print() {
        throw new UnsupportedOperationException("不支持打印操作!");
    }
}

// 菜单类
public class Menu extends MenuComponent {
    private List<MenuComponent> menuComponentList = new ArrayList<>();
    public Menu(String name,int level){
        this.level = level;
        this.name = name;
    }
    @Override
    public void add(MenuComponent menuComponent) {
        menuComponentList.add(menuComponent);
    }
    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponentList.remove(menuComponent);
    }
    @Override
    public MenuComponent getChild(Integer i) {
        return menuComponentList.get(i);
    }
    @Override
    public void print() {
        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
        for (MenuComponent menuComponent : menuComponentList) {
            menuComponent.print();
        }
    }
}

// 子菜单类
public class MenuItem extends MenuComponent {
    public MenuItem(String name,int level) {
        this.name = name;
        this.level = level;
    }
    @Override
    public void print() {
        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
    }
}

// 测试方法
public static void main(String[] args) {
    //创建一级菜单
    MenuComponent component = new Menu("系统管理",1);

    MenuComponent menu1 = new Menu("用户管理",2);
    menu1.add(new MenuItem("新增用户",3));
    menu1.add(new MenuItem("修改用户",3));
    menu1.add(new MenuItem("删除用户",3));

    MenuComponent menu2 = new Menu("角色管理",2);
    menu2.add(new MenuItem("新增角色",3));
    menu2.add(new MenuItem("修改角色",3));
    menu2.add(new MenuItem("删除角色",3));
    menu2.add(new MenuItem("绑定用户",3));

    //将二级菜单添加到一级菜单中
    component.add(menu1);
    component.add(menu2);

    //打印菜单名称(如果有子菜单一块打印)
    component.print();
}
// 测试结果
系统管理
--用户管理
----新增用户
----修改用户
----删除用户
--角色管理
----新增角色
----修改角色
----删除角色
----绑定用户

6.2 总结

适用场景:

  • 希望客户端可以忽略组合对象与单个对象的差异时。
  • 对象层次具备整体和部分,呈树形结构(如树形菜单,操作系统目录结构,公司组织架构等)。

优点:

  • 清楚地定义分层次的复杂对象,表示对象的全部或部分层次。
  • 让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 简化客户端代码。
  • 符合开闭原则。

缺点:

  • 限制类型时会较为复杂。
  • 使设计变得更加抽象。

分类:

  • 透明组合模式
    • 透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中MenuComponent声明了add() 、 remove() 、getChild()方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
    • 透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
  • 安全组合模式
    • 在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点Menu类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

7.享元模式(Flyweight Pattern)

享元模式又称为轻量级模式,是对象池的一种实现,类似于线程池,线程池可以避免不停的创建和销毁多个对象,消耗性能。提供了减少对象数量从而改善应用所需的对象结构的方式。宗旨:共享细粒度对象,将多个对同一对象的访问集中起来。

享元(Flyweight )模式中存在以下两种状态:

  • 内部状态,即不会随着环境的改变而改变的可共享部分。
  • 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

享元模式的主要有以下角色:

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

7.1 代码实现

下面通过查询火车票的例子来用代码进行模拟实现:

java 复制代码
// 抽象接口
public interface ITicket {
    void show(String seat);
}

// 抽象接口实现
public class TrainTicket implements ITicket {
    private String from;
    private String to;
    private Integer price;
    public TrainTicket(String from, String to) {
        this.from = from;
        this.to = to;
    }
    @Override
    public void show(String seat) {
        this.price = new Random().nextInt(500);
        System.out.println(from + "->" + to + ":" + seat + "价格:" + this.price);
    }
}

// 工厂类
public class TicketFactory {

    private static Map<String, ITicket> pool = new ConcurrentHashMap<>();
    
    public static ITicket getTicket(String from, String to) {
        String key = from + "->" + to;
        if (pool.containsKey(key)) {
            System.out.println("使用缓存获取火车票:" + key);
            return pool.get(key);
        }
        System.out.println("使用数据库获取火车票:" + key);
        ITicket ticket = new TrainTicket(from, to);
        pool.put(key, ticket);
        return ticket;
    }
}

// 测试
public static void main(String[] args) {
    ITicket ticket = getTicket("北京", "上海");
    //使用数据库获取火车票:北京->上海
    //北京->上海:二等座价格:20
    ticket.show("二等座");
    ITicket ticket1 = getTicket("北京", "上海");
    //使用缓存获取火车票:北京->上海
    //北京->上海:商务座价格:69
    ticket1.show("商务座");
    ITicket ticket2 = getTicket("上海", "北京");
    //使用数据库获取火车票:上海->北京
    //上海->北京:一等座价格:406
    ticket2.show("一等座");
    System.out.println(ticket == ticket1);//true
    System.out.println(ticket == ticket2);//false
}

可以看到ticket和ticket2是使用数据库查询的,而ticket1是使用缓存查询的,同时ticket ==

ticket1返回的是true,ticket == ticket2返回的是false,证明ticket和ticket1是共享的对象。

7.2 总结

适用场景:

  • 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
  • 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

优点:

  • 减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率。
  • 减少内存之外的其他资源占用。

缺点:

  • 关注内、外部状态。
  • 关注线程安全问题。
  • 使系统、程序的逻辑复杂化。

三、 行为型模式

1.模板方法模式(Template method pattern)

模板方法模式通常又叫模板模式,是指定义一个算法的骨架,并允许之类为其中的一个或者多个步骤提供实现。模板方法模式使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。

模板方法(Template Method)模式包含以下主要角色:

  • 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
      • 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
      • 具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
      • 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
  • 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

1.1 代码实现

下面以一个简单的请假流程来通过代码来实现:

java 复制代码
public abstract class DayOffProcess {
    // 请假模板
    public final void dayOffProcess() {
        // 领取申请表
        this.pickUpForm();
        // 填写申请信息
        this.writeInfo();
        // 签名
        this.signUp();
        // 提交到不同部门审批
        this.summit();
        // 行政部备案
        this.filing();
    }
    
    private void filing() {
        System.out.println("行政部备案");
    }
    
    protected abstract void summit();
    
    protected abstract void signUp();
    
    private void writeInfo() {
        System.out.println("填写申请信息");
    }
    
    private void pickUpForm() {
        System.out.println("领取申请表");
    }
}

public class ZhangSan extends DayOffProcess {
    @Override
    protected void summit() {
        System.out.println("张三签名");
    }
    @Override
    protected void signUp() {
        System.out.println("提交到技术部审批");
    }
}

public class Lisi extends DayOffProcess {
    @Override
    protected void summit() {
        System.out.println("李四签名");
    }
    @Override
    protected void signUp() {
        System.out.println("提交到市场部审批");
    }
}

// 测试方法
public static void main(String[] args) {
    DayOffProcess zhangsan = new ZhangSan();
    //领取申请表
    //填写申请信息
    //提交到技术部审批
    //张三签名
    //行政部备案
    zhangsan.dayOffProcess();
    DayOffProcess lisi = new Lisi();
    //领取申请表
    //填写申请信息
    //提交到市场部审批
    //李四签名
    //行政部备案
    lisi.dayOffProcess();
}

1.2 总结

适用场景:

  • 一次性实现一个算法不变的部分,并将可变的行为留给子类来实现。
  • 各子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复。

优点:

  • 利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
  • 将不同的代码不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性。
  • 把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则。

缺点:

  • 类数目的增加,每一个抽象类都需要一个子类来实现,这样导致类的个数增加。
  • 类数量的增加,间接地增加了系统实现的复杂度。
  • 继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要改一遍。

2.策略模式(Strategy Pattern)⭐

策略模式又叫政策模式(PolicyPattern),它是将定义的算法家族分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。可以避免多重分支的if...else和switch语句。

策略模式的主要角色如下:

  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

2.1 普通案例(会员卡打折)

java 复制代码
// 会员卡接口
public interface VipCard {
    public void discount();
}

public class GoldCard implements VipCard {
    @Override
    public void discount() {
        System.out.println("金卡打7折");
    }
}

public class SilverCard implements VipCard {
    @Override
    public void discount() {
        System.out.println("银卡打8折");
    }
}

public class CopperCard implements VipCard {
    @Override
    public void discount() {
        System.out.println("铜卡打9折");
    }
}

public class Normal implements VipCard {
    @Override
    public void discount() {
        System.out.println("普通会员没有折扣");
    }
}

// 会员卡容器类
public class VipCardFactory {
    private static Map<String, VipCard> map = new ConcurrentHashMap<>();
    static {
        map.put("gold", new GoldCard());
        map.put("silver", new SilverCard());
        map.put("copper", new CopperCard());
    }
    public static VipCard getVIPCard(String level) {
        return map.get(level) != null ? map.get(level) : new Normal();
    }

}

// 测试方法
public static void main(String[] args) {
    //金卡打7折
    VipCardFactory.getVIPCard("gold").discount();
    //银卡打8折
    VipCardFactory.getVIPCard("silver").discount();
    //普通会员没有折扣
    VipCardFactory.getVIPCard("other").discount();
}

用一个容器(Map)装起来,可以通过传进来的参数直接获取对应的策略,避免了if...else。

2.2 支付方式案例

java 复制代码
// 支付方式抽象类
public abstract class Payment {
    public String pay(String uid, double money) {
        double balance = queryBalance(uid);
        if (balance < money) {
            return "支付失败!余额不足!欠" + (money - balance) + "元!";
        }
        return "支付成功!支付金额:" + money + "余额剩余:" + (balance - money);
    }
    protected abstract String getPaymentName();
    protected abstract double queryBalance(String uid);
}

// 现金支付 默认方式
public class Cash extends Payment{
    @Override
    protected String getPaymentName() {
        return "现金支付";
    }
    @Override
    protected double queryBalance(String uid) {
        return 1000;
    }
}

// 支付宝类
public class AliPay extends Payment {

    @Override
    protected String getPaymentName() {
        return "支付宝";
    }

    @Override
    protected double queryBalance(String uid) {
        return 500;
    }
}

// 微信支付类
public class WeChatPay extends Payment {
    @Override
    protected String getPaymentName() {
        return "微信支付";
    }

    @Override
    protected double queryBalance(String uid) {
        return 300;
    }
}

// 支付方式容器策略类
public class PaymentStrategy {
    private static Map<String, Payment> map = new ConcurrentHashMap<>();
    static {
        map.put("WeChat", new WeChatPay());
        map.put("Ali", new AliPay());
    }
    public static Payment getPayment(String payment) {
        return map.get(payment) == null ? new Cash() : map.get(payment);
    }

}

// 订单交易类
@AllArgsConstructor
public class Order {
    private String uid;
    private double amount;
    public String pay() {
        return pay("cash");
    }
    public String pay(String key) {
        Payment payment = PaymentStrategy.getPayment(key);
        System.out.println("欢迎使用" + payment.getPaymentName());
        System.out.println("本次交易金额:" + this.amount + ",开始扣款...");
        return payment.pay(this.uid, this.amount);
    }
}

// 测试方法
public static void main(String[] args) {
    Order order = new Order("20221014001", 500);
    //欢迎使用微信支付
    //本次交易金额:500.0,开始扣款...
    //支付失败!余额不足!欠200.0元!
    System.out.println(order.pay("WeChat"));
    //欢迎使用支付宝
    //本次交易金额:500.0,开始扣款...
    //支付成功!支付金额:500.0余额剩余:0.0
    System.out.println(order.pay("Ali"));
    //欢迎使用现金支付
    //本次交易金额:500.0,开始扣款...
    //支付成功!支付金额:500.0余额剩余:500.0
    System.out.println(order.pay());
}

2.3 总结

适用场景:

  • 系统中有很多类,而它们的区别仅仅在于它们的行为不同。
  • 系统需要动态地在几种算法中选择一种。
  • 需要屏蔽算法规则。

优点:

  • 符合开闭原则。
  • 避免使用多重条件语句。
  • 可以提高算法的保密性和安全性。
  • 易于扩展。

缺点:

  • 客户端必须知道所有的策略,并且自行决定使用哪一个策略类。
  • 代码中会产生非常多的策略类,增加维护难度。

3.命令模式(Command Pattern)

命令模式是对命令的封装,每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式解耦了请求方和接收方,请求方只需请求执行命令,不用关心命令是怎样被接收,怎样被操作以及是否被执行等。本质:解耦命令的请求与处理。

命令模式包含以下主要角色:

  • 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
  • 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
  • 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。

3.1 代码实现

下面以一个播放器的例子来进行代码实现:

java 复制代码
// 播放器类
public class Player {
    public void play() {
        System.out.println("正常播放");
    }
    public void pause() {
        System.out.println("暂停播放");
    }
    public void stop() {
        System.out.println("停止播放");
    }
}

// 命令接口
public interface IAction {
    void excuse();
}

// 播放命令类
@AllArgsConstructor
public class PlayAction implements IAction {
    private Player player;
    @Override
    public void excuse() {
        this.player.play();
    }
}

// 暂停命令类
@AllArgsConstructor
public class PauseAction implements IAction {
    private Player player;
    @Override
    public void excuse() {
        this.player.pause();
    }
}

// 停止命令类
@AllArgsConstructor
public class StopAction implements IAction{
    private Player player;
    @Override
    public void excuse() {
        this.player.stop();
    }
}

// 控制器
public class Controller {
    public void excuse(IAction action) {
        action.excuse();
    }
}

// 测试方法
public static void main(String[] args) {
     // 正常播放
     new Controller().excuse(new PlayAction(new Player()));
     // 暂停播放
     new Controller().excuse(new PauseAction(new Player()));
     // 停止播放
     new Controller().excuse(new StopAction(new Player()));
 }

3.2 总结

适用场景:

  • 现实语义中具备"命令"的操作(如命令菜单,shell命令...)。
  • 请求调用者和请求接收者需要解耦,使得调用者和接收者不直接交互。
  • 需要抽象出等待执行的行为,比如撤销操作和恢复操作等。
  • 需要支持命令宏(即命令组合操作)。

优点:

  • 通过引入中间件(抽象接口),解耦了命令的请求与实现。
  • 扩展性良好,可以很容易地增加新命令。
  • 支持组合命令,支持命令队列。
  • 可以在现有的命令的基础上,增加额外功能。

缺点:

  • 具体命令类可能过多。
  • 增加 了程序的复杂度,理解更加困难。

4.职责链模式(chain of responsibility pattern)⭐

职责链模式是将链中每一个节点看作是一个对象,每个节点处理的请求均不同,且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直至有对象处理这个请求为止。

职责链模式主要包含以下角色:

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

4.1 代码实现

下面以一个简单的登录校验流程来通过代码进行实现:

java 复制代码
// 用户实体类
@Data
public class User {
    private String username;
    private String password;
    private String role;
}

// handler抽象类
public abstract class Handler {
    protected Handler next;
    // 返回handler方便链式操作
    public void next(Handler next) {
        this.next = next;
    }
    // 流程开始的方法
    public abstract void doHandler(User user);
}

// 校验用户名或者密码是否为空
public class ValidateHandler extends Handler {
    @Override
    public void doHandler(User user) {
        if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
            System.out.println("用户名或者密码为空!");
            return;
        }
        System.out.println("校验通过");
        next.doHandler(user);
    }
}

// 登录校验,校验用户名是否匹配密码
public class LoginHandler extends Handler {
    @Override
    public void doHandler(User user) {
        if (!"pyy52hz".equals(user.getUsername()) || !"123456".equals(user.getPassword())) {
            System.out.println("用户名或者密码不正确!请检查!");
            return;
        }
        user.setRole("admin");
        System.out.println("登陆成功!角色为管理员!");
        next.doHandler(user);
    }
}

// 权限校验
public class AuthHandler extends Handler {
    @Override
    public void doHandler(User user) {
        if (!"admin".equals(user.getRole())) {
            System.out.println("无权限操作!");
            return;
        }
        System.out.println("角色为管理员,可以进行下一步操作!");
    }
}

// 登录流程
public class LoginService {
    public void login(User user) {
        Handler validateHandler = new ValidateHandler();
        Handler loginHandler = new LoginHandler();
        Handler authHandler = new AuthHandler();
        validateHandler.next(loginHandler);
        loginHandler.next(authHandler);
        validateHandler.doHandler(user);
    }
}

// 测试方法
public static void main(String[] args){
  User user = new User();
  //校验通过
  //用户名或者密码不正确!请检查!
  user.setUsername("pyy52hz");
  user.setPassword("1234567");
  LoginService loginService = new LoginService();
  loginService.login(user);
  //校验通过
  //登陆成功!角色为管理员!
  //角色为管理员,可以进行下一步操作!
  user.setUsername("pyy52hz");
  user.setPassword("123456");
  loginService.login(user);
}

4.2 结合建造者模式

与基础版本区别主要是Handler类中新增一个Builder的内部类,以及流程类里改用链式写法,具体如下:

java 复制代码
// handler抽象类
public abstract class Handler<T> {
    protected Handler next;
    
    // 返回handler方便链式操作
    public Handler next(Handler next) {
        this.next = next;
        return next;
    }
    
    // 流程开始的方法
    public abstract void doHandler(User user);

	// Builder
    static class Builder<T> {
        private Handler<T> head;
        private Handler<T> tail;
        
        public Builder<T> addHandler(Handler<T> handler) {
            if (this.head == null) {
                this.head = this.tail = handler;
                return this;
            }
            this.tail.next(handler);
            this.tail = handler;
            return this;
        }
        
        public Handler<T> build() {
            return this.head;
        }
    }
}

public class LoginService {
    public void login(User user) {
        Handler.Builder builder = new Handler.Builder();
        builder.addHandler(new ValidateHandler())
            .addHandler(new LoginHandler())
            .addHandler(new AuthHandler());
        builder.build().doHandler(user);
    }
}

4.3 总结

适用场景:

  • 多个对象可以处理同一请求,但具体由哪个对象处理则在运行时动态决定。
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
  • 可动态指定一组对象处理请求。
  • spring cloud gateway的路由过滤器使用的就是责任链+建造者模式

优点:

  • 将请求与处理解耦。
  • 请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象。
  • 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果。
  • 链路结构灵活,可以通过改变链路结构动态地新增或删减责任。
  • 易于扩展新的请求处理类(节点),符合开闭原则。

缺点:

  • 责任链太长或者处理时间过长,会影响整体性能。
  • 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。

5.状态模式(State Pattern)

状态模式也称为状态机模式(State Machine Pattern),是允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

状态模式包含以下主要角色:

  • 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为。

5.1 代码实现

java 复制代码
// 电梯状态(抽象状态)
public abstract class LiftState {
    protected Context context;
    public abstract void open();
    public abstract void close();
    public abstract void run();
    public abstract void stop();
}

// 开门状态(具体状态)
public class OpenState extends LiftState {
    @Override
    public void open() {
        System.out.println("电梯门打开了");
    }
    @Override
    public void close() {
        super.context.setLiftState(Context.CLOSE_STATE);
        super.context.close();
    }
    @Override
    public void run() {
    }
    @Override
    public void stop() {
    }
}

// 关门状态(具体状态)
public class CloseState extends LiftState {
    @Override
    public void open() {
        super.context.setLiftState(Context.OPEN_STATE);
        super.context.open();
    }
    @Override
    public void close() {
        System.out.println("电梯门关闭了!");
    }
    @Override
    public void run() {
        super.context.setLiftState(Context.RUN_STATE);
        super.context.run();
    }
    @Override
    public void stop() {
        super.context.setLiftState(Context.STOP_STATE);
        super.context.stop();
    }
}

// 运行状态(具体状态)
public class RunState extends LiftState {
    @Override
    public void open() {
    }
    @Override
    public void close() {
    }
    @Override
    public void run() {
        System.out.println("电梯正在运行...");
    }
    @Override
    public void stop() {
        super.context.setLiftState(Context.STOP_STATE);
        super.context.stop();
    }
}

// 停止状态(具体状态)
public class StopState extends LiftState {
    @Override
    public void open() {
        super.context.setLiftState(Context.OPEN_STATE);
        super.context.open();
    }
    @Override
    public void close() {
        super.context.setLiftState(Context.CLOSE_STATE);
        super.context.close();
    }
    @Override
    public void run() {
        super.context.setLiftState(Context.RUN_STATE);
        super.context.run();
    }
    @Override
    public void stop() {
        System.out.println("电梯停止了!");
    }
}

// 上下文
public class Context {
    private LiftState liftState;
    public static final LiftState OPEN_STATE = new OpenState();
    public static final LiftState CLOSE_STATE = new CloseState();
    public static final LiftState RUN_STATE = new RunState();
    public static final LiftState STOP_STATE = new StopState();
    
    public void setLiftState(LiftState liftState) {
        this.liftState = liftState;
        this.liftState.setContext(this);
    }
    
    public void open() {
        this.liftState.open();
    }
    public void close() {
        this.liftState.close();
    }
    public void run() {
        this.liftState.run();
    }
    public void stop() {
        this.liftState.stop();
    }
}

// 测试
public static void main(String[] args){
  Context context = new Context();
  context.setLiftState(new CloseState());
  //电梯门打开了
  //电梯门关闭了!
  //电梯正在运行...
  //电梯停止了!
  context.open();
  context.close();
  context.run();
  context.stop();
}

5.2 总结

适用场景:

  • 行为随状态改变而改变的场景。
  • 状态数量有限
  • 一个操作中含有庞大的多分支结构,并且这些分支取决于对象的状态。
  • spring的状态机

优点:

  • 结构清晰:将状态独立为类,消除了冗余的if...else或switch...case语句,使代码更加简洁,提高系统可维护性。
  • 将状态转换显示化:通常的对象内部都是使用数值类型来定义状态,状态的切换是通过赋值进行表现,不够直观;而使用状态类,在切换状态时,是以不同的类进行表示,转换目的更加明确。
  • 状态类职责明确且具备扩展性。

缺点:

  • 类膨胀:如果一个事物具备很多状态,则会造成状态类太多。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

6.观察者模式(Observer Mode)⭐

观察者模式,又叫发布-订阅(Publish/Subscribe)模式,模型-视图(Model/View)模式,源-监听器(Source/Listener)模式或从属者(Dependents)模式。定义一种一对多的依赖关系,一个主题对象可被多个观察者同时监听,使得每当主题对象状态变化时,所有依赖于它的对象都会得到通知并被自动更新。

6.1 代码实现

通过一个微信用户(观察者)订阅公众号(被观察者)接收公众号推送消息的例子来进行简单的代码实现:

java 复制代码
// 抽象观察者接口
public interface Observer {
    void update(String message);
}

// 微信用户类 具体的观察者
@AllArgsConstructor
public class WeixinUser implements Observer {
    private String name;
    @Override
    public void update(String message) {
        System.out.println(name + "接收到了消息(观察到了):" + message);
    }
}

// 被观察者接口
public interface Observable {
    // 新增用户(新增观察者)
    void add(Observer observer);
    // 移除用户,或者说用户取消订阅(移除观察者)
    void del(Observer observer);
    // 发布 推送消息
    void notify(String message);
}

// 具体的被观察者(公众号)
public class Subject implements Observable {
    // 观察者列表(订阅用户)
    private List<Observer> list = new ArrayList<>();
    @Override
    public void add(Observer observer) {
        list.add(observer);
    }
    @Override
    public void del(Observer observer) {
        list.remove(observer);
    }
    // 给每一个观察者(订阅者)推送消息
    @Override
    public void notify(String message) {
        list.forEach(observer -> observer.update(message));
    }

}

// 测试
public static void main(String[] args){
  Observable o = new Subject();
  WeixinUser user1 = new WeixinUser("张三");
  WeixinUser user2 = new WeixinUser("李四");
  WeixinUser user3 = new WeixinUser("王五");
  o.add(user1);
  o.add(user2);
  o.add(user3);
  o.notify("薛之谦演唱会要来到合肥啦!");
  // 运行结果
  // 张三接收到了消息(观察到了):薛之谦演唱会要来到合肥啦!
  // 李四接收到了消息(观察到了):薛之谦演唱会要来到合肥啦!
  // 王五接收到了消息(观察到了):薛之谦演唱会要来到合肥啦!
}

可以看到被观察者内部维护观察者 的列表, 被观察者 发生变化时循环调用观察者的监听方法

6.2 JDK实现

在 Java 中,通过java.util.Observable类和java.util.Observer接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。

6.2.1 Observable类

  • Observable类是抽象目标类(被观察者),它有一个Vector集合成员变量,用于保存所有要通知的观察者对象,下面是它最重要的 3 个方法:
    • void addObserver(Observer o) 方法 :用于将新的观察者对象添加到集合中。
    • void notifyObservers(Object arg)方法:调用集合中的所有观察者对象的update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。
    • void setChange()方法:用来设置一个boolean类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。

6.2.2 Observer 接口

Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作。

6.2.3 代码实现

下面还是通过微信用户订阅公众号的例子进行代码实现,方便对比他们之间的区别:

java 复制代码
// 具体的被观察者(公众号)
@Data
@AllArgsConstructor
public class Subject extends Observable {
    // 公众号的名字
    private String name;
    // 公众号发布消息
    public void notifyMessage(String message) {
        System.out.println(this.name + "公众号发布消息:" + message + "请关注用户留意接收!");
        super.setChanged();
        super.notifyObservers(message);
    }
}
@AllArgsConstructor
public class WeixinUser implements Observer {
    private String name;
    /**
     * @param o 被观察者
     * @param arg 被观察者带过来的参数,此例子中是公众号发布的消息
     */
    @Override
    public void update(Observable o, Object arg) {
        System.out.println(name + "关注了公众号(被观察者):" + ((Subject)o).getName() + ",接收到消息:" + arg);
    }
}
    
// 测试
public static void main(String[] args){
   WeixinUser user1 = new WeixinUser("张三");
   WeixinUser user2 = new WeixinUser("李四");
   WeixinUser user3 = new WeixinUser("王五");
   Subject subject = new Subject("演唱会消息发布");
   subject.addObserver(user1);
   subject.addObserver(user2);
   subject.addObserver(user3);
   subject.notifyMessage("薛之谦已经到合肥啦!");
   // 返回结果
   // 演唱会消息发布公众号发布消息:薛之谦演唱会要来到广州啦!请关注用户留意接收!
   // 王五关注了公众号(被观察者):演唱会消息发布,接收到消息:薛之谦已经到合肥啦!
   // 李四关注了公众号(被观察者):演唱会消息发布,接收到消息:薛之谦已经到合肥啦!
   // 张三关注了公众号(被观察者):演唱会消息发布,接收到消息:薛之谦已经到合肥啦!
}

6.3 Google的Guava实现

EventBus 术语 解释 备注
事件(消息) 可以向事件总线(EventBus)发布的对象 通常是一个类,不同的消息事件用不同的类来代替,消息内容就是类里面的属性
订阅 向事件总线注册监听者,以接受事件的行为 EventBus.register(Object),参数就是监听者
监听者 提供一个处理方法,希望接受和处理事件的对象 通常也是一个类,里面有消息的处理方法
处理方法 监听者提供的公共方法,事件总线使用该方法向监听者发送事件;该方法应使用 Subscribe 注解 监听者里面添加一个 Subscribe注解的方法,就可以认为是消息的处理方法
发布消息 通过事件总线向所有匹配的监听者提供事件 EventBus.post(Object)
java 复制代码
@AllArgsConstructor
public class WeixinUser {
    private String name;
    
    @Subscribe
    public void getMessage(Object arg) {
        System.out.println(this.name + "接收到消息:" + arg);
    }
    
    // 测试
    public static void main(String[] args){
        // 消息总线
        EventBus eventBus = new EventBus();
        eventBus.register(new WeixinUser("张三"));
        eventBus.register(new WeixinUser("李四"));
        eventBus.post("薛之谦演唱会要来到合肥啦!");
        // 返回结果
        // 张三接收到消息:薛之谦演唱会要来到合肥啦!
        // 李四接收到消息:薛之谦演唱会要来到合肥啦!
    }
}

6.4 总结

适用场景:

  • 当一个抽象模型包含两个方面内容,其中一个方面依赖于另一个方面。
  • 其他一个或多个对象的变化依赖于另一个对象的变化。
  • 实现类似广播机制的功能,无需知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播。

多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知。

优点:

  • 观察者和被观察者是松耦合(抽象耦合)的,符合依赖倒置原则。
  • 分离了表示层(观察者)和数据逻辑层(被观察者),并且建立了一套触发机制,使得数据的变化可以相应到多个表示层上。
  • 实现了一对多的通讯机制,支持事件注册机制,支持兴趣分发机制,当被观察者触发事件时,只有感兴趣的观察者可以接收到通知。

缺点:

  • 如果观察者数量过多,则事件通知会耗时较长。
  • 事件通知呈线性关系,如果其中一个观察者处理事件卡壳,会影响后续的观察者接收该事件。
  • 如果观察者和被观察者之间存在循环依赖,则可能造成两者之间的循环调用,导致系统崩溃。

7.中介者模式(mediator pattern)

中介者模式又称为调解者模式或调停者模式。用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

核心:通过中介者解耦系统各层次对象的直接耦合,层次对象的对外依赖通信统统交由中介者转发。

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  • 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

7.1 代码实现

通过一个租房例子简单实现下逻辑,房主通过中介公司发布自己的房子的信息,而租客则需要通过中介公司获取到房子的信息:

java 复制代码
// 抽象同事类
@AllArgsConstructor
public class Person {
    protected String name;
    protected MediatorCompany mediatorCompany;
}

// 房主
public class HouseOwner extends Person {
    public HouseOwner(String name, MediatorCompany mediatorCompany) {
        super(name, mediatorCompany);
    }
    // 联络方法
    public void connection(String message) {
        mediatorCompany.connection(this, message);
    }
    // 获取消息
    public void getMessage(String message) {
        System.out.println("房主" + name + "获取到的信息:" + message);
    }
}

// 租客
public class Tenant extends Person {
    public Tenant(String name, MediatorCompany mediatorCompany) {
        super(name, mediatorCompany);
    }
    public void connection(String message) {
        mediatorCompany.connection(this, message);
    }
    public void getMessage(String message) {
        System.out.println("租客" + name + "获取到的信息:" + message);
    }
}

// 中介公司(中介者)
@Data
public class MediatorCompany {

    private HouseOwner houseOwner;
    private Tenant tenant;
    
    public void connection(Person person, String message) {
        // 房主需要通过中介获取租客信息
        if (person.equals(houseOwner)) {
            this.tenant.getMessage(message);
        } else { // 反之租客通过中介获取房主信息
            this.houseOwner.getMessage(message);
        }
    }
}

// 测试
public static void main(String[] args){
    // 先创建三个角色,中介公司,房主,租客
    MediatorCompany mediatorCompany = new MediatorCompany();
    // 房主和租客都在同一家中介公司
    HouseOwner houseOwner = new HouseOwner("张三", mediatorCompany);
    Tenant tenant = new Tenant("李四", mediatorCompany);
    // 中介公司获取房主和租客的信息
    mediatorCompany.setHouseOwner(houseOwner);
    mediatorCompany.setTenant(tenant);
    // 房主和租客都在这家中介公司发布消息,获取到对应的消息
    tenant.connection(tenant.name + "想租一房一厅!");
    houseOwner.connection(houseOwner.name + "这里有!来看看呗!");
    // 测试结果
    // 房主张三获取到的信息:李四想租一房一厅!
    // 租客李四获取到的信息:张三这里有!来看看呗!
}

7.2 总结

适用场景:

  • 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。
  • 交互的公共行为,如果需要改变行为则可以增加新的中介者类。

优点:

  • 减少类间的依赖,将多对多依赖转化成了一对多,降低了类间耦合。
  • 类间各司其职,符合迪米特法则。

缺点:

  • 中介者模式中将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。

8.迭代器模式(Iterator Pattern)

迭代器模式又称为游标模式(Cursor Pattern),它提供一种顺序访问集合/容器对象元素的方法,而又无须暴露结合内部表示。

本质:抽离集合对象迭代行为到迭代器中,提供一致访问接口。

迭代器模式主要包含以下角色:

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

8.1 代码实现

java 复制代码
// 迭代器接口
public interface Iterator<T> {
    Boolean hasNext();
    T next();
}

// 迭代器接口实现类
public class IteratorImpl<T> implements Iterator<T> {
    private List<T> list;
    private Integer cursor;
    private T element;

    public IteratorImpl(List<T> list) {
        this.list = list;
    }
    @Override
    public Boolean hasNext() {
        return cursor < list.size();
    }
    @Override
    public T next() {
        element = list.get(cursor);
        cursor++;
        return element;
    }
}

// 容器接口
public interface Aggregate<T> {
    void add(T t);
    void remove(T t);
    Iterator<T> iterator();
}

// 容器接口实现类
public class AggregateImpl<T> implements Aggregate<T> {
    private List<T> list = new ArrayList<>();
    @Override
    public void add(T t) {
        list.add(t);
    }
    @Override
    public void remove(T t) {
        list.remove(t);
    }
    @Override
    public Iterator<T> iterator() {
        return new IteratorImpl<>(list);
    }
}

PS:具体测试的话可以自己写一个集合测试一下即可

8.2 总结

适用场景:

  • 访问一个集合对象的内容而无需暴露它的内部表示。
  • 为遍历不同的集合结构提供一个统一的访问接口。

优点:

  • 多态迭代:为不同的聚合结构提供一致的遍历接口,即一个迭代接口可以访问不同的聚集对象。
  • 简化集合对象接口:迭代器模式将集合对象本身应该提供的元素迭代接口抽取到了迭代器中,使集合对象无须关心具体迭代行为。
  • 元素迭代功能多样化:每个集合对象都可以提供一个或多个不同的迭代器,使的同种元素聚合结构可以有不同的迭代行为。
  • 解耦迭代与集合:迭代器模式封装了具体的迭代算法,迭代算法的变化,不会影响到集合对象的架构。

缺点:

  • 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐。
  • 增加了类的个数,在一定程度上增加了系统的复杂性。

9.访问者模式(Visitor Pattern)

访问者模式是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作。

特征:可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

访问者模式包含以下主要角色:

  • 抽象访问者(Visitor)角色:定义了对每一个元素 (Element) 访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
  • 抽象元素(Element)角色:定义了一个接受访问者的方法( accept ),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素( Element ),并且可以迭代这些元素,供访问者访问。

9.1 代码实现

java 复制代码
// 访问者接口
public interface IVisitor {
    void visit(Engineer engineer);
    void visit(Pm pm);
}
// 具体的访问者类,访问者角色(CEO)
public class CeoVisitor implements IVisitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println(engineer.getName() + "KPI为:" + engineer.getKpi());
    }
    @Override
    public void visit(Pm pm) {
        System.out.println(pm.getName() + "KPI为:" + pm.getKpi());
    }
}
// 具体的访问者类,访问者角色(CTO)
public class CtoVisitor implements IVisitor {
    @Override
    public void visit(Engineer engineer) {
        System.out.println(engineer.getName() + "工作内容:" + engineer.getCodeLine() + "行代码");
    }
    @Override
    public void visit(Pm pm) {
        System.out.println(pm.getName() + "工作内容:" + pm.getProject() + "个项目");
    }
}
@Data
// 抽象元素(员工)
public abstract class Employee {
    private String name;
    private Integer kpi;
    public Employee(String name) {
        this.name = name;
        this.kpi = new Random().nextInt(10);
    }
    public abstract void accept(IVisitor visitor);
}
// 具体元素(程序员)
public class Engineer extends Employee {
    public Engineer(String name) {
        super(name);
    }
    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    public Integer getCodeLine() {
        return new Random().nextInt(10000);
    }
}
// 具体元素(项目经理)
public class Pm extends Employee {
    public Pm(String name) {
        super(name);
    }
    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
    public Integer getProject() {
        return new Random().nextInt(10);
    }
}
@AllArgsConstructor
public class Report {
    private List<Employee> employeeList;
    public void showReport(IVisitor visitor) {
        for (Employee employee : employeeList) {
            employee.accept(visitor);
        }
    }
}

// 测试
public static void main(String[] args){
  List<Employee> employeeList = new ArrayList<>();
  employeeList.add(new Engineer("工程师A"));
  employeeList.add(new Engineer("工程师B"));
  employeeList.add(new Engineer("项目经理A"));
  employeeList.add(new Engineer("工程师C"));
  employeeList.add(new Engineer("工程师D"));
  employeeList.add(new Engineer("项目经理B"));
  Report report = new Report(employeeList);
  System.out.println("=============CEO==============");
  report.showReport(new CeoVisitor());
  System.out.println("=============CTO==============");
  report.showReport(new CtoVisitor());
  // =============CEO==============
  // 工程师AKPI为:2
  // 工程师BKPI为:4
  // 项目经理AKPI为:4
  // 工程师CKPI为:2
  // 工程师DKPI为:0
  // 项目经理BKPI为:0
  // =============CTO==============
  // 工程师A工作内容:5811行代码
  // 工程师B工作内容:9930行代码
  // 项目经理A工作内容:2163行代码
  // 工程师C工作内容:4591行代码
  // 工程师D工作内容:333行代码
  // 项目经理B工作内容:3940行代码
}

9.2 伪动态双分派

访问者模式用到了一种伪动态双分派的技术。

9.2.1 分派

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如Map map = new

HashMap() ,map变量的静态类型是Map,实际类型是 HashMap

。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

  • 静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
  • 动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

9.2.2 伪动态双分派

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

在上面代码中,客户端将IVisitor接口做为参数传递给Employee抽象类的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派

,这里的IVisitor接口中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。

9.3 总结

适用场景:

  • 数据结构稳定,作用于数据结构的操作经常变化的场景。
  • 需要数据结构与数据操作分离的场景。
  • 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。

优点:

  • 解耦了数据结构与数据操作,使得操作集合可以独立变化。
  • 扩展性好:可以通过扩展访问者角色,实现对数据集的不同操作。
  • 元素具体类型并非单一,访问者均可操作。
  • 各角色职责分离,符合单一职责原则。

缺点:

  • 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则。
  • 具体元素变更困难:具体元素增加属性,删除属性等操作会导致对应的访问者类需要进行相应的修改,尤其当有大量访问者类时,修改访问太大。
  • 违背依赖倒置原则:为了达到"区别对待",访问者依赖的是具体元素类型,而不是抽象。

10.备忘录模式(Memento Pattern)

备忘录模式又称为快照模式(Snapshot Pattern)或令牌模式(TokenPattern),是指在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。

特征:"后悔药"

备忘录模式的主要角色如下:

  • 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。

备忘录有两个等效的接口:

  • 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

10.1 "白箱"备忘录模式

下面就以游戏打怪为简单的例子进行代码实现(下面"黑箱"同这个例子):

备忘录角色对任何对象都提供一个宽接口,备忘录角色的内部所存储的状态就对所有对象公开。

java 复制代码
// 游戏角色类
@Data
public class GameRole {
    private Integer vit; // 生命力
    private Integer atk; // 攻击力
    private Integer def; // 防御力
    // 初始化状态
    public void init() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }
    // 战斗到0
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }
    // 保存角色状态
    public RoleStateMemento saveState() {
        return new RoleStateMemento(this.vit, this.atk, this.def);
    }
    // 回复角色状态
    public void recoverState(RoleStateMemento roleStateMemento) {
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }
    // 展示状态
    public void showState() {
        System.out.println("角色生命力:" + this.vit);
        System.out.println("角色攻击力:" + this.atk);
        System.out.println("角色防御力:" + this.def);
    }
}
// 游戏状态存储类(备忘录类)
@Data
@AllArgsConstructor
public class RoleStateMemento {
    private Integer vit; // 生命力
    private Integer atk; // 攻击力
    private Integer def; // 防御力
}
// 角色状态管理者类
@Data
public class RoleStateCaretaker {
    private RoleStateMemento roleStateMemento;
}
// 测试结果
public static void main(String[] args){
  System.out.println("===========打boss前状态===========");
  GameRole gameRole = new GameRole();
  gameRole.init();
  gameRole.showState();
  // 保存进度
  RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
  roleStateCaretaker.setRoleStateMemento(gameRole.saveState());
  System.out.println("===========打boss后状态===========");
  gameRole.fight();
  gameRole.showState();
  System.out.println("===========恢复状态===========");
  gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
  gameRole.showState();
  // ===========打boss前状态===========
  // 角色生命力:100
  // 角色攻击力:100
  // 角色防御力:100
  // ===========打boss后状态===========
  // 角色生命力:0
  // 角色攻击力:0
  // 角色防御力:0
  // ===========恢复状态===========
  // 角色生命力:100
  // 角色攻击力:100
  // 角色防御力:100
}

"白箱"备忘录模式是破坏封装性的,但是通过程序员自律,同样可以在一定程度上实现大部分的用意。

10.2 "黑箱"备忘录模式

备忘录角色对发起人对象提供了一个宽接口,而为其他对象提供一个窄接口,在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。

将RoleStateMemento设为GameRole的内部类,从而将RoleStateMemento对象封装在GameRole

里面;在外面提供一个标识接口Memento给RoleStateCaretaker及其他对象使用。这样GameRole类看到的是RoleStateMemento所有的接口,而RoleStateCaretaker及其他对象看到的仅仅是标识接口Memento所暴露出来的接口,从而维护了封装型。

java 复制代码
// 窄接口,标识接口
public interface Memento {
}
// 角色状态管理者类
@Data
public class RoleStateCaretaker {
    private Memento memento;
}
// 游戏角色类
@Data
public class GameRole {
    private Integer vit; // 生命力
    private Integer atk; // 攻击力
    private Integer def; // 防御力
    // 初始化状态
    public void init() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }
    // 战斗到0
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }
    // 保存角色状态
    public RoleStateMemento saveState() {
        return new RoleStateMemento(this.vit, this.atk, this.def);
    }
    // 回复角色状态
    public void recoverState(Memento memento) {
        RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }
    // 展示状态
    public void showState() {
        System.out.println("角色生命力:" + this.vit);
        System.out.println("角色攻击力:" + this.atk);
        System.out.println("角色防御力:" + this.def);
    }
    // 备忘录内部类
    @Data
    @AllArgsConstructor
    private class RoleStateMemento implements Memento {
        private Integer vit; // 生命力
        private Integer atk; // 攻击力
        private Integer def; // 防御力
    }
}
// 测试结果
public static void main(String[] args){
  System.out.println("===========打boss前状态===========");
  GameRole gameRole = new GameRole();
  gameRole.init();
  gameRole.showState();
  // 保存进度
  RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
  roleStateCaretaker.setMemento(gameRole.saveState());
  System.out.println("===========打boss后状态===========");
  gameRole.fight();
  gameRole.showState();
  System.out.println("===========恢复状态===========");
  gameRole.recoverState(roleStateCaretaker.getMemento());
  gameRole.showState();
  // ===========打boss前状态===========
  // 角色生命力:100
  // 角色攻击力:100
  // 角色防御力:100
  // ===========打boss后状态===========
  // 角色生命力:0
  // 角色攻击力:0
  // 角色防御力:0
  // ===========恢复状态===========
  // 角色生命力:100
  // 角色攻击力:100
  // 角色防御力:100
}

10.3 总结

适用场景:

  • 需要保存历史快照的场景。
  • 希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容。

优点:

  • 简化发起人实体类职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节。
  • 提供状态回滚功能。

缺点:

  • 消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存。

11.解释器模式(interpreter pattern)

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

特征:为了解释一种语言,而为语言创建的解释器。

解释器模式包含以下主要角色:

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

11.1 代码实现

下面以简单的加减乘除为例子实现解释器模式:

java 复制代码
// 抽象角色 定义解释器
public interface Expression {
    int interpret();
}
@AllArgsConstructor
public class NumberTerminal implements Expression {
    private int number;
    @Override
    public int interpret() {
        return this.number;
    }
}
// 非终结表达式(抽象类)
@AllArgsConstructor
public abstract class NonTerminal implements Expression {
    protected Expression left;
    protected Expression right;
}
// 非终结表达式(加法)
public class PlusNonTerminal extends NonTerminal implements Expression {
    public PlusNonTerminal(Expression left, Expression right) {
        super(left, right);
    }
    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}
// 非终结表达式(减法)
public class MinusNonTerminal extends NonTerminal implements Expression {
    public MinusNonTerminal(Expression left, Expression right) {
        super(left, right);
    }
    @Override
    public int interpret() {
        return left.interpret() - right.interpret();
    }
}
// 非终结表达式(乘法)
public class MclNonTerminal extends NonTerminal implements Expression {
    public MclNonTerminal(Expression left, Expression right) {
        super(left, right);
    }
    @Override
    public int interpret() {
        return left.interpret() * right.interpret();
    }
}
// 非终结表达式(除法)
public class DivisionNonTerminal extends NonTerminal implements Expression {
    public DivisionNonTerminal(Expression left, Expression right) {
        super(left, right);
    }
    @Override
    public int interpret() {
        return left.interpret() / right.interpret();
    }
}
// 计算器类(实现运算逻辑)
public class Cal {
    private Expression left;
    private Expression right;
    private Integer result;
    public Cal(String expression) {
        this.parse(expression);
    }
    private Integer parse(String expression) {
        // 获取表达式元素
        String [] elements = expression.split(" ");
        for (int i = 0; i < elements.length; i++) {
            String element = elements[i];
            // 判断是否是运算符号
            if (OperatorUtils.isOperator(element)) {
                // 运算符号的右边就是右终结符
                right = new NumberTerminal(Integer.valueOf(elements[++i]));
                //计算结果
                result = OperatorUtils.getNonTerminal(left, right, element).interpret();
                // 计算结果重新成为左终结符
                left = new NumberTerminal(result);
            } else {
                left = new NumberTerminal(Integer.valueOf(element));
            }
        }
        return result;
    }
    public Integer cal() {
        return result;
    }

}
// 操作工具类
public class OperatorUtils {
    // 判断是不是非终结符
    public static boolean isOperator(String symbol) {
        return symbol.equals("+") || symbol.equals("-") || symbol.equals("*")|| symbol.equals("/");
    }
    // 简单工厂
    public static NonTerminal getNonTerminal(Expression left, Expression right, String symbol) {
        if (symbol.equals("+")) {
            return new PlusNonTerminal(left, right);
        } else if (symbol.equals("-")) {
            return new MinusNonTerminal(left, right);
        } else if (symbol.equals("*")) {
            return new MclNonTerminal(left, right);
        } else if (symbol.equals("/")) {
            return new DivisionNonTerminal(left, right);
        }
        return null;
    }
}
// 测试
// PS:此处进行的逻辑仅仅实现从左到右运算,并没有先乘除后加减的逻辑
public static void main(String[] args) {
    System.out.println(new Cal("10 + 20 - 40 * 60").cal()); // -600
    System.out.println(new Cal("20 + 50 - 60 * 2").cal()); // 20
}

11.2 Spring中的解释器模式

java 复制代码
public static void main(String[] args) {
    ExpressionParser expressionParser = new SpelExpressionParser();
    org.springframework.expression.Expression expression = expressionParser.parseExpression("10 + 20 + 30 * 4");
    Integer value = expression.getValue(Integer.class);
    System.out.println(value); // 150
    expression = expressionParser.parseExpression("(10+20+30)*4");
    value = expression.getValue(Integer.class);
    System.out.println(value); // 240
}

可以看到Spring中解释器写的是比较完善的,不仅有先乘除后加减和先括号进行运算的日常计算规则,而且对于空格也并没有要求,仅需要写出完整的表达式即可运算出来。

11.3 总结

适用场景:

  • 一些重复出现的问题可以用一种简单的语言来进行表述。
  • 一个简单语法需要解释的场景。

优点:

  • 扩展性强:在解释器模式中由于语法是由很多类表示的,当语法规则更改时,只需修改相应的非终结符表达式即可;若扩展语法时,只需添加相应非终结符类即可。
  • 增加了新的解释表达式的方式。
  • 易于实现文法:解释器模式对应的文法应当是比较简单且易于实现的,过于复杂的语法并不适合使用解释器模式。

缺点:

  • 语法规则较复杂时,会引起类膨胀。
  • 执行效率比较低
相关推荐
程序员与背包客_CoderZ5 小时前
C++设计模式——Abstract Factory Pattern抽象工厂模式
c语言·开发语言·c++·设计模式·抽象工厂模式
zzzhpzhpzzz5 小时前
设计模式——组合实体模式
设计模式
zzzhpzhpzzz8 小时前
设计模式——前端控制器模式
设计模式
forestsea9 小时前
【Java 解释器模式】实现高扩展性的医学专家诊断规则引擎
java·人工智能·设计模式·解释器模式
小白不太白95011 小时前
设计模式之 命令模式
设计模式·命令模式
吃汉堡吃到饱11 小时前
【创建型设计模式】单例模式
单例模式·设计模式
小白不太白95011 小时前
设计模式之 备忘录模式
服务器·设计模式·备忘录模式
zzzhpzhpzzz11 小时前
设计模式——策略模式
设计模式·策略模式
入门到跑路11 小时前
【君正T31开发记录】8.了解rtsp协议及设计模式
网络协议·设计模式
小白不太白95011 小时前
设计模式之 解释器模式
java·设计模式·解释器模式