【设计模式深度剖析】【2】【行为型】【命令模式】| 以打开文件按钮、宏命令、图形移动与撤销为例加深理解

👈️上一篇:模板方法模式    |   下一篇:职责链模式👉️

设计模式-专栏👈️

---

文章目录

  • 命令模式
  • 定义
    • 英文原话
    • 直译
    • 如何理解呢?
  • 四个角色
    • 1. Command(命令接口)
    • 2. ConcreteCommand(具体命令类)
    • 3. Client(客户端)
    • 4. Invoker(调用者)
    • 5. Receiver(接收者)
    • 类图
    • 类图分析
    • 代码示例
      • 命令接口Command
      • 具体命令ConcreteCommand
      • 客户端Client
      • 调用者Invoker
      • 接收者Receiver
  • 应用
      • 命令模式的应用
      • 命令模式的优点
      • 命令模式的缺点
      • 命令模式的使用场景
  • 示例解析1:菜单按钮(UI 控件)- 打开文件按钮
    • 类图
    • 类图分析
    • 代码示例
  • 示例解析2:宏命令(组合多个命令)
    • 类图
    • 类图分析
    • 代码示例
  • 示例解析3:图形移动/撤销(重做功能)(丐版)
    • 类图
    • 类图分析
    • 代码示例
  • 示例解析4:图形移动/撤销(重做功能)
    • 类图
    • 类图分析
    • 代码示例

命令模式

命令模式(Command Pattern)又称为行动(Action)模式或交易(Transaction)模式。

命令模式就像是一个魔法卷轴。在这个奇幻的世界里,魔法师(请求者)不需要亲自施展复杂的法术(接收者),他们可以将法术的咒语和步骤记录在魔法卷轴上(命令对象)。

当魔法师需要施展某个法术时,他们只需挥动魔法卷轴,卷轴上的咒语就会自动激活,释放出强大的魔法力量。这样,魔法师就可以轻松地使用各种法术,而无需每次都记住复杂的咒语和步骤。

魔法卷轴的存在让魔法师的技能得以扩展和保存。他们可以将自己创造的独特法术记录在卷轴上,与他人分享或传授。此外,魔法卷轴还具有撤销和重做的能力,如果魔法师在施展法术时出现了错误,他们可以简单地收回卷轴并重新施展。

因此,命令模式就像是一个魔法卷轴,它让魔法的施展变得更加简单、高效和可控,为魔法师们带来了无尽的便利和乐趣。

本文源码,点击查看👈️

定义

英文原话

Encapsulate a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.

直译

将请求封装为一个对象,使得你可以使用不同的请求对客户端进行参数化;可以对请求进行排队或记录请求日志,并支持可撤销的操作。

在这个定义中,“Encapsulate a request as an object”指的是将请求(或操作)封装为对象的形式,

这样做的好处是可以将请求与其执行者解耦,提高系统的灵活性和可维护性。同时,通过封装请求为对象,还可以实现诸如请求队列、日志记录、撤销/重做等功能。

如何理解呢?

理解命令模式(Command Pattern)的关键在于认识到它如何帮助我们将请求(或操作)封装为对象,并将请求的发送者和接收者解耦。以下是对命令模式的详细理解:

  1. 请求封装为对象
    在命令模式中,一个请求被封装为一个对象,即命令对象。这个命令对象包含了执行特定操作的必要信息。通过封装请求为对象,我们可以将请求视为与其他对象一样的实体,并对其进行操作,如传递、存储和组合。

  2. 发送者与接收者解耦
    命令模式允许我们将请求的发送者(客户端)与请求的接收者(执行者)解耦。发送者不需要直接调用接收者的方法,而是通过命令对象来间接地触发接收者的操作。这种解耦提高了系统的灵活性和可扩展性。

    例如,我们可以轻松地更换接收者(执行不同的操作)或添加新的命令对象(执行新的操作),而无需修改发送者的代码

  3. 支持撤销、重做和日志记录
    由于命令对象封装了请求的信息,我们可以轻松地实现撤销、重做和日志记录等功能。通过存储之前执行的命令对象,我们可以在需要时重新执行它们(实现重做)或反向执行它们(实现撤销)。此外,我们还可以记录命令的执行日志,以便后续的分析和调试。

  4. 排队和调度
    由于命令对象是可存储和可传递的,我们可以将它们放入队列中并按顺序执行(如命令队列),或者根据一定的调度策略来执行它们(如时间调度器)。这允许我们更灵活地控制操作的执行顺序和频率。

  5. 简化客户端代码
    通过引入命令对象作为中间层,我们可以简化客户端代码。客户端只需要知道如何创建和发送命令对象,而无需关心命令的具体执行细节。这使得客户端代码更加简洁和易于维护。

命令模式通过封装请求为对象并将发送者与接收者解耦,提供了一种灵活的方式来处理请求和操作。它支持撤销、重做、日志记录、排队和调度等功能,并简化了客户端代码。这些特性使得命令模式在许多场景中都非常有用,如GUI操作数据库事务游戏开发等。

四个角色

命令模式涉及以下几个主要角色:

1. Command(命令接口)

声明了一个执行操作的接口。

2. ConcreteCommand(具体命令类)

将一个接收者对象绑定于一个动作

通过调用接收者相应的操作,来实现Commond接口的抽象方法。

实现了Command接口,持有接收者(Receiver)对象,并调用接收者的功能来完成命令要执行的操作

3. Client(客户端)

创建一个具体命令对象并设定它的接收者。

4. Invoker(调用者)

要求该命令执行这个请求。

5. Receiver(接收者)

知道如何实施与执行一个请求的相关的操作。

真正执行命令的对象任何类都可能成为一个接收者,只要它能够实现命令要求的功能。

类图

在这里插入图片描述

类图分析

从类图中可以分析出:

调用者Invoker持有命令对象,ConcreteCommand具体命令持有接收者Receiver对象。

因此,调用者Invoker对象就可以间接执行接收者Receiver对象的动作,通过调用具体命令的执行方法execute(),而该方法实现方式是通过调用接收者Receiver对象的动作action()方法。

通过Command接口将调用者与接收者解耦,发送者不需要直接调用接收者的方法,而是通过命令对象来间接地触发接收者的操作。这种解耦提高了系统的灵活性和可扩展性。

代码示例

命令接口Command

package com.polaris.designpattern.list3.behavioral.pattern02.command.proto;

/**
 * 命令接口Command:命令(Command)角色
 */
public interface Command {
    // 执行命令的方法
    void execute();
}

具体命令ConcreteCommand

package com.polaris.designpattern.list3.behavioral.pattern02.command.proto;

/**
 * 具体命令ConcreteCommand:具体命令(Concrete Command)角色
 */
public class ConcreteCommand implements Command {
    private Receiver receiver;

    public ConcreteCommand(Receiver receiver) {
        this.receiver = receiver;
    }

    // 执行方法
    @Override
    public void execute() {
        this.receiver.action();
    }
}

客户端Client

package com.polaris.designpattern.list3.behavioral.pattern02.command.proto;

/**
 * 创建一个具体命令对象并设定它的接收者
 */
public class Client {
    public static void main(String[] args) {
        // 接受者
        Receiver receiver = new Receiver();
        // 将一个接收者对象绑定于一个动作
        Command command = new ConcreteCommand(receiver);

        // 调用者:要求该命令执行这个请求
        Invoker invoker = new Invoker();
        invoker.setCommand(command);
        invoker.callCommand();
    }
}

调用者Invoker

package com.polaris.designpattern.list3.behavioral.pattern02.command.proto;

/**
 * 调用者Invoker
 */
public class Invoker {
    private Command command;

    // 构造函数初始化该命令
    public void setCommand(Command command) {
        this.command = command;
    }

    // 执行命令
    public void callCommand() {
        this.command.execute();
    }
}

接收者Receiver

package com.polaris.designpattern.list3.behavioral.pattern02.command.proto;

/**
 * 接收者Receiver
 */
public class Receiver {
    // 行动方法
    public void action() {
        System.out.println("Command executed.");
    }
}

输出打印

Command executed.

在这个示例中,

Receiver 是知道如何执行请求的对象,ConcreteCommand 是实现了 Command 接口的具体命令类,它持有一个 Receiver 对象并在 execute 方法中调用其 action 方法。

Invoker 是调用者,它持有 Command 对象并在需要时调用其 execute 方法。

Client 是客户端,负责创建和设置命令对象及其接收者。

应用

命令模式的应用

命令模式广泛应用于许多需要执行命令、撤销命令、记录命令历史或实现宏命令的系统中。以下是几个具体的应用场景:

  1. 图形用户界面 (GUI) 中的按钮:每个按钮可以关联一个命令对象,当按钮被点击时,执行该命令。
  2. 文本编辑器:撤销和重做功能可以通过命令模式实现,每个编辑操作(如插入、删除、剪切、粘贴等)都可以是一个命令。
  3. 请求队列:系统可以接收多个命令请求,并将它们存储在一个队列中,然后按照先进先出(FIFO)的顺序执行这些命令。
  4. 事务处理:在金融系统中,交易可以被封装为命令,从而可以轻松地回滚(撤销)事务。
  5. 宏录制:用户可以录制一系列命令,然后在需要时重新执行这些命令,以实现自动化操作。

命令模式的优点

  1. 解耦:命令模式将请求者与执行者解耦,使得请求者不需要知道如何执行命令,只需要知道如何发送命令。
  2. 易于扩展:由于请求者和执行者之间的解耦,可以在不修改现有代码的情况下添加新的命令。
  3. 支持撤销/重做:通过保存命令的历史记录,可以很容易地实现撤销和重做功能。
  4. 易于实现事务:可以将多个命令组合成一个事务,从而确保所有命令都成功执行或全部回滚。
  5. 易于记录日志和监控:可以轻松地记录每个命令的执行情况和结果,以便于后续的分析和监控。

命令模式的缺点

  1. 可能导致过多的具体命令类:对于复杂的系统,可能需要大量的具体命令类来实现各种功能,这可能会增加系统的复杂性。
  2. 可能增加系统的开销:由于命令对象通常包含对接收者的引用,并且在执行过程中可能需要额外的内存来存储命令历史记录,因此可能会增加系统的内存开销。

命令模式的使用场景

当需要满足以下条件时,可以考虑使用命令模式:

  1. 请求者和执行者需要解耦:当请求者不需要知道如何执行请求,或者执行者不需要知道请求的来源时,可以使用命令模式。
  2. 支持撤销/重做:如果系统需要支持撤销和重做功能,那么命令模式是一个很好的选择。
  3. 需要记录请求的历史:如果系统需要记录每个请求的执行情况和结果,以便于后续的分析和监控,那么可以使用命令模式。
  4. 需要实现事务:如果需要将多个操作组合成一个事务,以确保它们要么全部成功执行,要么全部回滚,那么命令模式是一个很好的选择。

示例解析1:菜单按钮(UI 控件)- 打开文件按钮

在这个示例中,我们有一个 UI 控件(比如一个按钮),当用户点击这个按钮时,它应该执行某个操作(打开文件)。

我们可以使用命令模式来解耦按钮和它所执行的操作。

类图

在这里插入图片描述

类图分析

这个类图可以对比之前的角色分析时候的类图,没错,可以发现是一致的结构,是一种场景的具化,为的是通过该具体示例进一步理解命令模式的各个角色。

  1. Button调用者角色,持有一个命令对象,至于什么具体的命令对象,看用户给他传入什么命令对象,这里是要给它的构造器传入OpenFileCommand,用来初始化持有的命令对象,即该按钮的意图是要打开文件。而如何打开,打开哪个文件,由命令决定,调用者只需要负责按照自己的需要传入相应的命令即可
  2. OpenFileCommand具体命令,构造器初始化了它持有的接收者FileManager对象,以及要打开的文件名,即该命令的目的就是为了打开该指定的文件。该命令的操作方法execute()通过调用接收者打开文件的方法openFile(String)来实现的;
  3. 从上面两步分析,可知,接收者FileManager并不知道谁会用到它,不知道谁是调用者,而调用者Button它只持有命令,具体谁(哪个接收者)来处理这个命令,它不直接关心,或者说它并不关心,他只直接知道我使用这个命令能完成这个事情,而具体这个命令如何操作的,由具体命令进行了封装,调用相应的接收者处理调用者的需求因此,通过命令接口,解耦了调用者与接收者
  4. 客户端就是按照上面分析过程中,调用者需要持有一个具体命令对象,具体命令封装了调用哪个接收者的方法来处理调用者的需求,因此客户端Client实例化接收者,作为参数构造出具体命令,然后将具体命令对象作为参数构造出调用者对象,然后就是执行,调用者要求该命令执行这个请求。

代码示例

看完类图,然后把类图分析完,下面的代码就了然了。

package com.polaris.designpattern.list3.behavioral.pattern02.command.openfileuidemo;

// 命令接口
interface Command {  
    void execute();  
}  
  
// 打开文件的具体命令  
class OpenFileCommand implements Command {  
    private FileManager fileManager;  
    private String fileName;  
  
    public OpenFileCommand(FileManager fileManager, String fileName) {  
        this.fileManager = fileManager;  
        this.fileName = fileName;  
    }  
  
    @Override  
    public void execute() {  
        fileManager.openFile(fileName);  
    }  
}  
  
// 文件管理器(接收者)  
class FileManager {  
    public void openFile(String fileName) {  
        System.out.println("打开文件: " + fileName);  
    }  
    // ... 其他文件管理操作  
}  
  
// UI 控件(调用者)  
class Button {  
    private Command command;  
  
    public Button(Command command) {  
        this.command = command;  
    }  
  
    public void onClick() {  
        command.execute();  
    }  
}  
  
// 客户端代码  
public class Client {  
    public static void main(String[] args) {  
        FileManager fileManager = new FileManager();  
        Command openCommand = new OpenFileCommand(fileManager, "example.txt");  
        Button openButton = new Button(openCommand);  
  
        // 假设这是用户点击按钮的事件  
        openButton.onClick(); // 输出:打开文件: example.txt  
    }  
}

/* Output"
打开文件: example.txt
*///~

示例解析2:宏命令(组合多个命令)

在这个示例中,我们将展示如何使用命令模式来组合多个命令,形成一个宏命令。

注意一点即可,宏命令是组合了多个命令,它是要一次执行一组操作的。

类图

在这里插入图片描述

类图分析

除了接收者宏命令MacroCommand持有多个命令对象外,和示例1结构无差别。重点理解一组操作的概念,从类图也可以反映出来。

该示例与示例1的区别,我们模拟宏命令即组合多个命令,可以看到调用者MacroCommand持有一个命令列表,通过addCommand(Command)方法可以添加具体命令进去,它的需求就是一次将这一组命令依次执行,因此需要遍历执行每个命令。

代码示例

主要关注组合对象的实现即可,当然也可以再次重复理解一下命令模式的各个角色,及他们间的关系。

package com.polaris.designpattern.list3.behavioral.pattern02.command.macrocommand;


import com.polaris.designpattern.list3.behavioral.pattern02.command.proto.Command;

import java.util.ArrayList;
import java.util.List;

// 命令接口
interface Command {
    // 执行命令的方法
    void execute();
}

// 宏命令(组合多个命令)
// 宏命令(也是命令的一种,可以包含多个子命令)
class MacroCommand implements Command {
    private List<Command> commands = new ArrayList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }

    @Override
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }
}

// 接收者 TextEditor 类,用于编辑文本
class TextEditor {
    private String text; // 假设这是编辑器中的文本内容

    public TextEditor() {
        this.text = "";
    }

    // 获取编辑器中的文本
    public String getText() {
        return text;
    }

    // 设置编辑器中的文本(通常这不会是一个直接设置的方法,但为了简化示例)
    public void setText(String text) {
        this.text = text;
    }

    // 调整字体大小的方法(这里只是模拟,实际上可能需要更复杂的逻辑)
    public void adjustFontSize(int size) {
        // 在真实应用中,这里可能会更新编辑器的UI或状态来反映新的字体大小
        // 但在这个示例中,我们只是简单地打印一条消息
        System.out.println("字体大小已调整为: " + size);
    }

    // 其他可能的文本编辑方法...
}

// 具体的命令(比如调整字体大小)  
class AdjustFontSizeCommand implements Command {
    private TextEditor textEditor;
    private int size;

    public AdjustFontSizeCommand(TextEditor textEditor, int size) {
        this.textEditor = textEditor;
        this.size = size;
    }

    @Override
    public void execute() {
        textEditor.adjustFontSize(size);
    }
}

// 客户端代码  
public class Client {
    public static void main(String[] args) {
        TextEditor textEditor = new TextEditor(); // 假设 TextEditor 是接收者,用于编辑文本  

        // 创建具体的命令  
        Command fontSizeUp = new AdjustFontSizeCommand(textEditor, 12); // 增大字体  
        Command fontSizeDown = new AdjustFontSizeCommand(textEditor, 10); // 减小字体  

        // 创建宏命令,并添加具体的命令  
        MacroCommand macroCommand = new MacroCommand();
        macroCommand.addCommand(fontSizeUp);
        macroCommand.addCommand(fontSizeDown); // 注意:这里只是示例,通常不会连续执行增大和减小字体  

        // 执行宏命令  
        macroCommand.execute(); // 首先执行 fontSizeUp,然后执行 fontSizeDown  
    }
}

/* Output:
字体大小已调整为: 12
字体大小已调整为: 10
*///~

示例解析3:图形移动/撤销(重做功能)(丐版)

命令模式也可以用来实现撤销和重做功能。每个命令对象都可以存储其执行前的状态,以便在需要时撤销操作。

这里是一个简化的示例,图形的状态信息,这里主要关注图形的位置,在这里给简化省略了,我们只是通过打印文字模拟了一下。因此省略掉了接收者角色,直接在具体命令的方法中进行打印。

带有图形位置状态信息,且由接收者处理请求的示例见示例解析4

在这个示例,适应一下一个简单的变体:缺少接收者。

类图

在这里插入图片描述

类图分析

撤销,重做需要记录之前的状态信息,在这个示例我们简化了状态信息的记录,执行命令,与撤销命令通过在具体命令中进行打印,因此省略了接收者处理请求逻辑,因此在类图就省略了接收者角色;

UndoManager调用者,持有一个命令执行记录栈,当需要撤销,从栈中弹出一个上一次操作的命令执行该命令的撤销方法。

这里的命令接口的定义,分了两层,一层是基础命令,用来执行命令,另外扩展的带撤销功能的命令接口,定义了撤销方法,具体命令实现接口,也就实现了执行/撤销的逻辑。

代码示例

package com.polaris.designpattern.list3.behavioral.pattern02.command.undodemo.simple;

import java.util.Stack;

// 基础的命令接口
interface Command {
    void execute(); // 执行命令  
}

// 带有撤销功能的命令接口  
interface UndoableCommand extends Command {
    void undo(); // 撤销操作  
}

// 具体命令:具体的撤销命令(比如移动图形)
class MoveShapeCommand implements UndoableCommand {
    // 假设这里包含移动图形所需的状态信息  
    // 例如:起始位置、目标位置等  

    @Override
    public void execute() {
        // 执行移动操作  
        System.out.println("执行移动操作");
        // ... 实际移动图形的代码 ...  
    }

    @Override
    public void undo() {
        // 撤销移动操作,将图形移回原来的位置  
        System.out.println("撤销移动操作");
        // ... 实际撤销移动的代码 ...  
    }
}

// 撤销管理器(调用者)  
class UndoManager {
    private Stack<UndoableCommand> undoStack = new Stack<>();

    public void executeCommand(UndoableCommand command) {
        command.execute();
        undoStack.push(command); // 将命令添加到撤销栈中  
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            UndoableCommand command = undoStack.pop();
            command.undo(); // 调用命令的撤销方法  
            System.out.println("撤销成功");
        } else {
            System.out.println("没有可撤销的命令");
        }
    }

    // 可能还需要一个检查是否有可撤销命令的方法  
    public boolean canUndo() {
        return !undoStack.isEmpty();
    }

    // 如果需要重做功能,可以添加相应的栈和逻辑  
}

// 示例使用  
public class Client {
    public static void main(String[] args) {
        UndoManager undoManager = new UndoManager();
        UndoableCommand moveCommand = new MoveShapeCommand();

        // 执行命令  
        undoManager.executeCommand(moveCommand);

        // 撤销命令  
        if (undoManager.canUndo()) {
            undoManager.undo();
        }
    }
}
/* Output:
执行移动操作
撤销移动操作
撤销成功
*///~

示例解析4:图形移动/撤销(重做功能)

相较于上一个示例(示例3),通过增加接收者Shape类用来处理图形移动逻辑与Point类用来记录图形的位置信息。

类图

在这里插入图片描述

类图分析

  1. UndoManager调用者在执行命令时候会将执行的命令压栈到栈中,当需要撤销操作时候,出栈弹出上一次的命令,执行撤销动作
  2. 由于需要支持撤销,因此调用者必须持有的具体命令必须带有可撤销能力,因此需要持有 UndoableCommand 类型对象
  3. 接收者 Shape 构造函数需要指定 Point对象,用来记录它的位置,它有一个获取图形当前所在位置的方法 getPositon() 和 移动到指定位置的方法 moveTo(Point)该方法需要传入一个目标位置
  4. 具体命令构造器需要参数接收者 Shape 对象用来完成移动/撤销移动操作,同时获取该图形的当前位置信息,即原始位置,记录下来 ,用于撤销时候使用
  5. 具体命令构造器需要参数接收者 Point 对象 targetPosition 用来表示需要移动到的位置

代码示例

package com.polaris.designpattern.list3.behavioral.pattern02.command.undodemo.complex;


import java.util.Stack;

// 基础的命令接口(如果不需要,可以省略)
interface Command {
    void execute(); // 执行命令  
}

// 带有撤销功能的命令接口  
interface UndoableCommand extends Command {
    void undo(); // 撤销操作  
}

// 图形类:用于记录移动图形所需的状态信息
class Shape {
    Point position; // 假设有一个Point类表示位置  

    Shape(Point position) {
        this.position = position;
        System.out.println("图形原位置:" + position);
    }

    // ... 其他图形相关的方法 ...  

    void moveTo(Point newPosition) {
        this.position = newPosition;
        System.out.println("图形移动到: " + newPosition);
    }

    Point getPosition() {
        return position;
    }
}

// 点类,表示位置:用于记录移动图形所需的状态信息
class Point {
    int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

// 具体命令:具体的撤销命令(比如移动图形)
class MoveShapeCommand implements UndoableCommand {
    private Shape shape;
    private Point originalPosition; // 原始位置  
    private Point targetPosition; // 目标位置  

    public MoveShapeCommand(Shape shape, Point targetPosition) {
        this.shape = shape;
        this.originalPosition = new Point(shape.getPosition().x, shape.getPosition().y); // 保存原始位置  
        this.targetPosition = targetPosition;
    }

    @Override
    public void execute() {
        // 执行移动操作  
        System.out.println("执行移动操作");
        shape.moveTo(targetPosition); // 实际移动图形的代码  
    }

    @Override
    public void undo() {
        // 撤销移动操作,将图形移回原来的位置  
        System.out.println("撤销移动操作");
        shape.moveTo(originalPosition); // 实际撤销移动的代码  
    }
}

// 撤销管理器(调用者)
class UndoManager {
    private Stack<UndoableCommand> undoStack = new Stack<>();

    public void executeCommand(UndoableCommand command) {
        command.execute();
        undoStack.push(command); // 将命令添加到撤销栈中
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            UndoableCommand command = undoStack.pop();
            command.undo(); // 调用命令的撤销方法
            System.out.println("撤销成功");
        } else {
            System.out.println("没有可撤销的命令");
        }
    }

    // 可能还需要一个检查是否有可撤销命令的方法
    public boolean canUndo() {
        return !undoStack.isEmpty();
    }

    // 如果需要重做功能,可以添加相应的栈和逻辑
}

// 示例使用
public class Client {
    public static void main(String[] args) {
        UndoManager undoManager = new UndoManager();
        Shape shape = new Shape(new Point(0, 0)); // 创建一个在(0, 0)位置的图形  
        UndoableCommand moveCommand = new MoveShapeCommand(shape, new Point(10, 10)); // 移动到(10, 10)位置  

        // 执行命令
        undoManager.executeCommand(moveCommand);

        // 撤销命令
        if (undoManager.canUndo()) {
            undoManager.undo();
        }
    }
}
/* Output:
图形原位置:(0, 0)
执行移动操作
图形移动到: (10, 10)
撤销移动操作
图形移动到: (0, 0)
撤销成功
*///~

在这个示例中,我们添加了一个Shape类来表示图形,以及一个Point类来表示位置。MoveShapeCommand现在保存了图形的原始位置和目标位置,以便在executeundo方法中使用。在Main类的示例中,我们创建了一个在(0, 0)位置的图形,并创建了一个命令来将其移动到(10, 10)位置。然后,我们通过UndoManager执行该命令并撤销它。

---

👈️上一篇:模板方法模式    |   下一篇:职责链模式👉️

设计模式-专栏👈️

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

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

相关文章

linux进程间通讯指南-打通IPC大门,高效沟通无阻

在现代操作系统中&#xff0c;进程就像独立的个体&#xff0c;有时需要相互合作、数据共享&#xff0c;这就要求进程间能够高效通信。本文将为你揭开Linux进程间通信(IPC)的神秘面纱&#xff0c;探讨各种IPC工具的运作原理&#xff0c;同步机制的重要性&#xff0c;以及如何规避…

Ubuntu安装cuda

文章目录 前言一、安装NVIDIA驱动1.1 过程中的问题1.2 解决方法1.3 重启后出现 perform MOK management 二、安装Cuda2.1 检查是否安装显卡驱动2.2 安装Cuda2.3 验证CUDA是否安装成功 三、配置环境变量---未完2.4 图片居中加调整大学 总结 #pic_center 前言 只是为方便学习&…

淘宝扭蛋机源码解析:功能实现与技术细节

随着在线购物和娱乐的融合&#xff0c;淘宝扭蛋机作为一种创新的购物娱乐方式&#xff0c;受到了广大用户的喜爱。本文将深入解析淘宝扭蛋机的源码&#xff0c;探讨其功能实现与技术细节&#xff0c;以期为开发者们提供一些有价值的参考。 一、功能实现 1.用户登录与注册 淘宝…

win11通过网线分享网络到Ubuntu工控机

1.条件&#xff1a;一个能无线联网的win11&#xff0c;一根网线&#xff0c;一台Ubuntu工控机&#xff0c;并且使用网线连接两者 2.在win11电脑上 2.1 打开控制面板的网络和Internet 2.2 进入网络和共享中心&#xff0c;在左侧进入 更改适配器设置 2.3 在WLAN上右键&#xff0…

R语言数据探索和分析21-中国GDP及其影响因素多元线性回归分析

一、研究背景和意义 GDP 是宏观经济中最受关注的经济统计数字&#xff0c;目前我国国内生产总值年均增长率均明显高于同期美、日等发达经济体和巴 西、俄罗斯、南非、印度等其他金砖国家&#xff0c;成为世界经济增长的主力军&#xff0c;GDP 的增长对一个国家有着十分重要的意…

TSINGSEE青犀视频:城市道路积水智能监管,智慧城市的守护者

随着城市化进程的加快&#xff0c;城市道路网络日益复杂&#xff0c;尤其在夏季&#xff0c;由于暴雨频发&#xff0c;道路积水问题成为影响城市交通和市民生活的重要因素之一。传统的道路积水监测方式往往依赖于人工巡逻和简单的监控设备&#xff0c;这些方法存在效率低下、响…

软信天成:告别数据脏乱差!企业数据清洗实战方案分享

低质量数据普遍存在。据统计&#xff0c;数据质量问题每年给企业造成高达3.1万亿美元的损失。为了防范这种损失&#xff0c;越来越多的企业采用数据清洗来清洗数据&#xff0c;提高数据质量。 数据清洗&#xff0c;顾名思义是将数据上“脏”的部分清洗掉&#xff0c;让数据变得…

读《淘宝技术这10年》:从进化中感受技术的美与挑战

本文作者:小米,一个热爱技术分享的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! 大家好,我是小米,一个29岁的程序员,喜欢分享技术干货。今天,我想和大家聊一聊我最近读的一本书——《淘宝技术这10年》。这本书让我深刻领悟…

JetBrains PhpStorm 激活码限时特惠 7.1 折快抢!

各位程序员&#xff0c;每天敲代码真的需要一款好用的 IDE&#xff0c;大名鼎鼎的 JetBrains 值得信赖&#xff01;PHP 开发看过来&#xff0c;PhpStorm 个人版首年订阅 618 限时特惠 7.1 折&#xff0c;有需要的朋友一定不要错过&#xff01; PhpStorm 汇集了众多效率功能和集…

【微信小程序】网络请求

出于安全性方面的考虑&#xff0c;小程序官方对数据接口的请求做出了如下两个限制&#xff1a; 只能请求HTTPS类型的接口必须将接口的域名添加到信任列表中 登录微信小程序管理后台->开发->开发设置->服务器域名->修改request合法域名。 注意事项&#xff1a; 域…

视频汇聚EasyCVR安防监控系统GA/T 1400协议视图库对接:技术实现与应用

随着信息技术的不断发展&#xff0c;各类协议标准在各个领域得到了广泛应用。GA/T1400协议作为公安视频监控系统中的一种重要标准&#xff0c;对于提升公安工作的信息化水平、加强社会治安防控具有重要意义。本文将重点探讨GA/T1400协议视图库对接的技术实现及应用价值。 一、…

领菲linfeeLNF96E多功能电力仪表智能数码液晶显示三相电压电流表

品牌 LINFEE 型号 LNF96E 货号 LNF96E 产地 中国大陆 省份 江苏省 地市 无锡市 装修及施工内容 安装工程 电源电路 交流电表 电表类型 多功能电度表 颜色分类 LNF96E-C,LNF96E-CM,LNF96E-CJ,LNF96E-CK,LNF96E-CJK,LNF96E-CMJK 多功能电力仪表,LNF96E三相多…

c语言练习:POJ 1003 宿醉(HangOver)

为什么写这篇文章 作为一名计算机相关方向的学生&#xff0c;本人的代码能力却十分差劲&#xff0c;这不能不让人万分羞愧。于是&#xff0c;决定从此好好学代码&#xff0c;每天坚持刷题。而C语言是计算机程序语言的基础&#xff0c;遂决定从c语言开始&#xff0c;提高自身编…

Nvidia/算能 +FPGA+AI大算力边缘计算盒子:中国舰船研究院

中国舰船研究院又称中国船舶重工集团公司第七研究院&#xff0c;隶属于中国船舶重工集团公司&#xff0c;是专门从事舰船研究、设计、开发的科学技术研究机构&#xff0c;是中国船舶重工集团公司的军品技术研究中心、科技开发中心&#xff1b;主要从事舰船武器装备发展战略研究…

图神经网络实战(12)——图同构网络(Graph Isomorphism Network, GIN)

图神经网络实战&#xff08;12&#xff09;——图同构网络 0. 前言1. 图同构网络原理2. 构建 GIN 模型执行图分类2.1 图分类任务2.2 PROTEINS 数据集分析2.3 构建 GIN 实现图分类2.4 GCN 与 GIN 性能差异分析 3. 提升模型性能小结系列链接 0. 前言 Weisfeiler-Leman (WL) 测试…

解决vscode终端不显示conda环境变量名称问题【详细步骤!实测可行!!】

最近在使用Visual Studio Code (VSCode) 时候&#xff0c;发现终端没有正确显示激活的conda环境名称&#xff0c;搜了一下&#xff0c;找到原因&#xff0c;记录一下&#xff0c;如果有人也遇到同样的问题&#xff0c;可以收藏一下。   分别两种情况&#xff0c;一是windows系…

GaussDB技术解读——GaussDB架构介绍(一)

目录 1 GaussDB 关键架构目标 2 GaussDB分布式架构 2.1 GaussDB 分布式关键技术架构 3 数据计算路由层&#xff08;Coordinator&#xff09;关键技术方案 3.1 分布式优化器 3.2 分布式执行框架 GaussDB是华为自主创新研发的关系型数据库&#xff0c;基于华为在数据库领域…

Python编程学习第一篇——制作一个小游戏休闲一下

到上期结束&#xff0c;我们已经学习了Python语言的基本数据结构&#xff0c;除了数值型没有介绍&#xff0c;数值型用的非常广&#xff0c;但也是最容易理解的&#xff0c;将在未来的学习中带大家直接接触和学习掌握。后续我们会开始学习这门语言的一些基础语法和编程技巧&…

C++候捷stl-视频笔记4

一个万用的hash function 哈希函数的形式&#xff0c;一种是一般函数(右边)&#xff0c;一种是成员函数(左边)&#xff0c;类的对象将成为函数对象 具体做法例子。直接把属性的所有hash值加起来&#xff0c;会在hashtable中会产生很多的碰撞&#xff0c;放在同一个bucket中的元…

嵌入式学习记录6.5(内存分配/构造函数/析构函数)

目录 目录 一.c动态内存分配回收 1.1分配 1.2回收 1.3new、delete和malloc、free之间的区别(重点&#xff09; 二.构造函数 2.1功能,格式 2.2示例 三.析构函数 3.1功能&#xff0c;格式 3.2特点 3.3示例 四.思维导图/练习 4.1思维导图 4.2练习 一.c动态内存分配回…