设计模式 四、行为设计模式(2)

五、状态模式

1、概述

状态设计模式是一种行为型设计模式,它允许对象在其内部状态发生时改变其行为,这种模式可以消除大量的条件语句,并将每个状态的行为封装到单独的类中。

状态模式的主要组成部分如下:

1)上下文(Context):上下文通常包含一个具体状态的引用,用于维护当前状态,上下文委托给当前对象处理状态相关行为。

2)抽象状态(State):定义一个接口,用于封装与上下文的特定状态相关的行为。

3)具体状态(Concrete State):实现抽象状态接口,为具体状态定义行为。每个具体状态类对应一个状态。

简单示例,假设要模拟一个简易的电视遥控器,具有开启、关闭和调整音量的功能。
假设不使用设计模式:

java 复制代码
public class TV {
    private boolean isOn;
    private int volume;
    public TV() {
        isOn = false;
        volume = 0;
    }
    public void turnOn() {
        // 如果是开启状态
        if (isOn) {
            System.out.println("TV is already on.");
            // 否则打开电视
        } else {
            isOn = true;
            System.out.println("Turning on the TV.");
        }
    }
    public void turnOff() {
        if (isOn) {
            isOn = false;
            System.out.println("Turning off the TV.");
        } else {
            System.out.println("TV is already off.");
        }
    }
    public void adjustVolume(int volume) {
        if (isOn) {
            this.volume = volume;
            System.out.println("Adjusting volume to: " + volume);
        } else {
            System.out.println("Cannot adjust volume, TV is off.");
        }
    }
}
public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        tv.turnOn();
        tv.adjustVolume(10);
        tv.turnOff();
    }
}

该例子中我们状态比较少,所以代码看起来也不是很复杂,但是状态如果变多了就会变得不好控制,比如增加换台,快捷键,静音等功能。

使用状态设计模式之后:

首先定义抽象状态接口TVState,将每一个修改状态的动作抽象成一个接口:

java 复制代码
public interface TVState {
    void turnOn();
    void turnOff();
    void adjustVolume(int volume);
}

接下来为每个具体的状态创建类,实现TVState接口,例如:创建TVOnState 和 TVOffState 类:

java 复制代码
// 在on状态下,去执行以下各种操作
public class TVOnState implements TVState {
    @Override
    public void turnOn() {
        System.out.println("TV is already on.");
    }
    @Override
    public void turnOff() {
        System.out.println("Turning off the TV.");
    }
    @Override
    public void adjustVolume(int volume) {
        System.out.println("Adjusting volume to: " + volume);
    }
}
// 在关机的状态下执行以下的操作
public class TVOffState implements TVState {
    @Override
    public void turnOn() {
        System.out.println("Turning on the TV.");
    }
    @Override
    public void turnOff() {
        System.out.println("TV is already off.");
    }
    @Override
    public void adjustVolume(int volume) {
        System.out.println("Cannot adjust volume, TV is off.");
    }
}

接下来定义上下文类TV:

java 复制代码
public class TV {
    // 当前状态
    private TVState state;
    public TV() {
        state = new TVOffState();
    }
    public void setState(TVState state) {
        this.state = state;
    }
    public void turnOn() {
        // 打开
        state.turnOn();
        // 设置为开机状态
        setState(new TVOnState());
    }
    public void turnOff() {
        // 关闭
        state.turnOff();
        // 设置为关机状态
        setState(new TVOffState());
    }
    public void adjustVolume(int volume) {
        state.adjustVolume(volume);
    }
}

最后通过以下方式使用这些类:

java 复制代码
public class Main {
    public static void main(String[] args) {
        TV tv = new TV();
        tv.turnOn();
        tv.adjustVolume(10);
        tv.turnOff();
    }
}

这个例子展示了状态模式的基本结构和用法。通过使用状态模式,我们可以更好地组织和管理与特定状态相关的代码。当状态较多时,这种模式的优势就会凸显出来,同时我们在代码时,因为我们会对每个状态进行独立封装,所以也会简化代码编写。

2、有限状态机

有限状态机,英文翻译时Flinite State Machine,缩写为FSM,简称状态机,比较官方的说法是:有限状态机是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也成为转移条件(Transition Condition)。事件触发状态的转移及动作的执行,不过,动作不是必须的,也可能只转移状态,不执行任何动作。

2.1 分支法

如何实现状态机,总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移。原模原样的直译成代码。这样编写的代码会包含大量的 if - else或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑。所以我们把这种方法暂且命名为分支法:

下面是一个使用if-else 语句实现的马里奥形态变化的代码示例:

java 复制代码
public class Mario {
    private MarioState state;
    public Mario() {
        state = MarioState.SMALL;
    }
    public void handleEvent(Event event) {
        MarioState newState = state;
        // 处理吃蘑菇事件
        if (event == Event.MUSHROOM) {
            if (state == MarioState.SMALL) {
                newState = MarioState.BIG;
            }
        // 处理吃火花事件
        } else if (event == Event.FIRE_FLOWER) {
            if (state == MarioState.BIG) {
                newState = MarioState.FIRE;
            }
        // 处理遇到小怪事件
        } else if (event == Event.ENEMY_ATTACK) {
            if (state == MarioState.BIG) {
                newState = MarioState.SMALL;
            } else if (state == MarioState.FIRE) {
                newState = MarioState.BIG;
            } else if (state == MarioState.SMALL) {
                newState = MarioState.DEAD;
            }
        // 处理掉坑事件
        } else if (event == Event.FALL_INTO_PIT) {
            newState = MarioState.DEAD;
        }
        System.out.printf("从 %s 变为 %s%n", state, newState);
        state = newState;
    }
}
public class MarioDemo {
    public static void main(String[] args) {
        Mario mario = new Mario();
        mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
        mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
        mario.handleEvent(Event.ENEMY_ATTACK); // 变为死亡马里奥
    }
}

在这个示例中,我们使用 if-else 语句来处理状态转换。 在 handleEvent 方法中,我们根据事件和当前状态的组合来确定新状态 ,并 更新马里奥的状态 。这种实现方法相较于查表法和面向对象实现更为简单,但可能在状态和事件更多的情况下变得难以维护。选择合适的实现方法取决于实际需求和场景。

2.2 查表法

该种方法略,不怎么用,可以自己查资料。

2.3 状态模式

在查表法的代码实现中,事件触发的动作只是简单的状态或者数值,所以,我们用一个 MarioState类型的二维数组 TRANSITION_TABLE 就能表示,二维数组中的值表示出发事件后的新状态。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减分数、处理位置信息等等),我们就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性

虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,我们可以使用状态模式来解决

**状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分****支判断逻辑。**我们还是结合代码来理解这句话。利用状态模式,我们来补全 MarioStateMachine 类,补全后的代码如下所示。

以下是一个使用 Java 实现的简化版马里奥形态变化的案例代码,我为代码添加了中文注释以便理解:

java 复制代码
// 定义事件枚举类型
enum Event {
    // 吃蘑菇,吃火花,遇到小怪,调入深坑
    MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT
}
// 定义马里奥状态接口
interface MarioState {
    void handleEvent(Event event);
}
// 实现死亡马里奥状态
class DeadMario implements MarioState {
    private Mario mario;
    public DeadMario(Mario mario) {
        this.mario = mario;
    }
    @Override
    public void handleEvent(Event event) {
        System.out.println("马里奥已死亡,无法处理事件");
    }
}
// 实现小马里奥状态
class SmallMario implements MarioState {
    private Mario mario;
    public SmallMario(Mario mario) {
        this.mario = mario;
    }
    @Override
    public void handleEvent(Event event) {
        switch (event) {
            case MUSHROOM:
                System.out.println("变为大马里奥");
                mario.setState(new BigMario(mario));
                break;
            case FIRE_FLOWER:
                System.out.println("小马里奥不能直接变为火焰马里奥");
                break;
            case ENEMY_ATTACK:
                System.out.println("小玛丽奥去世了");
                mario.setState(new DeadMario(mario));
                break;
            case FALL_INTO_PIT:
                System.out.println("小玛丽奥去世了");
                mario.setState(new DeadMario(mario));
                break;
        }
    }
}
// 实现大马里奥状态
class BigMario implements MarioState {
    private Mario mario;
    public BigMario(Mario mario) {
        this.mario = mario;
    }
    @Override
    public void handleEvent(Event event) {
        switch (event) {
            case MUSHROOM:
                System.out.println("保持大马里奥");
                break;
            case FIRE_FLOWER:
                System.out.println("变为火焰马里奥");
                mario.setState(new FireMario(mario));
                break;
            case ENEMY_ATTACK:
                System.out.println("变为小马里奥");
                mario.setState(new SmallMario(mario));
                break;
            case FALL_INTO_PIT:
                System.out.println("马里奥去世了");
                mario.setState(new DeadMario(mario));
                break;
        }
    }
}
// 实现火焰马里奥状态
class FireMario implements MarioState {
    private Mario mario;
    public FireMario(Mario mario) {
        this.mario = mario;
    }
    @Override
    public void handleEvent(Event event) {
        switch (event) {
            case MUSHROOM:
                System.out.println("保持火焰马里奥");
                break;
            case FIRE_FLOWER:
                System.out.println("保持火焰马里奥");
                break;
            case ENEMY_ATTACK:
                System.out.println("变为大马里奥");
                mario.setState(new BigMario(mario));
                break;
            case FALL_INTO_PIT:
                System.out.println("马里奥去世了");
                mario.setState(new DeadMario(mario));
                break;
        }
    }
}
// 定义马里奥类,作为状态的上下文
class Mario {
    private MarioState state;
    public Mario() {
        state = new SmallMario(this);
    }
    public void setState(MarioState state) {
        this.state = state;
    }
    public void handleEvent(Event event) {
        state.handleEvent(event);
    }
}
// 测试类
public class MarioDemo {
    public static void main(String[] args) {
        Mario mario = new Mario();
        mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
        mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
        mario.handleEvent(Event.ENEMY_ATTACK); // 变为大马里奥
    }
}

在这个简化示例中,我们定义了 MarioState 接口以及实现了 DeadMario 、SmallMario 、 BigMario 和 FireMario 类,分别表示马里奥的四种形态。每个形态类实现了 handleEvent 方法,用于处理不同的游戏事件并根据有限状态机规 则进行状态转换。

Mario 类作为状态的上下文,用于管理和切换马里奥的状态。它有一个 setState方法,用于更新当前状态。 handleEvent 方法将事件传递给当前状态,以便根据事件执行相应的状态转换。

在 MarioDemo 测试类中,我们创建了一个 Mario 实例,并通过调用handleEvent 方法模拟游戏中的事件。通过运行这个测试类,你可以观察到马里奥根据有限状态机的规则在不同形态之间切换。

这个简化示例展示了如何使用有限状态机来实现马里奥角色的形态变化。在实际游戏开发中,你可能需要考虑更多的事件和状态,以及与游戏引擎或框架集成的方式。不过,这个示例可以帮助你理解有限状态机在游戏中的应用。

六、迭代器模式

迭代器模式,它用来遍历集合对象,不过,很多编程语言都将迭代器作为一个基础的类库,直接提供出来了。在平时的开发中,特别是业务开发,我们直接使用即可,很少会自己去实现一个迭代器。不过弄懂原理能帮助我们更好的使用这些工具类。

不怎么用,省略,需要可以自行查询。

七、访问者模式

1、概述和原理

访问者设计模式(Visitor Pattern)是一种行为型设计模式,它允许你再不修改现有类结构的情况下,为类添加新的操作,这种模式可以实现良好的解耦和扩展性,尤其适用于现有类层次结构中添加新的功能的情况。

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

  1. 访问者(Visitor):定义一个访问具体元素的接口,为每种具体元素类型声明一个访问操作。
  2. 具体访问者(ConcreteVisitor):实现访问者接口,为每种具体元素提供具体的访问操作实现。
  3. 元素(Element):定义一个接口,声明接受访问者的方法。
  4. 具体元素(ConcreteElement):实现元素接口,提供接受访问者的具体实现。
  5. 对象结构(ObjectStructure):包含一个元素集合,提供一个方法以遍历这些元素并让访问者访问它们。

以下是一个简单的访问者模式示例:

假设我们有一个表示计算机组件的类层次结构(如 CPU、内存和硬盘等),我们需要为这些组件实现一个功能,比如展示它们的详细信息。使用访问者模式,我们可以将**【展示详细信息】的功能与【组件类】分离** ,从而实现解耦和扩展性。
1、不同的元素(被访问的对象)可以接收不同的访问者。
2、不同的访问者会对不同的被访问者产生不同的行为。
3、如果想要扩展,则独立重新实现访问者接口,产生一个新的具体访问者就可以了。
4、他实际解耦的是【被访问者】和【对被访问者的操作】。

简单理解就是不同的访问者,到了同一个被访问对象的家里会干不同的事。这个【事】就是行为,通过访问者模式,我们可以将行为和对象分离解耦,如下图。

下边是一个使用了访问者模式的案例:

java 复制代码
// 访问者接口
interface ComputerPartVisitor {
    // 访问 Computer 对象
    void visit(Computer computer);
    // 访问 Mouse 对象
    void visit(Mouse mouse);
    // 访问 Keyboard 对象
    void visit(Keyboard keyboard);
}
// 具体访问者
class ComputerPartDisplayVisitor implements ComputerPartVisitor {
    // 访问 Computer 对象
    @Override
    public void visit(Computer computer) {
        System.out.println("Displaying Computer.");
    }
    // 访问 Mouse 对象
    @Override
    public void visit(Mouse mouse) {
        System.out.println("Displaying Mouse.");
    }
    // 访问 Keyboard 对象
    @Override
    public void visit(Keyboard keyboard) {
        System.out.println("Displaying Keyboard.");
    }
}
// 元素接口
interface ComputerPart {
    // 接受访问者的访问
    void accept(ComputerPartVisitor computerPartVisitor);
}
// 具体元素
class Computer implements ComputerPart {
    // 子元素数组
    ComputerPart[] parts;
    public Computer() {
        // 初始化子元素数组
        parts = new ComputerPart[]{new Mouse(), new Keyboard()};
    }
    // 接受访问者的访问
    @Override
    public void accept(ComputerPartVisitor computerPartVisitor) {
        // 遍历所有子元素并接受访问者的访问
        for (int i = 0; i < parts.length; i++) {
            parts[i].accept(computerPartVisitor);
        }
        // 访问 Computer 对象本身
        computerPartVisitor.visit(this);
    }
}
// 具体元素:鼠标
class Mouse implements ComputerPart {
    // 接受访问者的访问
    @Override
    public void accept(ComputerPartVisitor computerPartVisitor) {
        // 访问 Mouse 对象
        computerPartVisitor.visit(this);
    }
}
// 具体元素:键盘
class Keyboard implements ComputerPart {
    // 接受访问者的访问
    @Override
    public void accept(ComputerPartVisitor computerPartVisitor) {
        // 访问 Keyboard 对象
        computerPartVisitor.visit(this);
    }
}
// 客户端代码
public class VisitorPatternDemo {
    public static void main(String[] args) {
        // 创建一个 Computer 对象
        ComputerPart computer = new Computer();
        // 创建一个具体访问者
        ComputerPartVisitor visitor = new ComputerPartDisplayVisitor();
        // 让 Computer 对象接受访问者的访问
        computer.accept(visitor);
    }
}

在这个示例中,我们定义了一个表示计算机组件的类层次结构,包括 Computer 、Mouse 和 Keyboard 。这些类实现了 ComputerPart 接口,该接口声明了一个接受访问者的方法。我们还定义了一个 ComputerPartVisitor 接口,用于访问这些计算机组件,并为每种组件类型声明了一个访问操作。

ComputerPartDisplayVisitor 类实现了 ComputerPartVisitor 接口,为每种计算机组件提供了展示详细信息的功能。在客户端代码中,我们创建了一个Computer 对象和一个 ComputerPartDisplayVisitor 对象。当我们调用computer.accept() 方法时,计算机的所有组件都会被访问者访问,并显示相应的详细信息。

这个示例展示了如何使用访问者模式将功能与类结构分离,实现解耦和扩展性 。如果我们需要为计算机组件添加新功能,只需创建一个新的访问者类,而无需修改现有的****组件类。这使得在不影响现有代码的情况下,为系统添加新功能变得容易。

java 复制代码
// 添加一个更新计算机部件的访问者实现
class ComputerPartUpdateVisitorImpl implements ComputerPartVisitor {
    // 访问 Computer 对象并执行更新操作
    @Override
    public void visit(Computer computer) {
        System.out.println("Updating Computer.");
    }
    // 访问 Mouse 对象并执行更新操作
    @Override
    public void visit(Mouse mouse) {
        System.out.println("Updating Mouse.");
    }
    // 访问 Keyboard 对象并执行更新操作
    @Override
    public void visit(Keyboard keyboard) {
        System.out.println("Updating Keyboard.");
    }
}
// 客户端代码,几乎不用任何修改
public class VisitorPatternDemo {
    public static void main(String[] args) {
        // 创建一个 Computer 对象
        ComputerPart computer = new Computer();
        // 创建一个具体访问者
        ComputerPartVisitor visitor = new ComputerPartUpdateVisitorImpl();
        // 让 Computer 对象接受访问者的访问
        computer.accept(visitor);
    }
}

访问者模式可以算是 23 种经典设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式。

2、使用场景

2.1 抽象语法树

访问者模式在实际项目中的一个常见使用场景是处理抽象语法树(AST)。例如,在编译器或解释器中,我们需要处理不同类型的语法结构,如声明、表达式、循环等。 使用访问者模式,我们可以将处理这些结构的功能与结构类分离 ,实现解耦和扩展性。
以下是一个简单的示例,展示了如何使用访问者模式处理抽象语法树

java 复制代码
// AST 节点基类
abstract class AstNode {
    // 接受访问者的方法
    abstract void accept(AstVisitor visitor);
}
// 访问者接口
interface AstVisitor {
    // 访问表达式节点的方法
    void visit(ExpressionNode node);
    // 访问数字节点的方法
    void visit(NumberNode node);
    // 访问加法节点的方法
    void visit(AdditionNode node);
// 省略其他节点
}
// 数字节点,表示一个整数值
class NumberNode extends AstNode {
    int value;
    // 构造方法,接收一个整数作为值
    NumberNode(int value) {
        this.value = value;
    }
    // 实现基类的 accept 方法,接受访问者
    void accept(AstVisitor visitor) {
        visitor.visit(this);
    }
}
// 加法节点,表示两个子节点的相加
class AdditionNode extends AstNode {
    AstNode left;
    AstNode right;
    // 构造方法,接收两个子节点
    AdditionNode(AstNode left, AstNode right) {
        this.left = left;
        this.right = right;
    }
    // 实现基类的 accept 方法,接受访问者
    void accept(AstVisitor visitor) {
        visitor.visit(this);
    }
}
// 表达式节点,包含一个子节点
class ExpressionNode extends AstNode {
    AstNode node;
    // 构造方法,接收一个子节点
    ExpressionNode(AstNode node) {
        this.node = node;
    }
    // 实现基类的 accept 方法,接受访问者
    void accept(AstVisitor visitor) {
        visitor.visit(this);
    }
}

现在,我们可以创建一个实现了 AstVisitor 接口的类,用于遍历 AST 并计算其结果:

java 复制代码
class AstEvaluator implements AstVisitor {
    int result;
    AstEvaluator() {
        result = 0;
    }
    void visit(ExpressionNode node) {
        node.node.accept(this);
    }
    void visit(NumberNode node) {
        result = node.value;
    }
    void visit(AdditionNode node) {
        node.left.accept(this);
        int leftValue = result;
        node.right.accept(this);
        int rightValue = result;
        result = leftValue + rightValue;
    }
}

最后,我们可以使用这个访问者类计算一个简单的 AST

java 复制代码
public class Main {
    public static void main(String[] args) {
        // 创建一个简单的 AST:(2 + 3)
        AstNode ast = new ExpressionNode(
                new AdditionNode(
                        new NumberNode(2),
                        new NumberNode(3)
                )
        );
        // 创建一个访问者实例
        AstEvaluator evaluator = new AstEvaluator();
        // 使用访问者计算 AST 的结果
        ast.accept(evaluator);
        // 输出计算结果
        System.out.println("AST 的结果是: " + evaluator.result);
    }
}

这个示例将输出 AST 的结果是 : 5 。

八、备忘录模式

1、原理与实现

备忘录模式也叫快照(Snapshot)模式,英文翻译是Memento Design Pattern .在GoF的 设计模式 一书中,备忘录模式是这么定义的:

Captures and externalizes an object's internal state so that it can be restored later, all without violating encapsulation.

翻译成中文就是:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

这个模式的定义主要表达的两部分,一部分是,储存副本以便后期恢复,这一部分很好理解。另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复实现撤销(Undo)和 恢复(Redo)操作。结合例子解释一下:

备忘录设计模式设计三个主要组件:

1)发起人(Originator):这是我们希望保存的状态的对象。它可以创建一个备忘录(Memento)对象来保存其当前状态,并可以使用备忘录来恢复先前的状态。

2)备忘录(Memento):这个对象存储了发起人的内部状态。他应该有一个足够的状态,以便发起人可以恢复到之前的状态。备忘录类通常具有私有的访问权限。仅发起人可以访问的其内部状态。

3)负责人(Caretaker):这个对象负责保存和恢复备忘录。他不应该对备忘录的内容进行任何操作,只是将备忘录传递给发起人。

下面是一个使用 Java 实现的简单备忘录设计模式的示例:

java 复制代码
// 发起人类
class Originator {
    private String state;
    public void setState(String state) {
        this.state = state;
    }
    public String getState() {
        return state;
    }
    // 创建备忘录
    public Memento saveStateToMemento() {
        return new Memento(state);
    }
    // 恢复状态
    public void getStateFromMemento(Memento memento) {
        state = memento.getState();
    }
}
// 备忘录类
class Memento {
    private final String state;
    public Memento(String state) {
        this.state = state;
    }
    public String getState() {
        return state;
    }
}
// 负责人类
class Caretaker {
    private final List<Memento> mementoList = new ArrayList<>();
    public void add(Memento state) {
        mementoList.add(state);
    }
    public Memento get(int index) {
        return mementoList.get(index);
    }
}
public class Main {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();
        // 设置状态并保存到备忘录
        originator.setState("State1");
        caretaker.add(originator.saveStateToMemento());
        originator.setState("State2");
        caretaker.add(originator.saveStateToMemento());
        // 从备忘录中恢复状态
        originator.getStateFromMemento(caretaker.get(0));
        System.out.println("恢复的状态: " + originator.getState()); // 输出:恢复的状态: State1
        originator.getStateFromMemento(caretaker.get(1));
        System.out.println("恢复的状态: " + originator.getState()); // 输出:恢复的状态: State2
    }
}

这个例子演示了如何使用备忘录设计模式保存和恢复对象的状态。在这个例子中,我们创建了一个 Originator 类来存储状态并创建备忘录对象。 Memento 类用于保存 Originator 的状态。 Caretaker 类负责保存和恢复备忘录对象。

这个例子展示了如何在需要时从备忘录中恢复对象的状态。这种设计模式在撤销和重做操作时非常有用,因为它允许我们恢复到先前的状态,而不需要修改或者重新实现对象的行为。

下边我们举一个小例子:

假设有这样一道面试题,希望你编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入":list",程序在命令行中输出内存文本的内容;用户输入":undo",程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉。
我举了个小例子来解释一下这个需求,如下所示:

> hello
> : list
hello
> world
> : list
helloworld
> : undo
> : list
hello

怎么来编程实现呢?你可以打开 IDE 自己先试着编写一下,然后再看我下面的讲解。整体上来讲,这个小程序实现起来并不复杂。我写了一种实现思路,如下所示

java 复制代码
// 文本缓冲区类,用于存储文本并管理撤销操作
class TextBuffer {
    private StringBuilder text;
    public TextBuffer() {
        text = new StringBuilder();
    }
    // 向文本缓冲区追加新文本
    public void append(String newText) {
        text.append(newText);
    }
    // 获取文本缓冲区的内容
    public String getText() {
        return text.toString();
    }
    // 从历史记录恢复文本
    public void restoreText(String previousText) {
        text = new StringBuilder(previousText);
    }
}
public class Main {
    public static void main(String[] args) {
        TextBuffer textBuffer = new TextBuffer();
        // 我们搞一个list,当做栈来使用记录历史记录
        List<String> history = new ArrayList<>();
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("请输入命令:");
            String input = scanner.nextLine();
            // 列出当前文本缓冲区的内容
            if (":list".equals(input)) {
                System.out.println(textBuffer.getText());
            }
            // 撤销上一次输入的文本
            else if (":undo".equals(input)) {
                if (!history.isEmpty()) {
                    history.remove(history.size() - 1);
                    textBuffer.restoreText(history.isEmpty() ? "" : history.get(history.size()
                            - 1));
                } else {
                    System.out.println("无法撤销,没有更早的历史记录。");
                }
            }
            // 将用户输入的文本追加到文本缓冲区
            else {
                history.add(textBuffer.getText());
                textBuffer.append(input);
            }
        }
    }
}

实际上,备忘录模式的实现很灵活,也没有很固定的实现方式,在不同的业务需求、不同编程语言下,代码实现可能都不大一样。上面的代码基本上已经实现了最基本的备忘录的功能。List集合中存储的历史记录本质就是一个个的备忘录。

但是,如果我们深究一下的话,还有一些问题要解决:
第一,使用 List<String> 记录历史,扩展性很差,一旦需要记录更多内容,则必须修改原始代码。
第二,集合中的备忘信息不具备封装性,有被篡改的风险。

针对以上问题,我们对代码做两点修改。
其一,定义一个独立的类(Memento类)来表示备忘录,而不是使用 String 类或者其他。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。
其二,在 TextBuffer类中,我们把 setText() 方法重命名为 restoreFromMemento()方法,用意更加明确,只用来恢复对象。
按照这个思路,我们对代码进行重构。重构之后的代码如下所示:

java 复制代码
class TextBuffer {
    private StringBuilder text;
    public TextBuffer() {
        text = new StringBuilder();
    }
    public void append(String newText) {
        text.append(newText);
    }
    public String getText() {
        return text.toString();
    }
    public Memento saveToMemento() {
        return new Memento(text.toString());
    }
    public void restoreFromMemento(Memento memento) {
        text = new StringBuilder(memento.getSavedText());
    }
    // 备忘录
    static class Memento {
        private final String savedText;
        public Memento(String savedText) {
            this.savedText = savedText;
        }
        public String getSavedText() {
            return savedText;
        }
    }
}
public class Main {
    public static void main(String[] args) {
        TextBuffer textBuffer = new TextBuffer();
        List<TextBuffer.Memento> history = new ArrayList<>();
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("请输入命令:");
            String input = scanner.nextLine();
            if (":list".equals(input)) {
                System.out.println(textBuffer.getText());
            } else if (":undo".equals(input)) {
                if (!history.isEmpty()) {
                    history.remove(history.size() - 1);
                    textBuffer.restoreFromMemento(history.isEmpty() ? new
                            TextBuffer.Memento("") : history.get(history.size() - 1));
                } else {
                    System.out.println("无法撤销,没有更早的历史记录。");
                }
            } else {
                history.add(textBuffer.saveToMemento());
                textBuffer.append(input);
            }
        }
    }
}

实际上,上面的代码实现就是典型的备忘录模式的代码实现,也是很多书籍(包括GoF 的《设计模式》)中给出的实现方法。

事实上,很多场景下我们使用第一种方式就能满足绝大部分需求,我们要知道,设计模式不是万金油有时候简单的需求使用简单的编码就可以实现,很多时候不必要为了扩展而扩展,为了设计而设计,编码简单也是很重要的。

2、备份优化

前面我们只是简单介绍了备忘录模式的原理和经典实现,现在我们再继续深挖一下。 如果要备份的对象数据比较大,备份频率又比较高,那快照占用的内存会比较大,备份和恢复的耗时会比较长。这个问题该如何解决呢?

不同的应用场景下有不同的解决方法。比如,我们前面举的那个例子,应用场景是利用备忘录来实现撤销操作,而且仅仅支持顺序撤销,也就是说,每次操作只能撤销上一次的输入,不能跳过上次输入撤销之前的输入。在具有这样特点的应用场景下,为了节省内存,我们不需要在快照中存储完整的文本,只需要记录少许信息,比如在获取快照当下的文本长度,用这个值结合 InputText 类对象存储的文本来做撤销操作。

我们再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘) 的消耗,还是对时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会 采用**"低频率全量备份""高频率增量备份"**相结合的方法。

全量备份就不用讲了,它跟我们上面的例子类似,就是把所有的数据"拍个快照"保存下来。所谓"增量备份",指的是记录每次操作或数据变动。

当我们需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就先找到最近的一次全量备份,然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。

九、命令模式

1、概述和原理

命令模式、解释器模式、中介模式。这 3 个模式使用频率低、理解难度大,只在非常特定的应用场景下才会用到,所以,不是我们学习的重点,你只需要稍微了解,见了能认识就可以了。

命令模式的英文翻译是 Command Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:

The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations.

命令模式(Command Pattern)是一种行为设计模式,它将请求封装为一个对象 ,从而使得调用请求的客户端处理请求的服务端解耦。这使得我们可以将具体的请求、调用者和接收者进行更灵活的组合。命令模式常用于实现任务队列、历史记录(如撤销、重做操作)等场景。

命令模式的主要组成部分有以下几个:

  1. 命令接口(Command):定义了一个执行操作的接口,通常包含一个名为execute() 的方法。
  2. 具体命令(ConcreteCommand):实现命令接口的具体类,包含一个接收者 (Receiver)对象的引用,并在 execute() 方法中调用接收者的相应操作。
  3. 调用者(Invoker):负责调用命令对象的 execute() 方法。调用者并不需要了解命令是如何执行的,只需知道命令接口即可。
  4. 接收者(Receiver):负责执行与命令相关的具体操作。接收者对象通常是一个特定的业务对象。

命令模式的优点有:
降低系统的耦合度,使得调用者和接收者之间的依赖关系更加清晰。
可以对请求进行排队、记录日志、撤销和重做等操作。
可以将复杂的业务逻辑分解为较小的、更易于管理的部分,提高代码的可维护性。

下面是一个简单的 Java 代码示例,展示了命令模式的使用:

java 复制代码
// 命令接口
interface Command {
    void execute();
}
// 具体命令类
class LightOnCommand implements Command {
    private Light light;
    public LightOnCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        light.turnOn();
    }
}
// 接收者
class Light {
    public void turnOn() {
        System.out.println("Light is on");
    }
    public void turnOff() {
        System.out.println("Light is off");
    }
}
// 请求者
class RemoteControl {
    private Command command;
    public void setCommand(Command command) {
        this.command = command;
    }
    public void pressButton() {
        command.execute();
    }
}
public class Main {
    public static void main(String[] args) {
        // 创建一个灯
        Light light = new Light();
        // 创建一个开灯命令
        Command lightOnCommand = new LightOnCommand(light);
        RemoteControl remoteControl = new RemoteControl();
        remoteControl.setCommand(lightOnCommand);
        remoteControl.pressButton();
    }
}

在这个示例中,我们创建了一个简单的家庭自动化系统,可以通过遥控器控制灯光的开关。 Light 类是接收者, LightOnCommand 类是具体命令, RemoteControl 类是请求者。我们将命令对象设置到遥控器中,然后按下遥控器的按钮来执行命令。

这样,遥控器可以控制灯光,而不需要知道如何实现灯光的开关操作。 使用命令模式有以下好处:

  1. 解耦:命令模式将请求发送者(Invoker)与请求接收者(Receiver)解耦,请 求发送者不需要知道接收者的具体实现,只需知道如何发送请求。这样可以降低系统各部分之间的耦合度,使系统更易于维护和扩展。
  2. 可扩展性:新增命令时,只需实现一个新的具体命令类,而不需要修改请求发送者或请求接收者的代码。这使得系统更容易扩展,符合开闭原则。
  3. 灵活性:命令模式允许在运行时动态地更改命令。例如,在上面的示例中,我们可以在运行时为遥控器设置不同的命令,从而控制不同的设备。
  4. 可以实现宏命令:命令模式可以方便地实现宏命令(Macro Command),即一组命令的组合。宏命令可以按顺序执行多个命令,甚至可以实现撤销和重做功能。
  5. 命令历史和撤销功能:命令模式可以用于记录已执行的命令,从而实现命令历史、撤销和重做等功能。例如,文本编辑器可以使用命令模式记录用户的编辑操作,以便在需要时撤销或重做这些操作。
2、宏命令

下面是一个 Java 代码示例,展示了如何使用命令模式实现宏命令。在这个示例中, 我们扩展了之前的家庭自动化系统,使其可以同时控制灯光和空调。

java 复制代码
// 命令接口
interface Command {
    void execute();
}
// 具体命令类:开灯
class LightOnCommand implements Command {
    private Light light;
    public LightOnCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        light.turnOn();
    }
}
// 具体命令类:关灯
class LightOffCommand implements Command {
    private Light light;
    public LightOffCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        light.turnOff();
    }
}
// 接收者:灯
class Light {
    public void turnOn() {
        System.out.println("Light is on");
    }
    public void turnOff() {
        System.out.println("Light is off");
    }
}
// 具体命令类:开空调
class AirConditionerOnCommand implements Command {
    private AirConditioner airConditioner;
    public AirConditionerOnCommand(AirConditioner airConditioner) {
        this.airConditioner = airConditioner;
    }
    public void execute() {
        airConditioner.turnOn();
    }
}
// 具体命令类:关空调
class AirConditionerOffCommand implements Command {
    private AirConditioner airConditioner;
    public AirConditionerOffCommand(AirConditioner airConditioner) {
        this.airConditioner = airConditioner;
    }
    public void execute() {
        airConditioner.turnOff();
    }
}
// 接收者:空调
class AirConditioner {
    public void turnOn() {
        System.out.println("Air conditioner is on");
    }
    public void turnOff() {
        System.out.println("Air conditioner is off");
    }
}
// 宏命令类
class MacroCommand implements Command {
    private List<Command> commands;
    public MacroCommand() {
        commands = new ArrayList<>();
    }
    public void addCommand(Command command) {
        commands.add(command);
    }
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }
}
// 请求者:遥控器
class RemoteControl {
    private Command command;
    public void setCommand(Command command) {
        this.command = command;
    }
    public void pressButton() {
        command.execute();
    }
}
// 在 main 方法中添加关闭所有设备的宏命令
public class Main {
    public static void main(String[] args) {
        // ... 前面的代码省略
        // 创建宏命令:开启所有设备
        MacroCommand turnAllOnCommand = new MacroCommand();
        turnAllOnCommand.addCommand(lightOnCommand);
        turnAllOnCommand.addCommand(airConditionerOnCommand);
        // 创建宏命令:关闭所有设备
        MacroCommand turnAllOffCommand = new MacroCommand();
        turnAllOffCommand.addCommand(lightOffCommand);
        turnAllOffCommand.addCommand(airConditionerOffCommand);
        RemoteControl remoteControl = new RemoteControl();
        // 执行宏命令:开启所有设备
        System.out.println("Turn all devices on:");
        remoteControl.setCommand(turnAllOnCommand);
        remoteControl.pressButton();
        // 执行宏命令:关闭所有设备
        System.out.println("\nTurn all devices off:");
        remoteControl.setCommand(turnAllOffCommand);
        remoteControl.pressButton();
    }
}

在这个示例中,我们创建了两个宏命令: turnAllOnCommand 和 turnAllOffCommand 。 turnAllOnCommand 用于开启所有设备,而 turnAllOffCommand 用于关闭所有设备。通过为遥控器设置不同的宏命令,我们可以一键控制家中的多个设备。

这个示例展示了如何使用命令模式实现宏命令功能。通过将多个命令组合成一个宏命令,我们可以轻松地实现批量操作。此外,宏命令还可以与其他命令模式功能(例如撤销和重做)结合使用,实现更强大的功能。

3、历史命令和撤销

以下是一个使用命令模式实现命令历史和撤销功能的 Java 代码示例。在这个示例中,我们创建了一个简单的文本编辑器,支持添加文本、删除文本、撤销和重做操 作。

java 复制代码
// 命令接口
interface Command {
    void execute();
    void undo();
}
// 具体命令类:添加文本
class AddTextCommand implements Command {
    private StringBuilder textEditor;
    private String text;
    public AddTextCommand(StringBuilder textEditor, String text) {
        this.textEditor = textEditor;
        this.text = text;
    }
    public void execute() {
        textEditor.append(text);
    }
    public void undo() {
        textEditor.delete(textEditor.length() - text.length(), textEditor.length());
    }
}
// 请求者:文本编辑器
class TextEditor {
    private StringBuilder text;
    private Stack<Command> commandHistory;
    public TextEditor() {
        text = new StringBuilder();
        commandHistory = new Stack<>();
    }
    public void addText(String newText) {
        Command command = new AddTextCommand(text, newText);
        command.execute();
        commandHistory.push(command);
    }
    public void undo() {
        if (!commandHistory.isEmpty()) {
            Command lastCommand = commandHistory.pop();
            lastCommand.undo();
        }
    }
    public void printContent() {
        System.out.println(text.toString());
    }
}
public class Main {
    public static void main(String[] args) {
        TextEditor textEditor = new TextEditor();
        // 添加文本
        textEditor.addText("Hello, ");
        textEditor.addText("world!");
        textEditor.printContent(); // 输出:Hello, world!
        // 撤销操作
        textEditor.undo();
        textEditor.printContent(); // 输出:Hello,
        // 再次添加文本
        textEditor.addText("Command Pattern!");
        textEditor.printContent(); // 输出:Hello, Command Pattern!
    }
}

在这个示例中,我们创建了一个简单的文本编辑器 TextEditor ,它使用一个StringBuilder 对象存储文本内容,并使用一个栈( Stack )存储命令历史。添加文本操作对应的具体命令类是 AddTextCommand ,它实现了 execute() 和 undo() 方法。当执行 addText() 方法时,编辑器会创建一个 AddTextCommand 对象并执行它,然后将这个命令对象添加到命令历史栈中。当执行 undo() 方法 时,编辑器会从命令历史栈中弹出上一个命令对象并执行它的 undo() 方法,从而 撤销该命令。

这个示例展示了如何使用命令模式实现命令历史和撤销功能。通过维护一个命令历史栈,我们可以轻松地记录已执行的命令,并在需要时撤销这些命令。此外,这种方法还可以扩展为支持重做操作。 话不多说,让我们正式开始今天的学习吧!

4、使用场景

命令模式在实际开发中有很多应用场景,以下是一些常见的案例:

  1. 撤销和重做操作:在文本编辑器、图形编辑器等软件中,用户可能希望撤销或重做之前的操作。通过使用命令模式,我们可以将每个操作封装为一个命令对象,然后将它们存储在一个历史记录列表中。这样,撤销操作就可以通过反向执行命令列表中的命令来实现,而重做操作则可以通过正向执行命令列表来实现。

  2. 菜单和工具栏按钮:在图形用户界面中,菜单项和工具栏按钮通常与特定的操作相关联。命令模式允许我们将操作封装为命令对象,这样菜单项和按钮就可以通过调用命令对象的 execute() 方法来执行相应的操作。这种方式使得菜单和工具栏与底层的操作逻辑解耦,提高了系统的可扩展性和可维护性。

  3. 任务队列和后台任务:命令模式可以用于实现任务队列。例如,在一个多线程应用程序中,我们可以将不同的任务封装为命令对象,然后将它们添加到一个任务队列中。后台线程可以从队列中获取命令对象,并执行它们。这样可以实现对任务的并行处理和优先级控制。

  4. 宏(Macro):在一些软件中,用户可以创建宏来执行一系列操作。命令模式可以用于实现这种功能。每个操作都可以封装为一个命令对象,然后将这些命令对象组合成一个宏命令。用户可以通过执行宏命令来一次性执行所有操作。

  5. 电商系统中的订单处理:在电商系统中,我们可以将不同的订单操作(例如创建订单、取消订单、支付订单等)封装为命令对象。这样,处理这些操作的代码可以与具体的订单对象解耦,使得系统更加灵活和可维护。

  6. 智能家居系统:在智能家居系统中,各种家电设备(如灯光、空调、电视等)的操作可以被封装为命令对象。通过使用命令模式,用户可以通过一个统一的接口 (例如手机App或语音助手)来控制不同的设备,而无需关心设备的具体实现。

  7. 游戏开发:在游戏开发中,玩家的操作(如移动角色、使用技能等)可以被封装为命令对象。这样,我们可以将玩家操作与游戏逻辑解耦,实现更加灵活的游戏 控制和操作记录。

  8. 状态模式与命令模式结合:在某些场景下,可以将状态模式与命令模式结合使 用。例如,一个文档编辑器可能有多种编辑模式(如插入模式、选择模式等),每种模式对应不同的操作集合。我们可以将这些操作封装为命令对象,并根据当前的编辑模式来选择执行哪个命令。

十、解释器模式

1、解释器模式的原理和实现

解释器模式的英文翻译是 Interpreter Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的:

Interpreter pattern is used to defines a grammatical representation for a language and provides an interpreter to deal with this grammar.

翻译成中文就是:解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

解释器模式的主要组成部分如下:

  1. 抽象表达式(Abstract Expression):定义解释器的接口,规定了解释操作的方法。通常包含一个 interpret() 方法。
  2. 终结符表达式(Terminal Expression):实现抽象表达式接口的具体类,用于表示语法中的终结符。终结符是不可分解的最小单位,例如数字、变量等。
  3. 非终结符表达式(Nonterminal Expression):实现抽象表达式接口的具体类,用于表示语法中的非终结符。非终结符通常是由终结符组成的复杂结构,例如加法、乘法等。
  4. 上下文(Context):包含解释器需要的全局信息,例如变量与值的映射关系等。
  5. 客户端(Client):构建抽象语法树,然后调用解释器的 interpret() 方法来 、解释语法。

解释器模式的优点:
易于实现简单的文法:解释器模式可以方便地实现简单的文法表示和解释。
易于扩展新的文法:通过添加新的终结符表达式和非终结符表达式,可以方便地扩展解释器的功能。

解释器模式的缺点:
难以应对复杂的文法:对于复杂的文法,解释器模式可能导致大量的类创建,使得系统变得难以维护。
效率较低:解释器模式通过递归调用的方式解释语法,这可能导致较低的执行效率。

因此,在实际项目中,如果需要处理的问题可以表示为一种简单的语法结构,那么可以考虑使用解释器模式。然而,对于复杂的语法结构,应该寻找其他更合适的方法,如使用编译器生成器(如 ANTLR)等工具。

以下是一个简单的计算器示例,使用解释器模式解释由数字和加法、减法操作符组成的算术表达式:

java 复制代码
// 抽象表达式
interface Expression {
    int interpret(Map<String, Integer> variables);
}
// 终结符表达式:变量
class Variable implements Expression {
    private String name;
    public Variable(String name) {
        this.name = name;
    }
    @Override
    public int interpret(Map<String, Integer> variables) {
        return variables.getOrDefault(name, 0);
    }
}
// 非终结符表达式:加法
class Add implements Expression {
    private Expression left;
    private Expression right;
    public Add(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    @Override
    public int interpret(Map<String, Integer> variables) {
        return left.interpret(variables) + right.interpret(variables);
    }
}
// 非终结符表达式:减法
class Subtract implements Expression {
    private Expression left;
    private Expression right;
    public Subtract(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    @Override
    public int interpret(Map<String, Integer> variables) {
        return left.interpret(variables) - right.interpret(variables);
    }
}
public class Main {
    public static void main(String[] args) {
        // 构建语法树
        Expression left = new Variable("a");
        Expression right = new Variable("b");
        Expression add = new Add(left, right);
        Expression subtract = new Subtract(add, right);
        // 设置变量值
        Map<String, Integer> variables = new HashMap<>();
        variables.put("a", 10);
        variables.put("b", 5);
        // 解释表达式
        int result = subtract.interpret(variables);
        System.out.println("Result: " + result); // 输出:Result: 10
    }
}

在这个示例中,我们定义了一个简单的算术表达式语言,包括变量、加法和减法操作。 Expression 是抽象表达式接口, Variable 是终结符表达式,表示变量; Add 和 Subtract 是非终结符表达式,表示加法和减法操作。

客户端通过构建语法树来表示算术表达式。在本例中,我们构建了一个表示 (a + b) - b 的语法树。然后,客户端设置变量值,并使用 interpret() 方法解释表达式。最后,输出解释结果:10。

这个示例展示了如何使用解释器模式解释一个简单的算术表达式。解释器模式使得表示和解释这类语言变得容易,同时支持语法规则的修改和扩展。然而,对于复杂的、频繁变化的语法规则,请谨慎使用解释器模式。

解释器设计模式的优势和好处如下:

  1. 易于扩展和修改 :解释器模式的语法规则通常表示为一棵语法树,因此可以轻松地扩展和修改这些规则,而不会影响其他部分的代码。例如,可以添加新的终结符或非终结符,或者修改某些规则的解释方式。
  2. 灵活性和可重用性 :解释器模式使得语法规则和解释器之间的耦合度降低,从而使得语法规则和解释器可以独立地变化和重用。这种灵活性和可重用性使得解释器模式非常适用于需要频繁修改和扩展语法规则的场景。
  3. 可读性和可维护性 :解释器模式的语法规则通常表示为类似于语法树的结构,这使得代码更加易于理解和维护。在实际开发中,通过使用解释器模式,可以将复杂的算法或规则分解成简单的表达式和操作,从而提高代码的可读性和可维护性。
  4. 提高安全性和可靠性:解释器模式通常会检查输入是否符合语法规则,从而提高系统的安全性和可靠性。在某些场景下,解释器模式还可以用于验证和过滤用户输入。
2、解释器模式实战举例

在我们平时的项目开发中,监控系统非常重要,它可以时刻监控业务系统的运行情况,及时将异常报告给开发者。比如,如果每分钟接口出错数超过 100,监控系统就通过短信、微信、邮件等方式发送告警给开发者。

一般来讲,监控系统支持开发者自定义告警规则,比如我们可以用下面这样一个表达式,来表示一个告警规则,它表达的意思是:每分钟 API 总出错数超过 100 或者每分钟 API 总调用数超过 10000 就触发告警。

"api_error_per_minute > 100 || api_count_per_minute > 10000"

在监控系统中,告警模块只负责根据统计数据和告警规则,判断是否触发告警。至于每分钟 API 接口出错数、每分钟接口调用数等统计数据的计算,是由其他模块来负责的。其他模块将统计数据放到一个 Map 中(数据的格式如下所示),发送给告警模块。接下来,我们只关注告警模块。
Map < String , Long > apiStat = new HashMap <> ();
apiStat . put ( "api_error_per_minute" , 103 );
apiStat . put ( "api_count_per_minute" , 987 );

为了简化讲解和代码实现,我们假设自定义的告警规则只包含"||、&&、>、<、==" 这五个运算符,其中,">、<、=="运算符的优先级高于"||、&&"运算符,"&&"运算符优先级高于"||"。在表达式中,任意元素之间需要通过空格来分隔。除此之外,用户可以自定义要监控的 key,比如前面的 api_error_per_minute、api_count_per_minute。

我们可以把自定义的告警规则,看作一种特殊"语言"的语法规则。我们实现一个解释器,能够根据规则,针对用户输入的数据,判断是否触发告警。利用解释器模式,我们把解析表达式的逻辑拆分到各个小类中,避免大而复杂的大类的出现。按照这个实现思路,我把刚刚的代码补全,如下所示,你可以拿你写的代码跟我写的对比一下。

java 复制代码
public interface Expression {
    boolean interpret(Map<String, Long> stats);
}
public class GreaterExpression implements Expression {
    private String key;
    private long value;
    public GreaterExpression(String strExpression) {
        String[] elements = strExpression.trim().split("\\s+");
        if (elements.length != 3 || !elements[1].trim().equals(">")) {
            throw new RuntimeException("Expression is invalid: " + strExpression);
        }
        this.key = elements[0].trim();
        this.value = Long.parseLong(elements[2].trim());
    }
    public GreaterExpression(String key, long value) {
        this.key = key;
        this.value = value;
    }
    @Override
    public boolean interpret(Map<String, Long> stats) {
        if (!stats.containsKey(key)) {
            return false;
        }
        long statValue = stats.get(key);
        return statValue > value;
    }
}
// LessExpression/EqualExpression跟GreaterExpression代码类似,这里就省略
了
public class AndExpression implements Expression {
    private List<Expression> expressions = new ArrayList<>();
    public AndExpression(String strAndExpression) {
        String[] strExpressions = strAndExpression.split("&&");
        for (String strExpr : strExpressions) {
            if (strExpr.contains(">")) {
                expressions.add(new GreaterExpression(strExpr));
            } else if (strExpr.contains("<")) {
                expressions.add(new LessExpression(strExpr));
            } else if (strExpr.contains("==")) {
                expressions.add(new EqualExpression(strExpr));
            } else {
                throw new RuntimeException("Expression is invalid: " +
                        strAndExpression);
            }
        }
    }
    public AndExpression(List<Expression> expressions) {
        this.expressions.addAll(expressions);
    }
    @Override
    public boolean interpret(Map<String, Long> stats) {
        for (Expression expr : expressions) {
            if (!expr.interpret(stats)) {
                return false;
            }
        }
        return true;
    }
}
public class OrExpression implements Expression {
    private List<Expression> expressions = new ArrayList<>();
    public OrExpression(String strOrExpression) {
        String[] andExpressions = strOrExpression.split("\\|\\|");
        for (String andExpr : andExpressions) {
            expressions.add(new AndExpression(andExpr));
        }
    }
    public OrExpression(List<Expression> expressions) {
        this.expressions.addAll(expressions);
    }
    @Override
    public boolean interpret(Map<String, Long> stats) {
        for (Expression expr : expressions) {
            if (expr.interpret(stats)) {
                return true;
            }
        }
        return false;
    }
}
public class AlertRuleInterpreter {
    private Expression expression;
    public AlertRuleInterpreter(String ruleExpression) {
        this.expression = new OrExpression(ruleExpression);
    }
    public boolean interpret(Map<String, Long> stats) {
        return expression.interpret(stats);
    }
}

十一、中介者模式

1、原理和实现

中介模式的英文翻译是 Mediator Design Pattern。在 GoF 中的《设计模式》一书中,它是这样定义的:

Mediator pattern defines a separate (mediator) object that encapsulates the interaction between a set of objects and the objects delegate their interaction to a mediator object instead of interacting with each other directly.

翻译成中文就是:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

中介者模式是一种行为设计模式,它用于降低多个对象之间的通信复杂性。这种模式通过引入一个中介者对象来处理对象之间的通信,使得对象之间不需要直接相互引用,从而降低它们之间的耦合度。

以下是中介者模式的一些关键组件:

  1. 中介者(Mediator):定义了一个接口,用于与各个同事(Colleague)对象通信。
  2. 具体中介者(Concrete Mediator):实现中介者接口并协调各个同事对象之间的交互。
  3. 同事(Colleague):定义了各个对象之间的接口。每个同事对象知道它的中介者对象,但不知道其他同事对象。
  4. 具体同事(Concrete Colleague):实现同事接口并通过中介者与其他同事对象 通信。

实际上,中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。

下面是一个简单的Java代码实例:

java 复制代码
interface Mediator {
    void send(String message, Colleague colleague);
}
// 具体中介者
class ConcreteMediator implements Mediator {
    private Colleague1 colleague1;
    private Colleague2 colleague2;
    public void setColleague1(Colleague1 colleague1) {
        this.colleague1 = colleague1;
    }
    public void setColleague2(Colleague2 colleague2) {
        this.colleague2 = colleague2;
    }
    @Override
    public void send(String message, Colleague colleague) {
        if (colleague == colleague1) {
            colleague2.notify(message);
        } else {
            colleague1.notify(message);
        }
    }
}
// 同事接口
abstract class Colleague {
    protected Mediator mediator;
    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }
    public abstract void send(String message);
    public abstract void notify(String message);
}
// 具体同事1
class Colleague1 extends Colleague {
    public Colleague1(Mediator mediator) {
        super(mediator);
    }
    @Override
    public void send(String message) {
        mediator.send(message, this);
    }
    @Override
    public void notify(String message) {
        System.out.println("Colleague1 receives message: " + message);
    }
}
// 具体同事2
class Colleague2 extends Colleague {
    public Colleague2(Mediator mediator) {
        super(mediator);
    }
    @Override
    public void send(String message) {
        mediator.send(message, this);
    }
    @Override
    public void notify(String message) {
        System.out.println("Colleague2 receives message: " + message);
    }
}
// 客户端代码
public class MediatorPatternDemo {
    public static void main(String[] args) {
        ConcreteMediator mediator = new ConcreteMediator();
        Colleague1 colleague1 = new Colleague1(mediator);
        Colleague2 colleague2 = new Colleague2(mediator);
        mediator.setColleague1(colleague1);
        mediator.setColleague2(colleague2);
        colleague1.send("Hello, Colleague2!");
        colleague2.send("Hi, Colleague1! How are you?");
        colleague1.send("I'm fine, thank you!");
        colleague2.send("Great, have a nice day!");
    }
}

在这个例子中,我们让 Colleague1 和 Colleague2 对象之间互相发送消息。在主函数中,我们创建了一个 ConcreteMediator 对象,并将其分别传递给 Colleague1 和 Colleague2 对象。然后,我们使用中介者对象将两个同事对象关联起来。最后,我们让同事对象互相发送消息。

输出结果如下:

Colleague2 receives message: Hello, Colleague2!
Colleague1 receives message: Hi, Colleague1! How are you?
Colleague2 receives message: I'm fine, thank you!
Colleague1 receives message: Great, have a nice day!

通过使用中介者模式,我们实现了 Colleague1 和 Colleague2 对象之间的解耦。这样,如果我们需要在系统中引入更多的同事对象,只需创建一个新的具体同事类并在中介者中实现适当的逻辑即可。这大大降低了对象之间的耦合度,提高了代码的可维护性和可扩展性。

2、使用场景

我们可以像chatgpt咨询,这个模式有哪些使用场景,他告诉我们一下的答案:

  1. 聊天室:在一个聊天室应用中,用户之间需要相互发送消息。使用中介者模式, 我们可以创建一个中介者对象(聊天室服务器)来管理用户之间的消息传递,这样每个用户不需要知道其他用户的存在,只需将消息发送给中介者,然后由中介者负责分发消息。
  2. 机场调度系统:机场调度系统通常需要管理多个飞机、跑道和门等资源。在这种情况下,可以使用一个中介者(如调度塔)来协调和管理这些资源之间的通信和安排,以确保系统的正常运行。
  3. 图形用户界面(GUI)组件:在复杂的GUI应用程序中,各种组件(如按钮、列表框、文本框等)可能需要相互通信。使用中介者模式,我们可以创建一个中介者对象来处理这些组件之间的通信,避免组件之间直接相互引用,降低它们之间的耦合度。
  4. 智能家居系统:在智能家居系统中,各种设备(如灯、窗帘、空调等)需要根据用户的需求进行协调和控制。通过引入一个中介者(如智能家居中心),我们可以实现各种设备之间的协调和通信,从而实现对整个系统的统一管理。
  5. MVC(Model-View-Controller)架构:在MVC架构中,控制器(Controller)充当中介者的角色,协调模型(Model)和视图(View)之间的交互。这样,模型和视图之间的耦合度降低,使得它们可以独立地进行修改和扩展。
  6. 网络游戏:在多人在线游戏中,游戏服务器可以充当中介者,管理玩家之间的通信和交互。这样,每个玩家只需要与游戏服务器通信,而不必知道其他玩家的详细信息。
  7. 电子商务平台:在电子商务平台中,买家和卖家需要进行交易。平台可以作为一个中介者,处理买家和卖家之间的通信和交易,包括订单处理、支付、退款等。 这样,买家和卖家之间不需要直接进行通信,降低了交易的复杂性。
  8. 事件总线:在事件驱动的系统中,事件总线可以作为一个中介者,负责处理和分发事件。系统中的组件可以将事件发布到事件总线,而不必直接与其他组件通信。这样,组件之间的耦合度降低,系统变得更加模块化。
  9. 工作流引擎:在工作流引擎中,各个流程节点之间需要进行通信和协调。工作流引擎可以作为一个中介者,负责管理流程节点之间的通信,确保整个流程的正确执行。
    这些案例展示了中介者模式在不同领域和场景中的应用。通过引入中介者,我们可以降低对象之间的耦合度,简化系统的设计和维护。

2.1 多人游戏案例

在这个案例中,我们将展示如何在多人在线游戏中使用中介者模式。我们将创建一个简单的游戏,如五子棋,其中有两个玩家,分别为PlayerA和PlayerB。游戏服务器 (GameServer)将充当中介者,负责处理玩家之间的消息传递。

首先,我们定义中介者接口:

java 复制代码
interface GameServerMediator {
    void sendMessage(String message, Player player);
}

接下来,实现具体的中介者类:

java 复制代码
class GameServer implements GameServerMediator {
    private PlayerA playerA;
    private PlayerB playerB;
    public void setPlayerA(PlayerA playerA) {
        this.playerA = playerA;
    }
    public void setPlayerB(PlayerB playerB) {
        this.playerB = playerB;
    }
    @Override
    public void sendMessage(String message, Player player) {
        if (player == playerA) {
            playerB.receiveMessage(message);
        } else {
            playerA.receiveMessage(message);
        }
    }
}

定义玩家抽象类:

java 复制代码
abstract class Player {
    protected GameServerMediator mediator;
    public Player(GameServerMediator mediator) {
        this.mediator = mediator;
    }
    public abstract void send(String message);
    public abstract void receiveMessage(String message);
}

实现具体的玩家类:

java 复制代码
class PlayerA extends Player {
    public PlayerA(GameServerMediator mediator) {
        super(mediator);
    }
    @Override
    public void send(String message) {
        mediator.sendMessage(message, this);
    }
    @Override
    public void receiveMessage(String message) {
        System.out.println("PlayerA收到消息: " + message);
    }
}
class PlayerB extends Player {
    public PlayerB(GameServerMediator mediator) {
        super(mediator);
    }
    @Override
    public void send(String message) {
        mediator.sendMessage(message, this);
    }
    @Override
    public void receiveMessage(String message) {
        System.out.println("PlayerB收到消息: " + message);
    }
}

最后,在客户端代码中创建玩家对象并进行通信:

java 复制代码
public class MultiplayerGameDemo {
    public static void main(String[] args) {
        GameServer gameServer = new GameServer();
        PlayerA playerA = new PlayerA(gameServer);
        PlayerB playerB = new PlayerB(gameServer);
        gameServer.setPlayerA(playerA);
        gameServer.setPlayerB(playerB);
        playerA.send("你好,PlayerB!");
        playerB.send("你好,PlayerA! 一起玩游戏吗?");
        playerA.send("好的,我们开始吧!");
    }
}

输出结果如下:

PlayerB 收到消息 : 你好, PlayerB!
PlayerA 收到消息 : 你好, PlayerA! 一起玩游戏吗?
PlayerB 收到消息 : 好的,我们开始吧

在这个例子中,我们创建了一个简单的多人在线游戏,其中GameServer充当中介者,负责管理PlayerA和PlayerB之间的通信。玩家对象不直接相互通信,而是通过 GameServer进行消息传递。这样,如果我们需要添加更多玩家,只需在GameServer中进行相应的修改即可,而无需修改现有的玩家类。这降低了系统的耦合度,提高了可扩展性和可维护性。

例如,假设我们要在游戏中添加第三个玩家PlayerC。首先,我们需要创建一个新的玩家类:

java 复制代码
class PlayerC extends Player {
    public PlayerC(GameServerMediator mediator) {
        super(mediator);
    }
    @Override
    public void send(String message) {
        mediator.sendMessage(message, this);
    }
    @Override
    public void receiveMessage(String message) {
        System.out.println("PlayerC收到消息: " + message);
    }
}

然后,我们需要更新GameServer类以支持第三个玩家:

java 复制代码
class GameServer implements GameServerMediator {
    private PlayerA playerA;
    private PlayerB playerB;
    private PlayerC playerC;
    // ...省略setPlayerA()和setPlayerB()方法...
    public void setPlayerC(PlayerC playerC) {
        this.playerC = playerC;
    }
    @Override
    public void sendMessage(String message, Player player) {
        if (player == playerA) {
            playerB.receiveMessage(message);
            playerC.receiveMessage(message);
        } else if (player == playerB) {
            playerA.receiveMessage(message);
            playerC.receiveMessage(message);
        } else {
            playerA.receiveMessage(message);
            playerB.receiveMessage(message);
        }
    }
}

最后,在客户端代码中添加第三个玩家,并进行通信:

java 复制代码
public class MultiplayerGameDemo {
    public static void main(String[] args) {
        GameServer gameServer = new GameServer();
        PlayerA playerA = new PlayerA(gameServer);
        PlayerB playerB = new PlayerB(gameServer);
        PlayerC playerC = new PlayerC(gameServer);
        gameServer.setPlayerA(playerA);
        gameServer.setPlayerB(playerB);
        gameServer.setPlayerC(playerC);
        playerA.send("你好,PlayerB和PlayerC!");
        playerB.send("你好,PlayerA和PlayerC! 一起玩游戏吗?");
        playerC.send("我也加入!让我们开始吧!");
    }
}

输出结果如下:

makefileCopy code
PlayerB 收到消息 : 你好, PlayerB 和 PlayerC!
PlayerC 收到消息 : 你好, PlayerB 和 PlayerC!
PlayerA 收到消息 : 你好, PlayerA 和 PlayerC! 一起玩游戏吗?
PlayerC 收到消息 : 你好, PlayerA 和 PlayerC! 一起玩游戏吗?
PlayerA 收到消息 : 我也加入!让我们开始吧!
PlayerB 收到消息 : 我也加入!让我们开始吧!
通过使用中介者模式,我们可以轻松地在游戏中添加更多玩家,而不需要修改现有玩家类。这使得游戏的设计更加灵活和可扩展。

相关推荐
体育分享_大眼23 分钟前
从零搭建高并发体育直播网站:架构设计、核心技术与性能优化实战
java·性能优化·系统架构
水w1 小时前
【Python爬虫】简单案例介绍1
开发语言·爬虫·python
琢磨先生David1 小时前
Java 在人工智能领域的突围:从企业级架构到边缘计算的技术革新
java·人工智能·架构
计算机学姐2 小时前
基于SpringBoo的地方美食分享网站
java·vue.js·mysql·tomcat·mybatis·springboot·美食
qq_365911603 小时前
GPT-4、Grok 3与Gemini 2.0 Pro:三大AI模型的语气、风格与能力深度对比
开发语言
Hanson Huang5 小时前
【数据结构】堆排序详细图解
java·数据结构·排序算法·堆排序
Susea&5 小时前
数据结构初阶:队列
c语言·开发语言·数据结构
慕容静漪5 小时前
如何本地安装Python Flask并结合内网穿透实现远程开发
开发语言·后端·golang
ErizJ5 小时前
Golang|锁相关
开发语言·后端·golang
GOTXX5 小时前
【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
开发语言·数据库·c++·qt·图形渲染·图形化界面·qt新手入门