行为型设计模式(下)

命令模式

命令模式是一种行为设计模式,它将一个请求封装为一个对象,从而允许用户使用不同的请求、队列或日志请求,并支持可撤销的操作。它主要涉及三个角色:发送者(Invoker)、接收者(Receiver)和命令对象(Command)。
核心组件:

  1. Command(命令接口):声明执行操作的接口。
  2. ConcreteCommand(具体命令):实现命令接口的类,定义了接收者的动作和参数。
  3. Receiver(接收者):执行与请求相关的操作,具体执行命令的逻辑。
  4. Invoker(调用者):要求命令对象执行请求,通常持有命令对象,并在某个时间点调用命令对象的 execute() 方法。
  5. Client(客户端):创建具体命令对象,并设置其接收者。

优点:

  1. 降低系统的耦合度:命令模式将调用操作的对象与知道如何实现该操作的对象解耦。
  2. 增加新命令很容易:因为增加新命令不需要改变现有类的代码。
  3. 可以组合命令:可以组合多个命令来实现宏命令。

缺点:

  1. 可能产生大量具体命令类:每个操作都需要创建一个具体命令类。

使用场景:

  • 当需要将发出请求的对象和执行请求的对象解耦时。
  • 当需要支持撤销、排队等操作时。
  • 当需要支持宏命令,即一个命令的执行结果依赖于多个命令时。

在命令模式中,"可撤销操作"意味着执行的命令可以被撤销,即回到执行命令前的状态。这在需要提供撤销功能的场景中非常有用,例如文本编辑器中的撤销操作、事务操作等。
命令模式的优势在于它提供了一种将操作封装为对象的方法。这种封装有以下几个好处:

  1. 解耦:命令模式将发起操作的对象(调用者)和执行操作的对象(接收者)解耦,增加了系统的灵活性。
  2. 扩展性:可以很容易地添加新命令,而不影响其他类。
  3. 复合命令:可以组合多个命令,实现复杂的功能。
  4. 记录、队列和撤销:命令可以被记录下来、排队执行,以及支持撤销和重做。

设计场景
假设有一个简单的智能家居控制系统,通过一个中央控制器(如移动应用)来控制不同的家居设备(如灯光、空调)。这里,命令模式可以用来封装对各种设备的操作。
命令接口和具体命令
首先,定义一个命令接口和一系列实现此接口的具体命令类,对应于各种设备的操作:


// 命令接口
public interface Command {
    void execute();
    void undo();
}

// 灯光开启命令
public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOn();
    }

    @Override
    public void undo() {
        light.turnOff();
    }
}

// 灯光关闭命令
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOff();
    }

    @Override
    public void undo() {
        light.turnOn();
    }
}

接收者 - 家居设备
接着,定义接收者类,即家居设备,如灯光:


public class Light {
    public void turnOn() {
        System.out.println("Light is turned on.");
    }

    public void turnOff() {
        System.out.println("Light is turned off.");
    }
}

调用者 - 控制器
接下来,实现一个调用者类,比如一个遥控器,它将发送命令给家居设备:


public class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }

    public void pressUndo() {
        command.undo();
    }
}

Controller 层
在 Web 应用的 Controller 层,可以通过 HTTP 请求触发这些命令:


@RestController
@RequestMapping("/smartHome")
public class SmartHomeController {

    private final RemoteControl remoteControl = new RemoteControl();
    private final Light light = new Light();

    @PostMapping("/light/on")
    public ResponseEntity<String> turnLightOn() {
        Command lightOn = new LightOnCommand(light);
        remoteControl.setCommand(lightOn);
        remoteControl.pressButton();
        return ResponseEntity.ok("Light turned on");
    }

    @PostMapping("/light/off")
    public ResponseEntity<String> turnLightOff() {
        Command lightOff = new LightOffCommand(light);
        remoteControl.setCommand(lightOff);
        remoteControl.pressButton();
        return ResponseEntity.ok("Light turned off");
    }

    @PostMapping("/light/undo")
    public ResponseEntity<String> undoLightAction() {
        remoteControl.pressUndo();
        return ResponseEntity.ok("Undo last action");
    }
}

场景分析
在这个智能家居控制系统中,命令模式使得每个操作(如开灯、关灯)都被封装在了具体的命令对象中。这样,控制器(RemoteControl)不需要知道具体细节就可以执行操作。同时,可以很容易地添加新的命令来控制新设备,而不需要改变现有的 RemoteControl 类或其他命令。此外,由于每个命令对象都实现了 undo 方法,系统还可以支持撤销操作,增强了用户体验。

如果看代码你还是不能理解为什么要用命令模式,那看完我这段话你就明白了:
假设我现在有一个场景,有三种状态,然后如果我们只写了一个controller方法,那么就相当于这个controller方法要一个方法处理三种状态,并且如果后续要添加第四种状态,那么我们就得修改controller方法了,而如果使用了命令模式,我们就可以选择新建一个类,并且再Light类中也添加对应的这个类型的方法,这样子我们就不会违反开闭原则,也不用对controller中的方法进行修改了。

在没有命令模式的情况下,控制器方法可能包含复杂的条件判断来处理每种状态:


@PostMapping("/device/action")
public ResponseEntity<String> performAction(@RequestParam DeviceState state) {
    if (state == DeviceState.ON) {
        // 处理开状态
    } else if (state == DeviceState.OFF) {
        // 处理关状态
    } else if (state == DeviceState.STANDBY) {
        // 处理待机状态
    }
    // 如果添加新状态,需要在这里添加新的条件分支
    return ResponseEntity.ok("Action performed for state: " + state);
}

这种方法的问题在于,如果要添加新状态,就需要修改 performAction 方法来添加新的条件分支,这违反了开闭原则。
使用命令模式
使用命令模式,您可以将每种状态下的操作封装在不同的命令类中:


public interface Command {
    void execute();
}

public class OnCommand implements Command {
    public void execute() {
        // 实现开启操作
    }
}

public class OffCommand implements Command {
    public void execute() {
        // 实现关闭操作
    }
}

public class StandbyCommand implements Command {
    public void execute() {
        // 实现待机操作
    }
}

// 控制器
@PostMapping("/device/action")
public ResponseEntity<String> performAction(@RequestParam DeviceState state) {
    Command command;
    switch (state) {
        case ON: command = new OnCommand(); break;
        case OFF: command = new OffCommand(); break;
        case STANDBY: command = new StandbyCommand(); break;
        default: throw new IllegalStateException("Unexpected state: " + state);
    }
    command.execute();
    return ResponseEntity.ok("Action performed for state: " + state);
}

在这个例子中,控制器不再直接依赖于状态的具体处理逻辑。添加新状态时,只需添加新的命令类,并在控制器中实例化这个新命令。这样,您就不需要修改已有的 performAction 方法或其他状态的处理逻辑,从而遵循了开闭原则。

命令模式再上面我们说到,可以提供记录、队列、撤销的功能。
以文本编辑器的例子为例,假设我们有一个 Web 应用,用户可以通过 HTTP 请求来执行文本编辑操作(如插入和删除文本),并支持撤销和重做功能。
步骤 1: 创建命令接口和具体命令
首先定义命令接口和具体命令类,如之前所示。
步骤 2: 实现服务层
然后,实现一个服务层,它维护命令对象的历史记录,并执行、撤销或重做命令:


@Service
public class TextEditorService {
    private Document document = new Document();
    private Stack<Command> history = new Stack<>();
    private Stack<Command> redoStack = new Stack<>();

    public void executeCommand(Command command) {
        command.execute();
        history.push(command);
        redoStack.clear();
    }

    public void undo() {
        if (!history.isEmpty()) {
            Command command = history.pop();
            command.undo();
            redoStack.push(command);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            Command command = redoStack.pop();
            command.execute();
            history.push(command);
        }
    }

    // 其他方法...
}

步骤 3: Controller 层集成
最后,在 Controller 层中,定义端点来接收用户的编辑请求,并使用 TextEditorService 来处理这些请求:


@RestController
@RequestMapping("/textEditor")
public class TextEditorController {
    private final TextEditorService editorService;

    public TextEditorController(TextEditorService editorService) {
        this.editorService = editorService;
    }

    @PostMapping("/insert")
    public ResponseEntity<String> insertText(@RequestParam String text, @RequestParam int position) {
        Command insertCommand = new InsertTextCommand(editorService.getDocument(), text, position);
        editorService.executeCommand(insertCommand);
        return ResponseEntity.ok("Text inserted");
    }

    @PostMapping("/undo")
    public ResponseEntity<String> undo() {
        editorService.undo();
        return ResponseEntity.ok("Undo successful");
    }

    @PostMapping("/redo")
    public ResponseEntity<String> redo() {
        editorService.redo();
        return ResponseEntity.ok("Redo successful");
    }

    // 其他操作...
}

关联解释
在这个设计中,Controller 层负责处理 HTTP 请求,并将具体的操作请求转换为命令对象。服务层(TextEditorService)则负责执行这些命令,并维护一份命令历史记录,用于实现撤销和重做功能。
这样的设计使得 Controller 层保持简洁和专注于处理 HTTP 请求,而复杂的操作逻辑则封装在服务层和命令对象中。命令模式提供了一种灵活的方式来扩展和修改命令,而不需要修改 Controller 或服务层的代码。

解释器模式

实际开发中用的较少,一般再SQL里面会用到。
解释器模式是一种行为设计模式,用于定义一个语言的文法,并建立一个解释器来解释该语言中的句子。这种模式通常用于SQL解析、符号处理引擎等场景。
核心组件:

  1. 抽象表达式(Abstract Expression):声明一个解释操作的接口,这个接口为解释特定的上下文。
  2. 终结符表达式(Terminal Expression):实现与文法中的终结符相关联的解释操作。
  3. 非终结符表达式(Nonterminal Expression):为文法中的非终结符实现解释操作。
  4. 上下文(Context):包含解释器之外的一些全局信息。
  5. 客户端(Client):构建(或被给定)表示该文法定义的语言中一个特定句子的抽象语法树。该树由终结符和非终结符表达式组成。然后调用解释操作。

假设有一个简单的表达式语言,只包含加法和减法运算。这个语言的文法可以通过解释器模式来实现:

javaCopy code
// 抽象表达式
interface Expression {
    int interpret(Context context);
}

// 终结符表达式
class NumberExpression implements Expression {
    private int number;

    public NumberExpression(int number) {
        this.number = number;
    }

    @Override
    public int interpret(Context context) {
        return number;
    }
}

// 非终结符表达式
class AddExpression implements Expression {
    private Expression firstExpression, secondExpression;

    public AddExpression(Expression first, Expression second) {
        this.firstExpression = first;
        this.secondExpression = second;
    }

    @Override
    public int interpret(Context context) {
        return firstExpression.interpret(context) + secondExpression.interpret(context);
    }
}

// 上下文类
class Context {}

// 客户端
public class InterpreterDemo {
    public static void main(String[] args) {
        // 构造抽象语法树
        Expression expr = new AddExpression(new NumberExpression(1), new NumberExpression(2));

        Context context = new Context();
        System.out.println("Result: " + expr.interpret(context));
    }
}

在这个示例中,NumberExpression 表示数字,是一个终结符表达式;AddExpression 表示加法运算,是一个非终结符表达式。Context 类在这个简单示例中没有实际的用途,但在更复杂的解释器中,它通常包含了解释器需要的全局信息或状态。
实现思路说明:

  • 使用解释器模式,我们定义了用于算术表达式的文法规则。
  • 客户端构建了一个表达式的抽象语法树,然后调用解释器来计算结果。

优点:

  1. 易于改变和扩展文法:由于文法由许多小的类表示,所以可以容易地改变和扩展。
  2. 重用性:可以通过在其他文法中重用现有的表达式定义新的文法。

缺点:

  1. 复杂的文法难以维护:对于复杂的文法,可能需要定义许多类,使得系统变得复杂且难以维护。
  2. 效率问题:解释器模式通常比其他的解析方式运行更慢,因为它需要大量的循环和递归调用。

使用场景:

  • 当有一个语言需要解释执行,并且你可以将该语言中的句子表示为一个抽象语法树时。
  • 用于某些特定类型的问题的频繁发生时,例如,一些重复发生的问题可以用一种简单的语言来表达一个简单的文法。

解释器模式

迭代器模式

再我们的List等集合中用的比较多。
迭代器模式是一种行为设计模式,用于顺序访问集合对象的元素,而无需知道集合对象的底层表示。迭代器模式将遍历集合的职责从集合中抽离出来,封装在一个独立的迭代器对象中,以提供一种统一的方法来访问集合的各个元素。
核心组件:

  1. 迭代器(Iterator)接口:定义访问和遍历元素的操作,如 next()hasNext() 等。
  2. 具体迭代器(Concrete Iterator):实现迭代器接口,负责管理集合的遍历。
  3. 集合(Collection)接口:定义创建迭代器的接口,通常是 createIterator() 方法。
  4. 具体集合(Concrete Collection):实现集合接口,返回一个具体的迭代器实例。

以一个简单的书籍集合和其迭代器为例:


// 迭代器接口
interface Iterator {
    boolean hasNext();
    Object next();
}

// 集合接口
interface Aggregate {
    Iterator createIterator();
}

// 具体集合
class BookCollection implements Aggregate {
    private String[] books;

    public BookCollection(String[] books) {
        this.books = books;
    }

    @Override
    public Iterator createIterator() {
        return new BookIterator();
    }

    // 具体迭代器
    private class BookIterator implements Iterator {
        private int currentIndex = 0;

        @Override
        public boolean hasNext() {
            return currentIndex < books.length;
        }

        @Override
        public Object next() {
            if (this.hasNext()) {
                return books[currentIndex++];
            }
            return null;
        }
    }
}

// 客户端
public class IteratorDemo {
    public static void main(String[] args) {
        String[] books = {"Book 1", "Book 2", "Book 3"};
        BookCollection bookCollection = new BookCollection(books);
        Iterator iterator = bookCollection.createIterator();

        while (iterator.hasNext()) {
            String book = (String) iterator.next();
            System.out.println(book);
        }
    }
}

实现思路说明:

  • BookCollection:一个具体的集合类,持有一组数据(在这个例子中是一组书籍)。
  • BookIteratorBookCollection 的内部类,实现了 Iterator 接口,用于遍历书籍集合。
  • IteratorDemo:演示如何使用 BookCollectionBookIterator 来遍历集合。

优点:

  1. 支持多种遍历:可以提供多种不同的迭代器,每个迭代器实现不同的遍历策略。
  2. 简化集合接口:迭代器模式将遍历逻辑从集合类中分离出来,简化了集合的接口和实现。
  3. 同一时间多重遍历:可以同时对同一集合进行多重遍历,因为每个迭代器对象都维护着自己的遍历状态。

缺点:

  1. 额外的对象:为了支持迭代功能,需要额外的迭代器对象,可能增加系统的复杂性。

使用场景:

  • 当你需要对集合提供多种遍历方式时。
  • 当你需要一个统一的接口来遍历不同类型的集合时。
  • 当你希望在不暴露集合内部表示的情况下遍历集合元素时。

中介者模式

中介者模式是一种行为设计模式,用于减少多个对象或类之间的通信复杂性。这种模式提供了一个中介者对象,该对象通常封装了一组对象之间的交互和通信。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
核心组件:

  1. 中介者(Mediator)接口:定义了同事对象到中介者对象的接口。
  2. 具体中介者(Concrete Mediator):实现中介者接口并协调同事对象之间的交互关系。
  3. 同事类(Colleague):通信的对象,需要与其他同事对象通信时,通过中介者对象来完成。

请以消息总线为例子,编写一个使用中介者模式实现的消息总线
创建一个简单的消息总线,使用中介者模式,可以分为以下几个步骤:
优点:

  1. 降低了类之间的耦合:中介者模式减少了类之间的直接通信,将通信封装在中介者类中,使得各类之间的耦合松散。
  2. 集中控制交互:所有的对象间的交互都可以集中管理和维护。

缺点:

  1. 中介者可能变得过于复杂:所有的通信都通过中介者进行,中介者的职责可能过重,变得复杂且难以维护。

使用场景:

  • 当一组对象之间的通信方式复杂且混乱时,可以使用中介者模式来重新定义对象间的通信。
  • 当多个对象互相依赖时,为了降低它们之间的耦合度,可以引入一个中介者对象,使得各对象不直接与对方交互。

在使用事件总线的系统中,事件总线充当中介者,管理事件的发布和订阅。组件只需将事件发布到总线上或从总线订阅事件,无需关心事件的具体处理逻辑。
接下来就按照事件总线的方式实现一个中介者模式:
步骤 1: 定义中介者接口
首先,定义中介者接口,它声明了组件(消息发布者和订阅者)与中介者交互的方法。


public interface MessageMediator {
    void sendMessage(String message, Colleague colleague);
    void addColleague(Colleague colleague);
}

步骤 2: 实现具体中介者
实现具体的中介者类,该类将管理所有组件之间的通信。


public class ConcreteMessageMediator implements MessageMediator {
    private List<Colleague> colleagues;

    public ConcreteMessageMediator() {
        this.colleagues = new ArrayList<>();
    }

    @Override
    public void addColleague(Colleague colleague) {
        colleagues.add(colleague);
    }

    @Override
    public void sendMessage(String message, Colleague originator) {
        for (Colleague colleague : colleagues) {
            // 不要给消息的发送者发送消息
            if (colleague != originator) {
                colleague.receive(message);
            }
        }
    }
}

步骤 3: 定义同事类
定义同事类(组件),它们将通过中介者进行通信。


public abstract class Colleague {
    protected MessageMediator mediator;

    public Colleague(MessageMediator mediator) {
        this.mediator = mediator;
    }

    public void send(String message) {
        mediator.sendMessage(message, this);
    }

    public abstract void receive(String message);
}

public class ConcreteColleague extends Colleague {
    public ConcreteColleague(MessageMediator mediator) {
        super(mediator);
    }

    @Override
    public void receive(String message) {
        System.out.println("Colleague Received: " + message);
    }
}

步骤 4: 客户端使用
在客户端代码中,创建中介者和同事对象,然后通过中介者进行通信。

javaCopy code
public class MediatorPatternDemo {
    public static void main(String[] args) {
        MessageMediator mediator = new ConcreteMessageMediator();

        Colleague colleague1 = new ConcreteColleague(mediator);
        Colleague colleague2 = new ConcreteColleague(mediator);
        Colleague colleague3 = new ConcreteColleague(mediator);

        mediator.addColleague(colleague1);
        mediator.addColleague(colleague2);
        mediator.addColleague(colleague3);

        colleague1.send("Hello World!");
        colleague2.send("Hello!");
    }
}

在这个消息总线实例中,ConcreteMessageMediator 充当中介者,管理 Colleague 对象之间的通信。每个 ConcreteColleague 可以发送消息,这些消息将通过中介者转发给其他同事。
这个简单的示例演示了中介者模式如何在组件之间促进松散耦合的通信。在实际的应用程序中,你可以根据需要扩展这个模式,以处理更复杂的场景,例如根据主题或类型过滤消息等。

访问者模式

访问者模式是一种行为设计模式,它允许你向一个对象结构中添加新的操作,而无需改变该结构中的元素的类。这种模式主要用于对一个集合中的不同类型的对象执行操作。
核心组件:

  1. 访问者(Visitor)接口:声明了一系列访问操作,用于访问不同类型的具体元素。
  2. 具体访问者(Concrete Visitor):实现访问者接口,定义了对每种类型元素的访问行为。
  3. 元素(Element)接口:声明了一个 accept 方法,用于接受访问者对象。
  4. 具体元素(Concrete Element):实现元素接口,具体元素的每个类都有 accept 方法,用于接受访问者。
  5. 对象结构(Object Structure):通常是一个集合或复合对象,包含不同类型的元素对象,提供一种方式让访问者访问其元素。

优点:

  1. 增加新操作容易:可以通过增加新的访问者来增加新操作,而无需修改现有元素的类。
  2. 集中相关操作:可以在访问者中集中相关操作,而不是分散在元素类中。

缺点:

  1. 增加新元素困难:如果要增加新的元素,所有的访问者类都可能需要修改。
  2. 破坏封装:访问者可能需要访问元素的私有成员,从而破坏了封装。

使用场景:

  • 当系统中的对象结构相对稳定,但经常需要在此结构上定义新的操作时。
  • 当需要对一个复合对象中的所有元素进行一些复杂操作,且这些操作依赖于对象的类型时。

假设我正在开发一个文档编辑器,该编辑器支持多种类型的内容元素,例如文本段落、图片和表格。随着产品的发展,您可能需要对这些元素执行各种操作,如渲染、导出、语法检查等。如果直接在元素类中添加这些操作,随着时间推移,这些类可能会变得异常复杂且难以维护。
这里,访问者模式就可以派上用场。
访问者模式的应用

  1. 定义元素接口
    • 所有元素(文本、图片、表格)实现一个 Element 接口,该接口包含 accept(Visitor visitor) 方法。
  2. 实现具体元素
    • 每种类型的元素(如 Paragraph, Image, Table)实现 Element 接口。
  3. 定义访问者接口
    • 创建一个 Visitor 接口,定义针对不同类型元素的访问方法。
  4. 实现具体访问者
    • 针对每种操作(如渲染、导出),实现一个具体的访问者。

// 元素接口
interface Element {
    void accept(Visitor visitor);
}

// 具体元素
class Paragraph implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

class Image implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

class Table implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 访问者接口
interface Visitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
    void visit(Table table);
}

// 具体访问者 这里我以渲染操作为例
class Renderer implements Visitor {
    public void visit(Paragraph paragraph) {
        System.out.println("Render Paragraph");
    }

    public void visit(Image image) {
        System.out.println("Render Image");
    }

    public void visit(Table table) {
        System.out.println("Render Table");
    }
}

在这个例子中,添加新操作不需要更改元素类。例如,如果需要添加一个新的操作(比如导出为PDF),只需添加一个新的访问者类而无需更改 ParagraphImageTable 类。这样,元素类保持简洁,而操作的实现集中在访问者类中。

也就是如果我要添加一个类型的操作,我只需要实现一下Element接口,然后再Visitor中添加这个接口的方法,然后再具体的访问中提供这个方法即可。之后,如果我要使用这个方法,我将我的实现类放入进去再visit中进行执行就好了。
在访问者模式中,当我们需要为元素添加新的操作时,通常的步骤是:

  1. 实现 Element 接口:如果我有一个新的类型(比如 Graph),我首先需要让这个类实现 Element 接口,并在其 accept 方法中接收访问者并将自身传递给它。
  2. 在 Visitor 接口中添加新方法:在 Visitor 接口中添加一个新的方法,例如 visit(Graph graph),用于处理新类型的元素。
  3. 在具体的访问者中实现新方法:在具体的访问者类(例如 RendererExporter)中实现这个新的 visit 方法,以定义针对新元素的操作。
  4. 使用新操作:创建新元素的实例,并使用具体的访问者对象来执行操作。当调用元素的 accept 方法时,元素将自身传递给访问者的 visit 方法,从而触发实际的操作。

假设我已经有 Paragraph, Image, 和 Table 类,并想添加一个新的 Graph 类:


// 新的元素类
class Graph implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 更新 Visitor 接口
interface Visitor {
    void visit(Paragraph paragraph);
    void visit(Image image);
    void visit(Table table);
    void visit(Graph graph); // 新增方法
}

// 更新具体访问者
class Renderer implements Visitor {
    // ... 现有方法 ...

    @Override
    public void visit(Graph graph) {
        System.out.println("Render Graph");
    }
}

使用新操作:

public class VisitorPatternDemo {
    public static void main(String[] args) {
        List<Element> elements = Arrays.asList(new Paragraph(), new Image(), new Table(), new Graph());
        Visitor renderer = new Renderer();

        for (Element element : elements) {
            element.accept(renderer);
        }
    }
}

在这个扩展的例子中,添加新元素 Graph 和对应的访问者操作变得非常简单和直观,而不需要更改现有的元素或访问者类,从而遵循了开闭原则。

备忘录模式

备忘录模式(Memento Pattern)是一种设计模式,用于在不破坏封装性的条件下捕获并保存一个对象的内部状态,以便在之后可以将对象恢复到这个状态。这个模式是行为设计模式的一种,主要用于实现撤销(Undo)操作或是保存和恢复功能。备忘录模式的关键组成部分通常包括三个角色:

  1. 发起人(Originator):这是需要保存状态的对象。它创建一个包含其当前内部状态的备忘录对象,并可以使用备忘录对象恢复到之前的状态。
  2. 备忘录(Memento):这个对象负责存储发起人对象的内部状态。备忘录保护发起人状态的完整性,不允许对象以外的其他对象访问或修改它。
  3. 看护者(Caretaker):这个对象负责保存备忘录。它记录了发起人的历史状态,但不尝试修改或检查存储在备忘录中的状态。看护者可以存储一个或多个备忘录对象以支持多步撤销操作。

备忘录模式的优点包括提供了一种恢复状态的机制,同时不违反封装原则。这是因为备忘录对象通过将状态存储在一个外部对象中,而不是发起人对象自身,来避免暴露信息。
然而,这个模式也有一些缺点。例如,如果要保存的状态信息量很大,那么备忘录对象可能会消耗大量的内存。此外,如果发起人对象的状态改变频繁,管理备忘录可能会变得复杂。
备忘录模式常见于软件开发中的各种应用,特别是那些需要提供撤销操作的场景,如文本编辑器、游戏的保存/恢复功能等。
备忘录模式在实际开发中可以应用于多种场景,尤其是那些需要保存和恢复对象状态的情况。以下是一些具体的应用场景:

  1. 撤销和重做功能:在文本编辑器、图形编辑软件、开发环境等应用中,用户的每次操作都可能需要保存,以便之后可以撤销或重做。备忘录模式允许这些应用在不违反封装性的情况下保存和恢复状态。
  2. 游戏状态保存:在游戏开发中,可以使用备忘录模式来保存玩家的当前状态,例如角色位置、得分、物品等,这样玩家可以在之后从保存点恢复游戏。
  3. 软件快照:在某些复杂的应用中,比如数据库或虚拟机管理软件,备忘录模式可以用来创建系统的快照,这样在出现问题时可以回滚到先前的状态。
  4. 事务回滚:在数据库管理系统中,事务处理通常需要在操作失败时回滚到事务开始之前的状态,备忘录模式可以在这里发挥作用。

接下来我们按照MySQL事务回滚的方式模拟一个备忘录操作。有点类似于undo log,也就是存一份数据的备份。

  1. 实体类
    首先,我们定义一个简单的实体类,比如Book

public class Book {
    private String title;
    private String author;

    // 构造函数、getter和setter
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}
  1. 备忘录类
    接下来,我们创建一个备忘录类BookMemento,用于保存Book的状态:

public class BookMemento {
    private final String title;
    private final String author;

    public BookMemento(String title, String author) {
        this.title = title;
        this.author = author;
    }

    // getter方法
    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }
}
  1. 实体管理类
    然后,我们创建一个简单的实体管理类,用于管理实体和它们的备忘录:

import java.util.Stack;

public class EntityManager {
		//备忘录
    private Stack<BookMemento> history = new Stack<>();

    public void save(Book book) {
        history.push(new BookMemento(book.getTitle(), book.getAuthor()));
    }

    public void revert(Book book) {
        if (!history.isEmpty()) {
            BookMemento memento = history.pop();
            book.setTitle(memento.getTitle());
            book.setAuthor(memento.getAuthor());
        }
    }
}

最后,我们演示如何使用这些类:


public class MementoDemo {
    public static void main(String[] args) {
        Book book = new Book("Initial Title", "Initial Author");
        EntityManager manager = new EntityManager();

        // 保存当前状态
        manager.save(book);

        // 更改状态
        book.setTitle("New Title");
        book.setAuthor("New Author");

        // 恢复到之前的状态
        manager.revert(book);

        System.out.println("Title: " + book.getTitle() + ", Author: " + book.getAuthor());
        // 输出将是 "Initial Title" 和 "Initial Author"
    }
}

思路说明

  • Book类代表一个简单的实体。
  • BookMemento是备忘录类,用于保存Book的状态。
  • EntityManager类模仿JPA的EntityManager,但它的职责是管理备忘录对象而不是管理数据库交互。它允许我们保存和恢复Book对象的状态。
  • 在示例中,我们先保存了Book的初始状态,然后更改了它的标题和作者。之后,我们使用EntityManagerrevert方法将Book对象恢复到之前保存的状态。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/567623.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WAF防范原理

目录 一、什么是WAF 二、纵深安全防御 WAF的组网模式 WAF配置全景 WAF端 服务器 攻击端 拦截SQL注入&#xff0c;XSS攻击&#xff0c;木马文件上传 要求&#xff1a; 使用WAF&#xff0c;通过配置策略要求能防御常见的web漏洞攻击&#xff08;要求至少能够防御SQL、XSS、文…

毕业设计注意事项

1.开题 根据学院发的开题报告模板完成&#xff0c;其中大纲部分可参考资料 2.毕设 根据资料中的毕设评价标准&#xff0c;对照工作量 3.论文 3.1 格式问题 非常重要&#xff0c;认真对比资料中我发的模板&#xff0c;格式有问题&#xff0c;答辩输一半&#xff01; 以word…

wireshark RTP分析参数

主要看丢弃和Delta&#xff0c; 丢弃就是丢掉的udp包&#xff0c;所占的比率 Delta是当前udp包接收到的时间减去上一个udp包接收到的时间 根据载荷可以知道正确的delta应该是多少&#xff0c;比如G711A&#xff0c;ptime20&#xff0c;那么delta理论上应该趋近于20. 这里的de…

C++面向对象程序设计 - 运算符重载

函数重载就是对一个已有的函数赋予新的含义&#xff0c;使之实现新的功能。因此一个函数名就可以用来代表不同功能的函数&#xff0c;也就是一名多用。运算符也可以重载&#xff0c;即运算符重载&#xff08;operator overloading&#xff09;。 一、运算符重载的方法 运算符重…

# IDEA2019 如何打开 Run Dashboard 运行仪表面板

IDEA2019 如何打开 Run Dashboard 运行仪表面板 段子手168 1、依次点击 IDEA 上面工具栏 —> 【View】 视图。 —> 【Tool Windows】 工具。 —> 【Run Dashboard】 运行仪表面板。 2、如果 【Tool Windows 】工具包 没有 【Run Dashboard】 运行仪表面板 项 依次…

【好书推荐7】《机器学习平台架构实战》

【好书推荐7】《机器学习平台架构实战》 写在最前面《机器学习平台架构实战》编辑推荐内容简介作者简介目  录前  言本书读者内容介绍充分利用本书下载示例代码文件下载彩色图像本书约定 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&…

STM32系统参数和结构

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. 基本参数 2. 片上资源&#xff08;外设&#xff09; 3. STM32系列命名规则 4. 系统结构 5. 引脚定义 6. 启动配置 7. 最小系统电路 8. 型号分类和缩写 1. 基本参数 STM32F103C8T6 系列&#…

达梦(DM)数据库表索引

达梦DM数据库表索引 表索引索引准则其他准则 创建索引显式地创建索引其他创建索引语句 使用索引重建索引删除索引 表索引 达梦数据库表索引相关内容比较多&#xff0c;常用的可能也就固定的一些&#xff0c;这里主要说一下常用的索引&#xff0c;从物理存储角度进行分类&#…

B008-方法参数传递可变参数工具类

目录 方法参数传递可变参数冒泡排序Arrays工具类Arrays工具类常用方法 方法参数传递 /*** java中只有值传递* 基本数据类型 传递的是具体的值* 引用数据类型 传递的是地址值*/ public class _01_ParamPass {public static void main(String[] args) {// 调用方法 getSumge…

网络变压器在网络分析仪上能通过测试,装上设备后网速达不到呢?

Hqst华轩盛(石门盈盛)电子导读&#xff1a;今天和大家一起探讨网络变压器在网络分析仪上能通过测试&#xff0c;装上设备后网通设备网速达不到的可能原因及其处理方式 一、出现这种情况可能有以下原因&#xff1a; 1.1. 设备兼容性问题&#xff1a;设备其它元器件与 网络…

Docker容器化技术:概述与安装

目录 一、云基础知识 1、常见的云服务厂商 2、云计算服务模式三种层次 3、什么是虚拟化 4、什么是虚拟机 5、虚拟化产品 5.1 仿真虚拟化产品 5.2 半虚拟化产品 5.3 全虚拟化产品 6、虚拟机架构 6.1 寄居架构 6.2 源生架构 二、认识容器 1、容器的概述 2、容器的…

【Netty】ByteBuf与拆包粘包

ByteBuf 在介绍ByteBuf之前先来一套基础的代码来演示ByteBuf的使用。 package blossom.project.netty;import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled;import java.nio.charset.StandardCharsets;/*** author: ZhangBlossom* date: 2023/12/14 13:37* con…

web学习

day02-01 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>js快速引入</title> <!-- 内部脚本--> <!-- <script>--> <!-- alert(Hello JS)--> <!-- <…

【linux】匿名管道|进程池

1.进程为什么要通信&#xff1f; 进程也是需要某种协同的&#xff0c;所以如何协同的前提条件(通信) 通信数据的类别&#xff1a; 1.通知就绪的 2.单纯的数据 3.控制相关的信息 2.进程如何通信&#xff1f; 进程间通信&#xff0c;成本会高一点 进程间通信的前提&#xff0c;先…

制氢机远程监控运维方案

制氢机远程监控运维方案 在当今能源转型的大背景下&#xff0c;氢能作为清洁、高效且可再生的能源载体&#xff0c;其重要性日益凸显。而制氢机作为氢能产业链中的关键设备&#xff0c;其稳定运行与高效运维对于保障氢气供应、推动氢能产业健康发展至关重要。在此背景下&#…

动态规划——切割钢条问题

一、动态规划 动态规划算法通常用于解决最优化问题&#xff08;寻求最优解&#xff09;。其思想与分治法类似&#xff0c;将待求解的问题分成若干个子问题&#xff0c;先求出子问题&#xff0c;再根据子问题的解求出原来问题中的解&#xff0c;与分支法不同的是&#xff0c;在动…

Oracle使用内部包自定义创建表空间和用户

如果之前有类似的表空间,可以使用dbms自动生成对应的表空间和数据文件 select dbms_metadata.get_ddl(TABLESPACE,ts.tablespace_name) from dba_tablespaces ts; 可以使用类似的 SQL> set echo off SQL> spool /data/logs/create_tablespace.log SQL> select dbms…

Mimics21软件学习总结

一. Mimics21软件安装过程 ① 解压下载好的Mimics软件包&#xff1b; ② 双击“MIS_Medical_21.0.exe”打开等待安装程序初始化完成&#xff1b; ③ 进入安装向导点击“next”&#xff1b; ④ 点击选择“Iaccept the agreement”同意相关协议&#xff0c;随后点击“next”&…

多模态大模型训练数据以及微调数据格式

多模态数据&#xff0c;尤其是中文多模态数据&#xff0c;找一些中文多模态的数据 中文多模态数据集汇总_数据集-阿里云天池本文整理汇总了业界常用的多模态中文数据集&#xff0c;提供了每个数据集的简介、官网、下载地址、Github代码等信息&#xff0c;方便算法研究人员学习…

虚假新闻检测——Adapting Fake News Detection to the Era of Large Language Models

论文地址&#xff1a;https://arxiv.org/abs/2311.04917 1.概论 尽管大量的研究致力于虚假新闻检测&#xff0c;这些研究普遍存在两大局限性&#xff1a;其一&#xff0c;它们往往默认所有新闻文本均出自人类之手&#xff0c;忽略了机器深度改写乃至生成的真实新闻日益增长的现…
最新文章