设计模式笔记

206

这个并非教程, 是我学习设计模式的时候记下的笔记, 有很多写得比较简单

此笔记只能用作简单参考, 详细应该看书和权威资料

参考内容:

设计原则:

  1. 开闭原则: 对扩展开放, 对修改关闭

  2. 接口隔离原则: 类之间的依赖关系应该建立在最小的接口上

  3. 合成 / 聚合原则: 则优先使用组合而不是继承 (HAS-A 好于 IS-A).

  4. 依赖倒置: 依赖抽象, 不依赖具体类

    • 变量不应该持有到具体类的引用
    • 类不应该派生自具体类
    • 方法不应该覆盖其任何基类的已实现方法
  5. 最少知识原则: 一个软件实体应当尽可能少的与其他实体发生相互作用, 将交互的类的交互方式统一成一个方法

  6. 单一责任原则: 一个类只负责一个功能领域中的相应职责

  7. 里氏代换原则: 所有引用基类的地方必须能透明地使用其子类的对象

设计模式:

此处一共 23 种设计模式, 并不完全

创建型模式: 工厂方法, 抽象工厂, 生成器, 原型, 单例

结构型模式: 适配器, 桥接, 组合, 装饰, 外观, 享元, 代理

行为模式: 责任链, 命令, 迭代器, 中介者, 备忘录, 观察者, 状态, 策略, 模板方法, 访问者, 解释器

策略模式 (Strategy):

对于一个对象如果有多种子类且子类的功能都不一定相同时, 抽离变化的部分成接口, 不变的部分继承下来, 变化的部门以成员变量的形式组装进子类中. 比如鸭子父类, 子类有模型鸭, 黑鸭, 黄鸭, 这些鸭子都不一定会飞而且飞行的方式不一样, 此时抽离 "飞行" 的行为为一个接口, 让所有飞行的行为实现这个接口 (用生物翅膀飞, 赛博起飞), 根据每一种鸭子子类的需要, 把这些行为组装进去.

duck

这样的设计就可以随时改变每个鸭子的行为 (比如某只鸭子翅膀受伤, 或者某只模型鸭得到强化装载火箭升空模块), 而不是像继承那样从编译时就决定了

使用场景:

  • 当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时
  • 当你有许多仅在执行某些行为时略有不同的相似类时
  • 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来
  • 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时

优缺点:

优点缺点
你可以在运行时切换对象内的算法如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口. 使用该模式只会让程序过于复杂
你可以将算法的实现和使用算法的代码隔离开来客户端必须知晓策略间的不同——它需要选择合适的策略
你可以使用组合来代替继承许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法. 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁
开闭原则. 你无需对上下文进行修改就能够引入新的策略

观察者模式 (Observer):

观察者模式分为观察者对象和主题对象, 主题对象管理数据, 一旦主题对象的数据改变立即通知关注此主题的观察者.

对于观察者模式, 现实中类似的例子是视频订阅. 当你和你的朋友订阅了一个频道主, 频道主更新视频就会向你发来推送 --- "xxx 已更新", 这时你就会去看他的最新发布的视频. 这就是一个观察者模式, 频道主为主题对象, 你和你的朋友为观察者对象, 你们同时订阅了这个主题, 当这个主题有更新时就会通知所有的订阅者 (观察者)

做法是在主题对象中用一个容器保存所有关注此主题的观察者对象, 在主题对象进行了一些操作时遍历这个容器通知所有观察者.

以下是一个气象站的设计, StatisticsDisplay 用于实时显示各项数据最小/最大/平均值, CurrentConditinosDisplay 用于显示当前的值

observer

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay();
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay();
        weatherData.registerObserver(statisticsDisplay);
        weatherData.registerObserver(currentConditionsDisplay);
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

当主题的数据变化时调用 notifyObservers 通知所有关注此主题的观察者 (调用所有观察者的 update 方法), 观察者的 update 方法立即做相应数据更新或其他操作 (例如调用 display 展示更新的信息)

对于观察者模式中, 必须使用一对多: 一个主题对应多个观察者

使用场景:

  • 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式
  • 当应用中的一些对象必须观察其他对象时, 可使用该模式. 但仅能在有限时间内或特定情况下使用

优缺点:

优点缺点
开闭原则. 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)订阅者的通知顺序不能确定
你可以在运行时建立对象之间的联系

装饰者模式 (Decorator):

装饰者模式中, 就像人一样, 穿一件衬衫再穿一件貂皮大衣, 手上带上金戒指.... 这样一直装饰下去你的身价就显得越来越高, 腰子或许也卖得更值钱, 或者是一把游戏中的 m4, 本身后坐力比较大, 但是可以不断给他装备配件: 枪托, 枪口, 瞄准镜, 握把, 这样就可以让这把枪更加稳定 (这里其实是更复杂的设计, 因为涉及装备/拆卸等)

装饰者模式的初衷是添加 "行为" 到对象, 需要窥探装饰者链条的层次时就超出了装饰者模式的范围

案例是一个咖啡店卖咖啡, 比如某幸, 某幸目前的咖啡种类我们简化为三个: 拿铁, 美式, 其他. 某幸的拿铁中会有不同的拿铁, 比如生椰拿铁, 丝绒拿铁, 每种咖啡的价格都不一样, 原料和辅料也不尽相同. 这时如果完全用继承的思想来做, 会出现类爆炸 (类的数量过多), 陷入维护地狱, 解决方案正是装饰者模式.

不管是xx拿铁还是什么咖啡我们假设就是从一杯基础咖啡加上各种调味料做出来的, 调味料可能会有牛奶, 糖, 所有咖啡都必须加且量一样的东西我们用继承来实现复用, 其他的例如加冰, 加糖, 加茅台, 加牛奶, 我们就可以用装饰者来实现.

做法是将饮料父类做成抽象类, 将所有可能会变化的方法做成抽象方法 (比如返回这杯咖啡的价格的方法), 所有的咖啡基类继承自抽象饮料父类. 创建一个调味料抽象装饰类继承咖啡类 (这里继承的目的是为了使用多态), 在这个类中包含一个咖啡对象, 并且以唯一有参构造构入, 让糖, 牛奶等等继承这个抽象装饰类, 在例如返回价格的方法中, 将自身价格加上装饰类中封装的咖啡对象的价格返回, 这样就可以直接得到最终成品的价格.

decorator

public class CoffeeStoreApplication {
    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + " " + beverage.cost());
        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        beverage2 = new Whip(beverage2);
        System.out.println(beverage2.getDescription() + " " + beverage2.cost());
        Beverage beverage3 = new HouseBlend();
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Whip(beverage3);
        String description = beverage3.getDescription();
        System.out.println(description + " " + beverage3.cost());
    }
}

这样设计之后, 不管某幸出哪种新咖啡, 都可以不用修改原来的代码, 直接通过调味料装饰者不断装饰得到, 即使是出了类似酱香拿铁的咖啡, 只需要新增茅台调味料装饰者就可以了 (需结合工厂模式)

使用场景:

  • 如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为
  • 如果用继承来扩展对象行为的方案难以实现或者根本不可行

优缺点:

优点缺点
你无需创建新子类即可扩展对象的行为在封装器栈中删除特定封装器比较困难
你可以在运行时添加或删除对象的功能实现行为不受装饰栈顺序影响的装饰比较困难
你可以用多个装饰封装对象来组合几种行为各层的初始化配置代码看上去可能会很糟糕
单一职责原则. 你可以将实现了许多不同行为的一个大类拆分为多个较小的类

工厂模式 (Factory):

工厂的出现是为了解决对象的创建问题和解耦合, 例如在装饰者模式中, 你需要不断的 new 来装饰 new 出来的对象, 每次都这样做会比较麻烦而且容器出错. 这时就可以用一个工厂来代替, 我们把类型传给工厂, 工厂制作出相应的产品, 并且我们并不需要关心其内部是如何创建对象的.

简单工厂:

简单工厂其实不是一个设计模式, 而是一种编程习惯

比如一个咖啡店的前台, 收到单点信息后, 并不会关心是哪种咖啡, 直接告诉后厨咖啡名字, 后厨做好之后拿到前台打包....

可以直接上代码:

public class SimpleCoffeeFactory {
    public Coffee create(String type) {
        switch (type) {
            case "latte" -> {
                return new LatteCoffee();
            }
            case "puree" -> {
                return new PureeCoffee();
            }
            case "sauce" -> {
                return new SauceCoffee();
            }
            default -> {
                return null;
            }
        }
    }
}

如果直接在点单的方法中判断是何种咖啡, 随着咖啡的种类增删你会不断的修改点单方法中有关 "制作" 咖啡的代码块, 这听起来就很诡异, 所以我们需要一个工厂来委托制作咖啡

工厂方法 (Factory Method):

定义了一个创建对象的接口, 但由子类决定要实例化哪个类

工厂方法让类的实例化推迟到子类

把创建对象的方法做成抽象方法, 让每个工厂子类去实现这个方法, 这样做的目的是让每个产品有一个对应的工厂, 当每个产品都有了一个自己的工厂之后, 需要创建产品是只需要知道产品名称即可, 这样可以进一步解耦, 在新增产品时只需要再新增一个对应工厂就可以. 相对于简单工厂, 工厂方法符合开闭原则

使用场景:
  • 当你在编写代码的过程中, 如果无法预知对象确切类别及其依赖关系时
  • 工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码
  • 如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象可使用工厂方法
优缺点:
优点缺点
你可以避免创建者和具体产品之间的紧密耦合应用工厂方法模式需要引入许多新的子类, 代码可能会因此变得更复杂. 最好的情况是将该模式引入创建者类的现有层次结构中
单一职责原则. 你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护
开闭原则. 无需更改现有客户端代码, 你就可以在程序中引入新的产品类型

抽象工厂 (Abstract Factory):

提供一个接口来创建相关或依赖对象的家族, 而不需要指定具体类

抽象工厂中引入了一个产品族的概念, 在抽象工厂的定义中, 一个抽象工厂生产多个产品族的产品

public interface Factory {
    Phone createPhone();

    HeadPhone createHeadPhone();
    
    /**
     * Phone 和 HeadPhone 都是代表一个产品族
     */
}

实际工厂实现自上面的接口, 表现形式类似工厂方法, 最后创建一个类来用于选择工厂, 或者将工厂作为像策略模式一样的类属性保存起来, 通过构造器初始化

public abstract class AbstractDigitalStore {
    protected Factory factory;

    public AbstractDigitalStore(Factory factory) {
        this.factory = factory;
    }
}
public class AppleStore extends AbstractDigitalStore implements DigitalStore {
    public AppleStore() {
        super(new AppleFactory());
    }

    public AppleStore(Factory factory) {
        super(factory);
    }

    public Phone salePhone() {
        return factory.createPhone();
    }

    public HeadPhone saleHeadPhone() {
        return factory.createHeadPhone();
    }
}

使用场景:
  • 如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建
  • 如果你有一个基于一组抽象方法的类, 且其主要功能因此变得不明确
优缺点:
优点缺点
你可以确保同一工厂生成的产品相互匹配由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂
你可以避免客户端和具体产品代码的耦合
单一职责原则. 你可以将产品生成代码抽取到同一位置, 使得代码易于维护
开闭原则. 向应用程序中引入新产品变体时, 你无需修改客户端代码

单例模式 (Singleton):

确保一个类只有一个实例, 并提供一个全局访问点

为什么不把变量和方法都定义成静态?

静态成员的初始化顺序由 JVM 决定, 不确定因素很多, 可能会出现 bug

单例模式一般分为两种, 提前创建好对象和延迟创建对象 (懒加载)

枚举单例和正确的内部类单例是懒加载且线程安全

饱汉式:

提前创建对象

不会出现线程安全的问题

public class FullSingleton {
    private static final FullSingleton INSTANCE = new FullSingleton();
    
    private FullSingleton() {
        
    }

    public static FullSingleton getINSTANCE() {
        return INSTANCE;
    }
}

饿汉式:

延迟创建对象

因为显示使用 new 关键字的缘故, 所以会出现线程安全的问题, 需加锁避免

public class HungrySingleton {
    private static volatile HungrySingleton INSTANCE;

    private HungrySingleton() {

    }

    // 线程安全
    public static HungrySingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (HungrySingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new HungrySingleton();
                }
            }
        }
        return INSTANCE;
    }
}

饿汉式必须解决的是线程安全问题

使用双重检查锁是为了优化性能, 这样只有在第一次创建对象时才会进行同步, 以后都不需要进行同步了

对于以上加锁方式的详解:

虽然是写设计模式笔记, 但还是记一下相关知识

voliatile 关键字是用于禁止指令重排序, 如果不禁止, 通过一系列优化后, cpu 执行的顺序可能会变为: A 线程先分配内存将对象指向刚分配的内存空间再实例化对象. 这样就可能导致 B 线程使用的对象是一个没有进行初始化的对象

double-check-lock 的目的是为了优化性能, 而不是增加安全性

对于加锁的粒度:

上面的代码在 synchronized 同步块中是以 HungrySingleton.class 字节码加锁

  • 字节码锁

    以字节码加锁的方法或者代码块的锁是字节码时, 只要是这个类产生的对象, 在执行同步的代码时都会产生互斥

    也就是说有如果一个类有两个对象 M, N, A 线程 和 B 线程分别访问 M, N 对象的以字节码加锁的代码, 这时会互斥

  • 对象锁

    可以理解为给这个对象的内存上锁, 注意只是这块内存, 其他同类对象都会有各自的内存锁, 这时候一个以上线程获取到该锁之后执行该对象的这个同步方法(注意: 是该对象) 就会产生互斥, 而如果是不同对象的这个同步方法则不会互斥

    也就是说有如果一个类有两个对象 M, N, A 线程 和 B 线程分别访问 M, N 对象的以对象加锁的代码, 这时并不会互斥, 只会在 A 线程和 B 线程同时访问 M 或者 N 中的这个同步时才会互斥

对于加锁的方式:

  • 非静态方法加锁

    对于非静态方法, 是对象锁 (以 this 加锁)

  • 静态方法加锁

    在静态方法上加锁是以 xxx.class 本类字节码的形式加锁, 只要是这个类产生的对象的该方法访问都会是互斥的

  • 同步代码块

    同步代码块根据加锁的粒度不同也是一个道理

枚举单例:

枚举也可以用作一种单例模式的实现

public enum EnumSingleton {
    INSTANCE;
    
    public void doSomething() {
        
    }
}

内部类单例:

用内部类的方式实现单例模式, 可以用类初始化的特性 (在初始化时是单线程的) 来保证线程安全, 并且是懒加载 (在不调用 getInstance() 之前静态内部类不会初始化)

public class InnerSingleton {
    
    private InnerSingleton() {
        
    }

    private static class SingletonHolder {
        public static final InnerSingleton INSTANCE = new InnerSingleton();
    }

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

static 内部类和非 static 内部类:

在一个类初始化时会初始化他的非静态内部类, 但不会初始化静态内部类

静态内部类:

  • 静态内部类与外部类实例无关, 可以独立存在
  • 静态内部类可以直接通过外部类的类名访问, 不需要创建外部类的实例
  • 静态内部类不能访问外部类的非静态成员 (字段和方法), 只能访问静态成员
  • 静态内部类可以包含静态成员和非静态成员, 因此它可以被看作是一个独立的类

非静态内部类:

  • 非静态内部类依赖于外部类的实例存在, 每个非静态内部类都与特定的外部类实例关联
  • 非静态内部类可以访问外部类的所有成员, 包括静态和非静态成员
  • 要创建非静态内部类的实例, 必须首先创建外部类的实例, 然后使用外部类实例来创建内部类实例

类加载时机: JVM 在有且仅有的 5 种场景下会对类进行初始化

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

涉及的线程安全问题:

在饱汉式, 枚举和内部类单例时, 多个线程调用 getInstance() 方法时 ( INSTANCEstatic ), 只会有一个线程去初始化这个类

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

使用场景:

  • 如果程序中的某个类对于所有客户端只有一个可用的实例
  • 如果你需要更加严格地控制全局变量

优缺点:

优点缺点
你可以保证一个类只有一个实例违反了单一职责原则. 该模式同时解决了两个问题
你获得了一个指向该实例的全局访问节点单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等
仅在首次请求单例对象时对其进行初始化该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象

命令模式 (Command):

把请求封装成对象, 以便用于不同的请求/队列或日志请求来参数化其他对象, 并支持可撤销的操作

这个模式就像是遥控器上的按键, 你可以清楚的知道每个按键用于做什么却不需要知道按下后逻辑门的状态等

LightCommand

注意以上具体命令类是 record 类

命令模式可以很好的支持撤销和重做等操作. 可以在 Command 类中用一个属性记录之前的状态, 再用一个 Stack 来保存使用过的命令, 从而简单的支持撤回和重做

使用场景:

  • 如果你需要通过操作来参数化对象
  • 如果你想要将操作放入队列中, 操作的执行或者远程执行操作
  • 如果你想要实现操作回滚功能

优缺点:

优点缺点
单一职责原则. 你可以解耦触发和执行操作的类代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次
开闭原则. 你可以在不修改已有客户端代码的情况下在程序中创建新的命令
你可以实现撤销和恢复功能
你可以实现操作的延迟执行
你可以将一组简单命令组合成一个复杂命令

适配器模式 (Adapter):

适配器模式是作为两个不兼容的接口之间的桥梁

这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能

就像是 MacBook 没有 USB 接口, 如果想要使用 U 盘, 必须使用一个转接器将 USB 转成 Type-C 才可以使用

最简单的一个例子是 Callable 接口转 Runnable 接口

package com.erzbir.adapter;

import java.util.concurrent.Callable;

/**
 * @author Erzbir
 * @Date 2023/10/7
 */
public class RunnableAdapter implements Runnable {
    Callable<?> callable;

    public RunnableAdapter(Callable<?> callable) {
        this.callable = callable;
    }

    @Override
    public void run() {
        try {
            callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Callable<Void> callable = () -> {
            System.out.println("hello adapter");
            return null;
        };
        Thread thread = new Thread(new RunnableAdapter(callable));
        thread.start();
    }
}

与装饰者模式的差别是, 装饰者主要是关于责任或行为, 适配器是转换接口

使用场景:

  • 当你希望使用某个类, 但是其接口与其他代码不兼容时
  • 如果需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性

优缺点:

优点缺点
单一职责原则. 你可以将接口或数据转换代码从程序主要业务逻辑中分离代码整体复杂度增加, 因为你需要新增一系列接口和类. 有时直接更改服务类使其与其他代码兼容会更简单
开闭原则. 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器

外观模式 (Facade):

为子系统中的一组接口提供了一个统一的接口. 外观定义了一个高级别的接口, 使得子系统更容易使用

假设你必须在代码中使用某个复杂的库或框架中的众多对象. 正常情况下, 你需要负责所有对象的初始化工作, 管理其依赖关系并按正确的顺序执行方法等, 比如电脑的开关机涉及到各个组件的一系列写入, 卸载, 断电等操作, 但调用者只需要知道按下关机键就关机, 并不会电脑也不会允许你手动关闭每个组件来实现关机

FacadeComputerSystemApplication

与策略模式不同的是, 外观模式指在组合子系统的操作

使用场景:

  • 如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限
  • 如果需要将子系统组织为多层结构

优缺点:

优点缺点
你可以让自己的代码独立于复杂子系统外观可能成为与程序中所有类都耦合的上帝对象 (知道的和能做到的事太多)

模板方法模式 (Template Method):

在一个方法中定义一个算法的骨架, 而把一些步骤延迟到子类实现

模板方法使得子类在不改变算法结构的情况下, 重新定义算法中的某些步骤

在现实中, 比如某幸咖啡店, 不管卖咖啡还是不是咖啡的饮料, 在一个制作过程中都会涉及加水, 加冰等等相同的操作, 但是加的调料会各不相同

以下例子是包含咖啡因的饮料

public abstract class CaffeineBeverage {
    // 这个方法就是魔板方法, 实现不变的部分, 变化的部分延迟到子类实现
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    protected abstract void brew();

    protected abstract void addCondiments();

    void boilWater() {
        System.out.println("boiling water");
    }

    void pourInCup() {
        System.out.println("pouring into cup");
    }
}

实际上是定义了操作顺序, 但把具体操作方式交给子类

使用场景:

  • 当只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时
  • 当多个类的算法除一些细微不同之外几乎完全一样时, 可使用该模式. 但其后果就是, 只要算法发生变化, 就可能需要修改所有的类

优缺点:

优点缺点
你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小部分客户端可能会受到算法框架的限制
你可将重复代码提取到一个超类中通过子类抑制默认步骤实现可能会导致违反里氏替换原则
模板方法中的步骤越多, 其维护工作就可能会越困难

迭代器模式 (Iterator):

提供一种方式, 可以访问一个聚合对象中的元素而又不暴露其潜在的表示

此模式在 Java 基础中多少都会有涉及, 在 Java 内置的容器中都实现了一个 Iterable 接口, 这个接口的一个方法 iterator() 返回一个 Iterator 迭代器用于迭代

iIterator:

public interface Iterator<E> {
    boolean hasNext();

    E next();
    
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

如果有两个或以上的类的内部使用的容器不一样 (一个为数组, 另一个为 List), 当要提供这两个类的聚合时, 就让这两个类实现 Iterator 接口

此模式让我们把遍历的任务放在迭代器对象上, 而不是聚合上, 这样就简化了聚合的接口和实现, 让责任放在合适的地方

使用场景:

  • 当集合背后为复杂的数据结构, 且你希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑)
  • 使用该模式可以减少程序中重复的遍历代码
  • 如果你希望代码能够遍历不同的甚至是无法预知的数据结构

优缺点:

优点缺点
单一职责原则. 通过将体积庞大的遍历算法代码抽取为独立的类, 你可对客户端代码和集合进行整理如果你的程序只与简单的集合进行交互, 应用该模式可能会矫枉过正
开闭原则. 你可实现新型的集合和迭代器并将其传递给现有代码, 无需修改现有代码对于某些特殊集合, 使用迭代器可能比直接遍历的效率低
可以并行遍历同一集合, 因为每个迭代器对象都包含其自身的遍历状态
可以暂停遍历并在需要时继续

组合模式 (Composite):

允许你将对象组合成树形结构来表现部分 --- 整体层次结构. 组合让客户可以统一处理个别对象和对象组合

此模式也叫对象树 (Object Tree)

组合模式是数据结构以外实现树状关系的一种方式, 通过叶节点类和组合类实现同一接口来实现

以菜单和子菜单为例子.

做法是:

  • 声明一个 MenuComponent 接口, 让 MenuItem 类作为 Leaf 实现 MwnuComponent 接口, 让 Menu 类作为 Composite 实现 MenuComponent 接口

    composite

    注: 此图中是让 MenuComponent 作为子接口, 在此例中可以不用子接口的方式

  • 在接口中定义公共的方法并提供默认实现:

    public interface MenuComponent {
        default void add(Component menuComponent) {
            throw new UnsupportedOperationException();
        }
    
        default void remove(Component menuComponent) {
            throw new UnsupportedOperationException();
        }
    
        default Component getChild(int index) {
            throw new UnsupportedOperationException();
        }
        
        default String getName() {
            throw new UnsupportedOperationException();
        }
    
        default String getDescription() {
            throw new UnsupportedOperationException();
        }
    
        default double getPrice() {
            throw new UnsupportedOperationException();
        }
    
        default boolean isVegetarian() {
            throw new UnsupportedOperationException();
        }
    
        default void print() {
            throw new UnsupportedOperationException();
        }
    }
    
    

    MenuItem 覆盖需要的方法 (getName(), getDescription(), getPrice(), isVegetarian(), print()), 以及 Menu 覆盖需要的方法 (add(*), remove(*), getChild(*))

    这里体现的就是单一责任原则

  • 客户端在调用的代码

    public class Waitress {
        private MenuComponent menus;
    
        public Waitress(MenuComponent menus) {
            this.menus = menus;
        }
    
        public void printMenu() {
            menus.print();
        }
    }
    
    
    public class CompositeApplication {
        public static void main(String[] args) {
            MenuComponent pancakeHouseMenu =
                    new Menu("PANCAKE HOUSE MENU", "Breakfast");
            MenuComponent dinerMenu =
                    new Menu("DINER MENU", "Lunch");
            MenuComponent cafeMenu =
                    new Menu("CAFE MENU", "Dinner");
            MenuComponent dessertMenu =
                    new Menu("DESSERT MENU", "Dessert of course!");
            MenuComponent coffeeMenu = new Menu("COFFEE MENU", "Stuff to go with your afternoon coffee");
    
            MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
    
            allMenus.add(pancakeHouseMenu);
            allMenus.add(dinerMenu);
            allMenus.add(cafeMenu);
    
            pancakeHouseMenu.add(new MenuItem(
                    "K&B's Pancake Breakfast",
                    "Pancakes with scrambled eggs and toast",
                    true,
                    2.99));
            pancakeHouseMenu.add(new MenuItem(
                    "Regular Pancake Breakfast",
                    "Pancakes with fried eggs, sausage",
                    false,
                    2.99));
            pancakeHouseMenu.add(new MenuItem(
                    "Blueberry Pancakes",
                    "Pancakes made with fresh blueberries, and blueberry syrup",
                    true,
                    3.49));
            pancakeHouseMenu.add(new MenuItem(
                    "Waffles",
                    "Waffles with your choice of blueberries or strawberries",
                    true,
                    3.59));
    
            dinerMenu.add(new MenuItem(
                    "Vegetarian BLT",
                    "(Fakin') Bacon with lettuce & tomato on whole wheat",
                    true,
                    2.99));
            dinerMenu.add(new MenuItem(
                    "BLT",
                    "Bacon with lettuce & tomato on whole wheat",
                    false,
                    2.99));
            dinerMenu.add(new MenuItem(
                    "Soup of the day",
                    "A bowl of the soup of the day, with a side of potato salad",
                    false,
                    3.29));
            dinerMenu.add(new MenuItem(
                    "Hot Dog",
                    "A hot dog, with saurkraut, relish, onions, topped with cheese",
                    false,
                    3.05));
            dinerMenu.add(new MenuItem(
                    "Steamed Veggies and Brown Rice",
                    "Steamed vegetables over brown rice",
                    true,
                    3.99));
    
            dinerMenu.add(new MenuItem(
                    "Pasta",
                    "Spaghetti with marinara sauce, and a slice of sourdough bread",
                    true,
                    3.89));
    
            dinerMenu.add(dessertMenu);
    
            dessertMenu.add(new MenuItem(
                    "Apple Pie",
                    "Apple pie with a flakey crust, topped with vanilla icecream",
                    true,
                    1.59));
    
            dessertMenu.add(new MenuItem(
                    "Cheesecake",
                    "Creamy New York cheesecake, with a chocolate graham crust",
                    true,
                    1.99));
            dessertMenu.add(new MenuItem(
                    "Sorbet",
                    "A scoop of raspberry and a scoop of lime",
                    true,
                    1.89));
    
            cafeMenu.add(new MenuItem(
                    "Veggie Burger and Air Fries",
                    "Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
                    true,
                    3.99));
            cafeMenu.add(new MenuItem(
                    "Soup of the day",
                    "A cup of the soup of the day, with a side salad",
                    false,
                    3.69));
            cafeMenu.add(new MenuItem(
                    "Burrito",
                    "A large burrito, with whole pinto beans, salsa, guacamole",
                    true,
                    4.29));
    
            cafeMenu.add(coffeeMenu);
    
            coffeeMenu.add(new MenuItem(
                    "Coffee Cake",
                    "Crumbly cake topped with cinnamon and walnuts",
                    true,
                    1.59));
            coffeeMenu.add(new MenuItem(
                    "Bagel",
                    "Flavors include sesame, poppyseed, cinnamon raisin, pumpkin",
                    false,
                    0.69));
            coffeeMenu.add(new MenuItem(
                    "Biscotti",
                    "Three almond or hazelnut biscotti cookies",
                    true,
                    0.89));
    
            Waitress waitress = new Waitress(allMenus);
    
            waitress.printMenu();
        }
    }
    
    

使用场景:

  • 如果你需要实现树状对象结构, 可以使用组合模式
  • 如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式

优缺点:

优点缺点
你可以利用多态和递归机制更方便地使用复杂树结构对于功能差异较大的类, 提供公共接口或许会有困难. 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解
开闭原则. 无需更改现有代码, 你就可以在应用中添加新元素使其成为对象树的一部分

状态模式 (State):

状态模式的底层设计和策略模式一样, 但状态模式是通过改变对象的内部状态来控制其行为

允许对象在内部状态发生变化时改变其行为. 对象看起来好像改变了它的类

有一个例子是糖果状态机

假设购买一个糖果的价钱为 1RMB (1 个硬币), 那么现在有四个状态: 有 1 RMB, 没有 1 RMB, 糖果售罄, 糖果售出

candymachine.drawio

购买流程就为:

gachaflow

此时可能首先想到的是用静态变量来表示状态:

final static int SOLD_OUT = 0;
final static int NO_COIN = 1;
final static int HAS_COIN = 2;
final static int SOLD = 3;

int state = SOLD_OUT

这样做会不得已在代码中增加很多判断, 导致后期难以更新维护

改变设计:

  • 可以专门定义一个 State 接口, 针对扭蛋机的每个动作有一个方法
  • 为机器的每个状态实现一个 State, 每个 State 负责在相应状态下的机器行为

state

GachaMachine 相当于一个 Context, 在此例中状态的推进是在状态类中完成的

使用场景:

  • 如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话
  • 如果某个类需要根据成员变量的当前值改变自身行为, 从而需要使用大量的条件语句时
  • 当相似状态和基于条件的状态机转换中存在许多重复代码时

优缺点:

优点缺点
单一职责原则. 将与特定状态相关的代码放在单独的类中如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作
开闭原则. 无需修改已有状态类和上下文就能引入新状态
通过消除臃肿的状态机条件语句简化上下文代码

代理模式 (Proxy):

为另一个对象提供一个替身或占位符来控制对这个对象的访问

  • 让代理类来处理一些繁杂的任务可以让类的职责更加清晰

  • 可以实现切面编程

  • 可以做缓存

  • 记录日志

  • .......

在现实中的例子是房屋中介, 你不需要联系户主只需要提供给中介相应信息后交钱就可以入住, 中介负责帮你和户主协商等等一些麻烦的操作, 他还可以提高房屋价格来抽成

静态代理:

让代理类引用真实类

动态代理:

动态代理有两种, 一种是 JDK 本身提供的基于接口的代理, 另一种则是 cglib 等第三方库提供的基于类的代理

JDK:

@Getter
public class OwnerInvocationHandler implements InvocationHandler {
    private Person person;

    public OwnerInvocationHandler(Person person) {
        this.person = person;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, InvocationTargetException {
        System.out.println("代理被调用了");
        return method.invoke(person, args);
    }
}
public class PersonProxy {
    private static Person getOwnerProxy(Person person) {
        return (Person) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new OwnerInvocationHandler(person));
    }

}

cglib:

public class ServerProxy implements MethodInterceptor {

    private Server target;

    public ServerProxy (Server server) {
        target = server;
    }

    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("服务器被访问");
        return method.invoke(target, args);
    }
}

public class CglibApplication {
    public static void main(String[] args) {
        Server server = new Server();
        Client client = new Client();
        Server proxy = getProxy(server);
        client.request(proxy);
    }

    private static Server getProxy(Server server) {
        //动态代理创建的class文件存储到本地
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./proxy");
        //通过cglib动态代理获取代理对象的过程,创建调用的对象
        Enhancer enhancer = new Enhancer();
        //设置enhancer对象的父类
        enhancer.setSuperclass(Server.class);
        //设置enhancer的回调对象
        enhancer.setCallback(new ServerProxy(server));
        return (Server) enhancer.create();
    }
}

JDK 8 以后添加 jvm 参数:

--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/sun.net.util=ALL-UNNAMED

代理模式形式:

虚拟代理:

在需要真正的对象工作之前, 如果代理对象能够处理, 那么暂时不需要真正对象来出手

**主要解决: **在直接访问对象时带来的问题, 比如说: 要访问的对象在远程的机器上. 在面向对象系统中, 有些对象由于某些原因 (比如对象创建开销很大, 或者某些操作需要安全控制, 或者需要进程外的访问), 直接访问会给使用者或者系统结构带来很多麻烦, 我们可以在访问此对象时加上一个对此对象的访问层

现在有两个类 ClientRealSubject, Client 请求 RealSubject 时可以在他们之中多加一个 Proxy 类和 RealSubject 实现相同的接口, 把请求交给 Proxy, Proxy 进行一些增强处理或者预处理后再将请求交给 RealSubject, 这样可以对 RealSubject 得到更多的监控或者特殊处理

virtualproxy

这里 ImageIconRealSubject

public class ImageProxy implements Icon {
    @Setter(onMethod_ = {@Synchronized})
    private volatile ImageIcon imageIcon; // RealSubject
    private final URL imageURL;
    private Thread retrievalThread;
    private boolean retrieving;

    public ImageProxy(URL imageURL) {
        this.imageURL = imageURL;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        if (imageIcon != null) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("Loading album cover, please wait...", x + 300, y + 190);
            if (!retrieving) {
                retrieving = true;
                retrievalThread = new Thread(() ->{
                    try {
                        // 此处在真正需要真实对象工作之前, 是不创建对象的, 需要时再通过异步的方式创建
                        setImageIcon(new ImageIcon(imageURL, "Album Cover"));
                        c.repaint();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
                retrievalThread.start();
            }
        }
    }

    @Override
    public int getIconWidth() {
        return imageIcon != null ? imageIcon.getIconWidth() : 800;
    }

    @Override
    public int getIconHeight() {
        return imageIcon != null ? imageIcon.getIconHeight() : 600;
    }

}

使用场景:

  • 延迟初始化 (虚拟代理). 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时
  • 访问控制 (保护代理). 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式
  • 本地执行远程服务 (远程代理). 适用于服务对象位于远程服务器上的情形
  • 记录日志请求 (日志记录代理). 适用于当你需要保存对于服务对象的请求历史记录时
  • 缓存请求结果 (缓存代理). 适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时
  • 智能引用. 可在没有客户端使用某个重量级对象时立即销毁该对象

桥接模式 (Bridge):

桥接模式让抽象和实现可以独立扩展, 使两者解耦

比如设计一个程序是做出有颜色的各种图形, 如果是不同形状的每种颜色的图形都有一个类, 那么指数增长的类会导致程序难以维护. 这时候可以将 形状颜色 分离开, 让形状包含颜色, 这样就可以得到各种图形

这里是一个万能遥控器的设计

devicebridge

使用场景:

  • 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类)
  • 如果你希望在几个独立维度上扩展一个类
  • 如果你需要在运行时切换不同实现方法

优缺点:

优点缺点
你可以创建与平台无关的类和程序对高内聚的类使用该模式可能会让代码更加复杂
客户端代码仅与高层抽象部分进行互动, 不会接触到平台的详细信息
开闭原则. 你可以新增抽象部分和实现部分, 且它们之间不会相互影响
单一职责原则. 抽象部分专注于处理高层逻辑, 实现部分处理平台细节

生成器模式 (Builder):

这个模式很常见, 能够分步骤创建复杂对象, 且可以生成包含不同信息的对象

加入你有一个 "游戏规则说明" 类, 每天的游戏规则都不一样, 就可以用生成器模式来按需要的信息构建对象

建房子就是一个很好的例子, 先是建好地基, 然后框架... 你可以用石头或者水泥来做墙, 木头或合金来做门窗... 这主要是分步骤按需构建

分步骤构建在代码中的体现就是, 有一个包含十个参数的构造函数的类, 你肯定不会想去用 new 来构造, 这时候用生成器来分步骤构建就很清晰

使用场景:

  • 使用生成器模式可避免 "重叠构造函数 (telescoping constructor)" 的出现
  • 当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时
  • 使用生成器构造组合树或其他复杂对象, 可以延迟执行某些步骤而不会影响最终产品

生成器在执行制造步骤时, 不能对外发布未完成的产品. 这可以避免客户端代码获取到不完整结果对象的情况

优缺点:

优点缺点
你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加
生成不同形式的产品时, 你可以复用相同的制造代码
单一职责原则. 你可以将复杂构造代码从产品的业务逻辑中分离出来

责任链模式 (Chain of Responsibility):

当你要把一个请求的机会给予多于一个对象时, 使用责任链模式

例如 Java 中的类加载器的设计 --- 双亲委派机制, 就采用了责任链模式

现实中的例子是去部门办事踢皮球 (只有这一句就能理解这个模式了), 或者是客服踢皮球

假设我不小心购买了软件 CleanMyMac, 于是我准备去退费. 首先我找到了 CleanMyMac 公司的客服, 第一次他告诉我苹果那边的数据库他们无法查看, 让我联系苹果. 我联系苹果之后苹果说无法处理. 那么这个时候我可以选择的是走消费者维权继续处理, 或者说不管了直接结束

@Setter
@Getter
public abstract class Support {
    private Support nextSupport;

    protected abstract void handleRequest(Problem problem);
}
public class AppleSupport extends Support {
    @Override
    public void handleRequest(Problem problem) {
        if (!(problem instanceof AppleProblem)) {
            System.out.println(problem.getName() + " 未解决, 交给下一个");
            getNextSupport().handleRequest(problem);
            return;
        }
        System.out.println(problem.getName() + " 解决");
    }
}

public class CMMSupport extends Support {
    @Override
    public void handleRequest(Problem problem) {
        if (!(problem instanceof CMMProblem)) {
            System.out.println(problem.getName() + " 未解决, 交给下一个");
            getNextSupport().handleRequest(problem);
            return;
        }
        System.out.println(problem.getName() + " 解决");
    }
}
public class CourtSupport extends Support {
    @Override
    protected void handleRequest(Problem problem) {
        System.out.println(problem.getName() + " 解决");
    }
}

public class ChainApplication {
    public static void main(String[] args) {
        AppleProblem appleProblem = new AppleProblem("退费");
        CMMProblem cmmProblem = new CMMProblem("退费");
        OtherProblem otherProblem = new OtherProblem("未知");
        AppleSupport appleSupport = new AppleSupport();
        CMMSupport cmmSupport = new CMMSupport();
        CourtSupport courtSupport = new CourtSupport();
        appleSupport.setNextSupport(cmmSupport);
        cmmSupport.setNextSupport(courtSupport);
        appleSupport.handleRequest(cmmProblem);
        cmmSupport.handleRequest(appleProblem);
        appleSupport.handleRequest(otherProblem);
    }
}

使用场景:

  • 当程序需要使用不同方式处理不同种类请求, 而且请求类型和顺序预先未知时
  • 当必须按顺序执行多个处理者时
  • 如果所需处理者及其顺序必须在运行时进行改变

优缺点:

优点缺点
你可以控制请求处理的顺序部分请求可能未被处理
单一职责原则. 你可对发起操作和执行操作的类进行解耦
开闭原则. 你可以在不更改现有代码的情况下在程序中新增处理者

原型模式 (Prototype):

使你能够复制已有对象, 而又无需使代码依赖它们所属的类

一般以构造器的方式来新建对象赋值完成克隆, 或者是在 Java 中可以使用 clone() 方法

使用场景:

  • 如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类
  • 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量. 别人创建这些子类的目的可能是为了创建特定类型的对象

优缺点:

优点缺点
你可以克隆对象, 而无需与它们所属的具体类相耦合克隆包含循环引用的复杂对象可能会非常麻烦
你可以克隆预生成原型, 避免反复运行初始化代码
你可以更方便地生成复杂对象
你可以用继承以外的方式来处理复杂对象的不同配置

享元模式 (Flyweight):

这个模式也叫蝇量

它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象

这个模式不建议对象存储外在状态, 只存储内在状态 (常量)

像是游戏中的树一样, 为了节省内存占用一般来说一片 "树林" 其实都是同一颗树, 只是座标和颜色不同

我们就可以将不需要变化的部分抽离成一个新的对象, 让不同的树复用这个对象

@Getter
@Setter
public class TreeType {
    private String color;
    private String texture;

    public TreeType(String color, String texture) {
        this.color = color;
        this.texture = texture;
    }

    public void draw(int x, int y) {
        System.out.println("在 " + x + ", " + y + " 绘制了一棵树, 享元类为: " + this.hashCode());
    }
}
//  封装创建享元的复杂机制
public class TreeFactory {
    private final static List<TreeType> treeTypes = new ArrayList<>();

    /**
     * 如果颜色和材质相同就从容器中取
     */
    public static TreeType getTreeType(String color, String texture) {
        TreeType treeType = null;
        for (TreeType type : treeTypes) {
            if (type.getColor().equals(color) && type.getTexture().equals(texture)) {
                treeType = type;
            }
        }
        if (treeType == null) {
            treeType = new TreeType(color, texture);
            treeTypes.add(treeType);
        }
        return treeType;
    }
}

@Getter
@Setter
public class Tree {
    private final TreeType treeType;
    private int x;
    private int y;

    public Tree(TreeType treeType, int x, int y) {
        this.treeType = treeType;
        this.x = x;
        this.y = y;
    }

    public void draw() {
        treeType.draw(x, y);
    }
}
public class Forest {
    private List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String color, String texture) {
        TreeType type = TreeFactory.getTreeType(color, texture);
        Tree tree = new Tree(type, x, y);
        trees.add(tree);
    }

    public void paint() {
        trees.forEach(Tree::draw);
    }
}

使用场景:

  • 仅在程序必须支持大量对象且没有足够的内存容量时使用

优缺点:

优点缺点
如果程序中有很多相似对象, 那么将可以节省大量内存可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据
代码会变得更加复杂. 团队中的新成员总是会问: 为什么要像这样拆分一个实体的状态?

解释器模式 (Interpreter Pattern):

定义一个语言的语法, 并且建立一个解释器来解释该语言中的句子

AbstractExpression (抽象解释器): 在抽象表达式中声明了抽象的解释操作, 具体的解释任务由各个实现类完成, 它是所有终结符表达式和非终结符表达式的公共父类

TerminalExpression (终结符表达式): 实现与文法中的元素相关联的解释操作, 通常一个解释器模式中只有一个终结表达式, 但有多个实例, 对应不同的终结符

**NonterminalExpression (非终结符表达式) **: 文法中的每条规则对应于一个非终结表达式, 非终结符表达式根据逻辑的复杂程度而增加, 原则上每个文法规则都对应一个非终结符表达式

Context (环境类): 环境类又称为上下文类, 它用于存储解释器之外的一些全局信息, 通常它临时存储了需要解释的语句

interpreterop

**非终结符表达式 (相当于树的树杈) **: 比如加减的表达式, 因为当运算遇到这类的表达式的时候, 必须先把非终结符的结果计算出来, 一层一层的调用

终结符表达式 (相当于树的叶子): 遇到这个表达式 interpreter 执行能直接返回结果, 不会向下继续调用

使用场景:

  • 构建语法并将其解释为特定操作

中介者模式 (Mediator):

此模式侧重于聚合所有对象的操作, 让对象之前解耦

比如你有闹钟, 热水壶, 电视机并且都是智能家居, 当你起床的时候关掉闹钟, 闹钟就提示热水壶就会开始烧水, 开始烧水后又设置了闹钟的定时, 当你关掉烧水壶的时候电视又会自动打开

如果设计成三个类, "闹钟", "热水壶", "电视机", 让这三个类之前互相调用达到通知的目的, 这样就会使对象高度耦合. 在他们三个之中多加一个 "中介" 类, 让这个 "中介" 来通知和控制所有的逻辑, 从而让这三个类解耦

有个更好的现实例子是交通信号灯, 十字路口的车辆谁停谁走司机们并不会相互交流协商, 而是直接看交通信号灯. 或者说是指挥交通的交警更加贴切

实例: MVC 框架中的 Controller 就是中介

这个模式类似监听者模式, 但监听者更多的是监听某个事件, 中介则是转发事件和控制协作

mediator

使用场景:

  • 当一些对象和其他对象紧密耦合以致难以对其进行修改时
  • 当组件因过于依赖其他组件而无法在不同应用中复用时
  • 如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时

优缺点:

优点缺点
单一职责原则. 你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护一段时间后, 中介者可能会演化成为上帝对象
开闭原则.你无需修改实际组件就能增加新的中介者
你可以减轻应用中多个组件间的耦合情况
你可以更方便地复用各个组件

备忘录模式 (Memento):

允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态

游戏存档就是一个备忘录模式

备忘录有两个目标:

  1. 保存系统关键对象的重要状态
  2. 维护关键对象的封装

让正在保存的状态和关键对象分离. 这个有状态的, 分离的对象, 就是备忘录

三个角色: Memento, Originator, CareTaker

可以抽象出 Memento 的接口, 让 Originator 在内部实现 Memento 把备份和恢复的职责交给 Originator, 这样就不需要 CareTaker, 但是会破坏职责单一原则

使用场景:

  • 当你需要创建对象状态快照来恢复其之前的状态时
  • 当直接访问对象的成员变量和获取器或设置器将导致封装被突破时

优缺点:

优点缺点
你可以在不破坏对象封装情况的前提下创建对象状态快照如果客户端过于频繁地创建备忘录, 程序将消耗大量内存
你可以通过让负责人维护原发器状态历史记录来简化原发器代码负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用的备忘录
绝大部分动态编程语言 (例如 PHP, Python 和 JavaScript) 不能确保备忘录中的状态不被修改

访问者模式 (Visitor):

将算法与其所作用的对象隔离

访问者可以将数据结构于数据操作分开, 就比如访问树形结构的数据, 生成一个统计图

这里用军队举例, 军队里有士兵, 士官和指挥官. 要求你统计军官的所有下属, 其中包括下属的下属

访问者类:

public interface Visitor {
    void visit(Soldier soldier);

    void visit(Sergeant sergeant);

    void visit(Commander commander);

}

节点抽象类:

public abstract class Unit {
    private final Unit[] children;
    @Getter
    @Setter
    private String name;


    public Unit(String name, Unit... children) {
        this.children = children;
        this.name = name;
    }

    // 此处实现用于访问子节点
    public void accept(Visitor visitor) {
        Arrays.stream(children).forEach(child -> child.accept(visitor));
    }
}

指挥官类:

public class Commander extends Unit {

    public Commander(String name, Unit... children) {
        super(name, children);
    }

    // 这里重写方法后又调用了父类的方法, 访问子节点
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
        super.accept(visitor);
    }

}

指挥官访问类:

这里用了 双分派 的机制来选择使用 Visitor 的哪个方法

服务端的方法参数包含客户端, 客服端调用时将自身作为参数

public class CommanderVisitor implements Visitor {
    @Override
    public void visit(Soldier soldier) {

    }

    @Override
    public void visit(Sergeant sergeant) {

    }

    @Override
    public void visit(Commander commander) {
        System.out.println("指挥官: " + commander.getName());
    }
}

客户端类:

public class VisitorApplication {
    public static void main(String[] args) {
        Commander commander = new Commander("wuk",
                new Sergeant("nck", new Soldier("bob"), new Soldier("dev"), new Soldier("cbk")),
                new Sergeant("pob", new Soldier("pok"), new Soldier("ckl"), new Soldier("vik"))
        );
        commander.accept(new SoldierVisitor());
        commander.accept(new SergeantVisitor());
        commander.accept(new CommanderVisitor());
    }
}

输出结果:

士兵: bob
士兵: dev
士兵: cbk
士兵: pok
士兵: ckl
士兵: vik
中士: nck
中士: pob
指挥官: wuk

使用场景:

  • 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作
  • 可使用访问者模式来清理辅助行为的业务逻辑
  • 当某个行为仅在类层次结构中的一些类中有意义, 而在其他类中没有意义时

优缺点:

优点缺点
开闭原则. 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者
单一职责原则. 可将同一行为的不同版本移到同一个类中在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限
访问者对象可以在与各种对象交互时收集一些有用的信息. 当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助