Java中23种设计模式(随时不定时更新)

一、创建型模式

1.单例模式(Singleton Pattern)

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

1.1 饿汉式

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

        // 构造方法私有化
	private Singleton() {
		
	}
 
	// 饿汉式创建单例对象
	private static Singleton singleton = new Singleton();
 
	public static Singleton getInstance() {
		return singleton;
	}

1.2 懒汉式

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

	/*
	 * 懒汉式创建单例模式 由于懒汉式是非线程安全, 所以加上线程锁保证线程安全
	 */
	private static Singleton singleton;
 
	public static synchronized Singleton getInstance() {
		if (singleton == null) {
			singleton = new Singleton();
		}
		return singleton;
	}

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

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

    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,只适用于静态域

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

1.5 枚举

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

public enum Singleton {
    INSTANCE;
}

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

1.6.1 反序列化
            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()这个方法。

private Object readResolve() {
        return singleton;
    }
//com.ruoyi.base.mapper.Singleton@50134894
//com.ruoyi.base.mapper.Singleton@50134894
1.6.2 反射
            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

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

private static boolean singletonFlag = false;

    private Singleton() {
        if (singleton != null || singletonFlag) {
            throw new RuntimeException("试图用反射破坏异常");
        }
        singletonFlag = true;
    }

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

            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("singletonFlag");
            singletonFlag.setAccessible(true);
            singletonFlag.setBoolean(singleton, false);
            Singleton singleton1 = constructor.newInstance();
            System.out.println(singleton1); // com.ruoyi.base.mapper.Singleton@5e8c92f4

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

1.7 容器式单例

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

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单例

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

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他们的对象是不一样的,但是线程内部,他们的对象是一样的,这就是线程内部保证唯一。

1.9 总结

适用场景:

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

优点:

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

缺点:

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

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

2.1 简单工厂模式

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

2.1.1 基础版
// 工厂类
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 升级版
    // 使用反射创建对象
    // 加一个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):实现了抽象产品角色所定义的接口,由具体工厂来创建,它和具体工厂之间一一对应。
// 抽象工厂
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 {
}

总结:

适用场景:

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

优点:

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

缺点:

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

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

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

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

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

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

总结:

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

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

适用场景:

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

优点:

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

缺点:

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

4.原型模式(Prototype)

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

原型模式包含如下角色:

  • 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
@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 浅克隆

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

@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 深克隆

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

@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:上面例子有的代码,这里便不重复写了,可以在上面的代码基础上添加以下代码

// 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()方法即可,如下:

    @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 常规写法

//产品类 电脑
@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 简化写法

// 把指挥者类和抽象建造者合在一起的简化建造者类
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 链式写法

// 链式写法建造者类
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 静态代理

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

// 火车站接口,有卖票功能
public interface TrainStation {
    void sellTickets();
}
// 广州火车站卖票
public class GuangzhouTrainStation implements TrainStation {
    @Override
    public void sellTickets() {
        System.out.println("广州火车站卖票啦");
    }
}
// 代售点卖票(代理类)
public class ProxyPoint implements TrainStation {
    // 目标对象(代理火车站售票)
    private GuangzhouTrainStation station = new GuangzhouTrainStation();
    @Override
    public void sellTickets() {
        System.out.println("代售加收5%手续费");
        station.sellTickets();
    }
    public static void main(String[] args) {
        ProxyPoint proxyPoint = new ProxyPoint();
        // 代售加收5%手续费
        // 广州火车站卖票啦
        proxyPoint.sellTickets();
    }
}
    // 测试
    public static void main(String[] args) {
        ProxyPoint proxyPoint = new ProxyPoint();
        // 代售加收5%手续费
        // 火车站卖票啦
        proxyPoint.sellTickets();
    }

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

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

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

1.2 动态代理

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

1.2.1 jdk动态代理

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

// 火车站接口,有卖票功能
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());
        // 代售火车票收取5%手续费
        // 广州火车站卖票啦
        guangzhouTrainStation.sellTickets();
        TrainStation shenzhenTrainStation = proxy.getProxyObject(new ShenzhenTrainStation());
        // 代售火车票收取5%手续费
        // 深圳火车站卖票啦
        shenzhenTrainStation.sellTickets();
    }

优点:

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

缺点:

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

CGLIB是一个强大的、高性能的代码生成库。它可以在运行期扩展Java类和接口,其被广泛应用于AOP框架中(Spring、dynaop)中, 用以提供方法拦截。CGLIB比JDK动态代理更强的地方在于它不仅可以接管Java接口, 还可以接管普通类的方法。

        <!-- 先引入cglib包 -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>${cglib-version}</version>
        </dependency>
// 代售点卖票(代理类)
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);
        // 代售火车票收取5%手续费
        // 广州火车站卖票啦
        guangzhouTrainStation.sellTickets();
        TrainStation shenzhenTrainStation = proxy.getProxyObject(ShenzhenTrainStation.class);
        // 代售火车票收取5%手续费
        // 深圳火车站卖票啦
        shenzhenTrainStation.sellTickets();
    }

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 类适配器

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

类图
// 适配者 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 对象适配器

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

// 电源接口
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…这些转换方法,通通都写到一个接口里去,这样当我们需要转换哪种就直接调用哪种即可。

// 这里例子 输出不同直流电接口
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;
    }
}

总结

适用场景:

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

优点:

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

缺点:

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

3.装饰模式(Decorator Pattern)

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

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

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

总结

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

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

适用场景:

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

优点:

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

缺点:

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

4.桥接模式(Bridge Pattern)

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

5.外观模式(Facade)

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

6.组合模式(Composite Pattern)

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

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

  • 组合模式主要包含三种角色:
    抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

总结

适用场景:

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

优点:

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

缺点:

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

7.享元模式(Flyweight Pattern)

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

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

三、行为型模式

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

2.策略模式(Strategy Pattern)

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

3.命令模式(Command Pattern)

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

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

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

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

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

总结

适用场景:

  • 多个对象可以处理同一请求,但具体由哪个对象处理则在运行时动态决定。
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
  • 可动态指定一组对象处理请求。

优点:

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

缺点:

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

5.状态模式(State Pattern)

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

6.观察者模式(Observer Mode)

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

总结

适用场景:

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

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

优点:

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

缺点:

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

7.中介者模式(mediator pattern)

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

8.迭代器模式(Iterator Pattern)

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

9.访问者模式(Visitor Pattern)

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

10.备忘录模式(Memento Pattern)

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

特征:“后悔药”

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

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

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

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

总结

适用场景:

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

优点:

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

缺点:

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

11.解释器模式(interpreter pattern)

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

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

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

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

11.1 代码实现

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

// 抽象角色 定义解释器
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中的解释器模式

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 总结

适用场景:

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

优点:

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

缺点:

  • 语法规则较复杂时,会引起类膨胀。
  • 执行效率比较低。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇