23、深入理解命令模式:原理、应用与优缺点

命令模式详解及应用场景

深入理解命令模式:原理、应用与优缺点

1. 命令模式构建菜单

在软件开发中,我们常常需要构建各种菜单来提供用户操作入口。使用命令模式可以很好地实现菜单功能,并且使代码更加模块化。以下是一个简单的菜单构建示例:

var textBlockCommand = new MenuCommand(insertActions.textBlock);
insertMenu.add(new MenuItem('Text Block', textBlockCommand));
appMenuBar.add(insertMenu);

/* The Help menu. */
var helpMenu = new Menu('Help');
var showHelpCommand = new MenuCommand(helpActions.showHelp);
helpMenu.add(new MenuItem('Show Help', showHelpCommand));
appMenuBar.add(helpMenu);

/* Build the menu bar. */
document.getElementsByTagName('body')[0].appendChild(appMenuBar.getElement());
appMenuBar.show();

如果后续需要添加更多菜单项,也非常简单。例如,要在插入菜单中添加一个插入图片的命令,只需要两行代码:

var imageCommand = new MenuCommand(insertActions.image);
insertMenu.add(new MenuItem('Image', imageCommand));

命令模式的核心优势在于将接收用户请求的对象与实现该请求的对象解耦。这使得负责执行工作的类与构建用户界面的类可以分离,甚至可以让多个用户界面使用相同的接收器或命令对象。命令可以作为一等对象被重用和传递,不同的调用者都可以再次执行它。

2. 命令模式实现无限撤销功能

命令模式还可以方便地实现无限撤销功能。通过在命令对象中添加 undo 方法,我们可以让调用者回滚之前执行的操作。以下是一个类似“电子涂鸦板”游戏的示例,展示了如何使用命令模式实现无限撤销:
- 定义可撤销命令接口

/* ReversibleCommand interface. */
var ReversibleCommand = new Interface('ReversibleCommand', ['execute', 'undo']);
  • 创建移动命令类
/* Movement commands. */
var MoveUp = function(cursor) { // implements ReversibleCommand
    this.cursor = cursor;
};
MoveUp.prototype = {
    execute: function() {
        cursor.move(0, -10);
    },
    undo: function() {
        cursor.move(0, 10);    
    }
};

var MoveDown = function(cursor) { // implements ReversibleCommand
    this.cursor = cursor;
};
MoveDown.prototype = {
    execute: function() {
        cursor.move(0, 10);
    },
    undo: function() {
        cursor.move(0, -10);    
    }
};

var MoveLeft = function(cursor) { // implements ReversibleCommand
    this.cursor = cursor;
};
MoveLeft.prototype = {
    execute: function() {
        cursor.move(-10, 0);
    },
    undo: function() {
        cursor.move(10, 0);    
    }
};

var MoveRight = function(cursor) { // implements ReversibleCommand
    this.cursor = cursor;
};
MoveRight.prototype = {
    execute: function() {
        cursor.move(10, 0);
    },
    undo: function() {
        cursor.move(-10, 0);    
    }
};

这些命令类的 execute 方法将光标移动到指定方向, undo 方法则将光标移回相反方向。这种实现方式使得操作很容易被撤销,而不需要知道系统的先前状态。

  • 实现光标类
/* Cursor class. */
var Cursor = function(width, height, parent) {
    this.width = width;
    this.height = height;
    this.position = { x: width / 2, y: height / 2 };
    this.canvas = document.createElement('canvas');
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    parent.appendChild(this.canvas);
    this.ctx = this.canvas.getContext('2d');
    this.ctx.fillStyle = '#cc0000';
    this.move(0, 0);  
};
Cursor.prototype.move = function(x, y) {
    this.position.x += x;
    this.position.y += y; 
    this.ctx.clearRect(0, 0, this.width, this.height);
    this.ctx.fillRect(this.position.x, this.position.y, 3, 3);
};

Cursor 类负责实现命令类请求的操作,即在指定位置绘制一个正方形。

  • 使用装饰器模式增强命令类
    为了避免在每个用户界面类中重复实现将命令推送到撤销栈的代码,我们可以使用装饰器模式。以下是一个将命令推送到栈中再执行的装饰器:
/* UndoDecorator class. */
var UndoDecorator = function(command, undoStack) { // implements ReversibleCommand
    this.command = command;
    this.undoStack = undoStack;
};
UndoDecorator.prototype = {
    execute: function() {
        this.undoStack.push(this.command);
        this.command.execute();
    },
    undo: function() {
        this.command.undo();
    }
};

这个装饰器允许我们在执行命令之前将其推送到撤销栈中,同时保持命令对象的接口不变。

  • 创建按钮类
/* CommandButton class. */
var CommandButton = function(label, command, parent) {
    Interface.ensureImplements(command, ReversibleCommand);
    this.element = document.createElement('button');
    this.element.innerHTML = label;
    parent.appendChild(this.element);
    addEvent(this.element, 'click', function() {
        command.execute();
    });
};

/* UndoButton class. */
var UndoButton = function(label, parent, undoStack) {
    this.element = document.createElement('button');
    this.element.innerHTML = label;
    parent.appendChild(this.element);
    addEvent(this.element, 'click', function() {
        if(undoStack.length === 0) return;
        var lastCommand = undoStack.pop();
        lastCommand.undo();
    });
};

CommandButton 类用于调用命令的 execute 方法, UndoButton 类用于调用命令的 undo 方法。

  • 实现代码
/* Implementation code. */
var body = document.getElementsByTagName('body')[0];
var cursor = new Cursor(400, 400, body);
var undoStack = [];
var upCommand = new UndoDecorator(new MoveUp(cursor), undoStack);
var downCommand = new UndoDecorator(new MoveDown(cursor), undoStack);
var leftCommand = new UndoDecorator(new MoveLeft(cursor), undoStack);
var rightCommand = new UndoDecorator(new MoveRight(cursor), undoStack);
var upButton = new CommandButton('Up', upCommand, body);
var downButton = new CommandButton('Down', downCommand, body);
var leftButton = new CommandButton('Left', leftCommand, body);
var rightButton = new CommandButton('Right', rightCommand, body);
var undoButton = new UndoButton('Undo', body, undoStack);

通过以上代码,我们创建了一个带有画布和五个按钮的界面。点击四个命令按钮可以移动光标,点击撤销按钮可以撤销之前的移动操作。

3. 处理不可撤销操作的撤销功能

前面的撤销示例适用于容易撤销的操作,如移动光标。但对于一些不可撤销的操作,如绘制线条,需要采用不同的方法。实现思路是记录所有执行的命令,当需要撤销操作时,弹出最后一个命令并丢弃,然后清空画布并重新执行之前的所有命令。

以下是修改后的代码:
- 修改命令类
移除所有命令类中的 undo 方法,因为操作不再是可逆的。例如:

/* Movement commands. */
var MoveUp = function(cursor) { // implements Command
    this.cursor = cursor;
};
MoveUp.prototype = {
    execute: function() {
        cursor.move(0, -10);
    }
};
  • 修改光标类
/* Cursor class, with an internal command stack. */
var Cursor = function(width, height, parent) {
    this.width = width;
    this.height = height;
    this.commandStack = [];
    this.canvas = document.createElement('canvas');
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    parent.appendChild(this.canvas);
    this.ctx = this.canvas.getContext('2d');
    this.ctx.strokeStyle = '#cc0000';
    this.move(0, 0);
};
Cursor.prototype = {
    move: function(x, y) {
        var that = this;
        this.commandStack.push(function() { that.lineTo(x, y); });
        this.executeCommands();
    },
    lineTo: function(x, y) {
        this.position.x += x;
        this.position.y += y; 
        this.ctx.lineTo(this.position.x, this.position.y);
    },
    executeCommands: function() {
        this.position = { x: this.width / 2, y: this.height / 2 };
        this.ctx.clearRect(0, 0, this.width, this.height); // Clear the canvas.
        this.ctx.beginPath();
        this.ctx.moveTo(this.position.x, this.position.y);
        for(var i = 0, len = this.commandStack.length; i < len; i++) {
            this.commandStack[i]();
        }
        this.ctx.stroke();
    },
    undo: function() {
        this.commandStack.pop();
        this.executeCommands();
    }
};
  • 修改撤销按钮类
/* UndoButton class. */
var UndoButton = function(label, parent, cursor) {
    this.element = document.createElement('button');
    this.element.innerHTML = label;
    parent.appendChild(this.element);
    addEvent(this.element, 'click', function() {
        cursor.undo();
    });  
};
  • 实现代码
/* Implementation code. */
var body = document.getElementsByTagName('body')[0];
var cursor = new Cursor(400, 400, body);
var upCommand = new MoveUp(cursor);
var downCommand = new MoveDown(cursor);
var leftCommand = new MoveLeft(cursor);
var rightCommand = new MoveRight(cursor);
var upButton = new CommandButton('Up', upCommand, body);
var downButton = new CommandButton('Down', downCommand, body);
var leftButton = new CommandButton('Left', leftCommand, body);
var rightButton = new CommandButton('Right', rightCommand, body);
var undoButton = new UndoButton('Undo', body, cursor);

通过这种方式,我们实现了一个具有无限撤销功能的在线电子涂鸦板,并且可以方便地添加新的操作按钮。

4. 命令日志用于崩溃恢复

命令日志还可以用于在程序崩溃后恢复程序状态。在前面的示例中,可以使用XHR将序列化的命令日志发送到服务器。当用户下次访问页面时,获取这些命令并恢复画布上的线条状态,就像浏览器关闭时一样。这不仅可以为用户维护状态,还允许用户撤销之前会话中的操作。在更复杂的应用中,这种日志记录的存储需求可能会很大,因此可以为用户提供一个按钮,用于提交目前为止的所有操作并清空命令栈。

5. 命令模式的使用场景

命令模式的主要目的是将调用对象(如用户界面、API、代理等)与实现操作的对象解耦。因此,在需要提高两个对象交互的模块化程度时,都可以使用命令模式。它是一种组织模式,几乎可以应用于任何系统,但在需要将操作规范化,使单一调用类可以调用各种方法而无需了解其具体实现的情况下最为有效。许多用户界面元素都非常适合使用命令模式,如前面示例中的菜单。使用命令模式可以使用户界面元素与执行工作的类完全解耦,从而可以在任何页面或项目中重用这些元素,并且可以被不同的用户界面元素调用。

此外,命令模式还有一些其他的应用场景:
- 封装回调函数 :在XHR调用或其他延迟调用的情况下,可以使用命令对象封装回调函数,将多个函数调用封装在一个包中。
- 实现撤销机制 :通过将执行的命令推送到栈中,可以轻松实现无限撤销功能,甚至对于不可撤销的操作也可以通过命令日志实现撤销。
- 崩溃恢复 :记录命令日志可以在程序崩溃后恢复程序状态。

6. 命令模式的优缺点
  • 优点
    • 提高模块化和灵活性 :正确使用命令模式可以使程序更加模块化和灵活,将调用对象与实现对象解耦,便于代码的维护和扩展。
    • 实现复杂功能 :可以轻松实现撤销和状态恢复等复杂而有用的功能。
    • 更多特性 :命令对象比简单的方法引用具有更多的特性,如参数化、定义额外的方法(如 undo )、定义操作的元数据等。
  • 缺点
    • 效率问题 :如果只是简单的方法调用,创建命令对象可能会导致效率低下。
    • 调试困难 :命令对象增加了代码的层次,可能会使调试变得更加困难,尤其是在运行时动态创建命令对象时,很难确定其具体执行的操作。

综上所述,命令模式是一种非常有用的设计模式,但在使用时需要根据具体情况权衡其优缺点,确保在合适的场景下使用。

深入理解命令模式:原理、应用与优缺点

7. 命令模式的操作流程总结

为了更清晰地理解命令模式在不同场景下的应用,下面将其操作流程进行总结:

7.1 菜单构建流程
步骤 操作内容
1 创建具体的命令对象,如 MenuCommand
2 将命令对象关联到菜单项,如 MenuItem
3 将菜单项添加到菜单中,如 insertMenu
4 将菜单添加到菜单栏,如 appMenuBar
5 将菜单栏添加到页面的 body 元素中并显示。

下面是对应的 mermaid 流程图:

graph LR
    A[创建命令对象] --> B[关联菜单项]
    B --> C[添加到菜单]
    C --> D[添加到菜单栏]
    D --> E[添加到页面并显示]
7.2 可撤销操作的无限撤销流程
步骤 操作内容
1 定义可撤销命令接口,如 ReversibleCommand
2 创建具体的命令类,实现 execute undo 方法。
3 实现接收器类,如 Cursor ,负责执行具体操作。
4 使用装饰器模式增强命令类,将命令推送到撤销栈。
5 创建命令按钮和撤销按钮类,分别调用 execute undo 方法。
6 实例化相关对象,创建界面并绑定操作。

对应的 mermaid 流程图如下:

graph LR
    A[定义接口] --> B[创建命令类]
    B --> C[实现接收器类]
    C --> D[使用装饰器]
    D --> E[创建按钮类]
    E --> F[实例化对象创建界面]
7.3 不可撤销操作的撤销流程
步骤 操作内容
1 修改命令类,移除 undo 方法。
2 修改接收器类,使用内部命令栈记录操作。
3 修改撤销按钮类,调用接收器的 undo 方法。
4 实例化相关对象,创建界面并绑定操作。

对应的 mermaid 流程图如下:

graph LR
    A[修改命令类] --> B[修改接收器类]
    B --> C[修改撤销按钮类]
    C --> D[实例化对象创建界面]
8. 命令模式与其他设计模式的结合

命令模式可以与其他设计模式结合使用,以实现更强大的功能。

8.1 与装饰器模式结合

在前面的无限撤销功能实现中,我们已经看到了命令模式与装饰器模式的结合。装饰器模式用于在执行命令之前将其推送到撤销栈,同时保持命令对象的接口不变。这种结合使得代码更加模块化,避免了在多个用户界面类中重复实现撤销栈的操作。

8.2 与工厂模式结合

可以使用工厂模式来创建命令对象。工厂模式可以根据不同的条件创建不同类型的命令对象,从而提高代码的可维护性和可扩展性。例如:

var CommandFactory = {
    createMoveCommand: function(direction, cursor) {
        switch (direction) {
            case 'up':
                return new MoveUp(cursor);
            case 'down':
                return new MoveDown(cursor);
            case 'left':
                return new MoveLeft(cursor);
            case 'right':
                return new MoveRight(cursor);
            default:
                throw new Error('Invalid direction');
        }
    }
};

var cursor = new Cursor(400, 400, document.body);
var upCommand = CommandFactory.createMoveCommand('up', cursor);
9. 命令模式的实际应用案例

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

9.1 文本编辑器

在文本编辑器中,各种操作如复制、粘贴、撤销、重做等都可以使用命令模式实现。每个操作对应一个命令对象,用户界面元素(如菜单、工具栏按钮)作为调用者,编辑器核心作为接收器。这样可以方便地实现撤销和重做功能,并且可以轻松扩展新的操作。

9.2 游戏开发

在游戏开发中,角色的移动、攻击、技能释放等操作都可以封装成命令对象。通过命令模式,可以实现游戏的回放功能,即记录玩家的所有操作命令,在需要时重新执行这些命令来重现游戏过程。

9.3 自动化测试

在自动化测试中,测试用例的执行可以看作是一系列命令的执行。每个测试步骤对应一个命令对象,测试框架作为调用者,被测试的系统作为接收器。使用命令模式可以方便地管理测试用例的执行顺序,并且可以实现测试用例的撤销和重试功能。

10. 总结与建议

命令模式是一种强大的设计模式,它通过将调用对象与实现对象解耦,提高了代码的模块化和灵活性。在实际开发中,我们可以根据具体的需求和场景来选择是否使用命令模式。

如果项目需要实现撤销、重做、状态恢复等功能,或者需要将操作规范化,使不同的调用者可以调用各种方法,那么命令模式是一个不错的选择。但在使用命令模式时,需要注意其可能带来的效率问题和调试困难,避免在简单的场景中过度使用。

同时,可以结合其他设计模式,如装饰器模式、工厂模式等,进一步提高代码的可维护性和可扩展性。通过合理使用命令模式,我们可以开发出更加健壮、灵活的软件系统。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值