22、命令模式:实现灵活解耦的编程利器

命令模式:解耦调用与执行

命令模式:实现灵活解耦的编程利器

1. 命令模式概述

命令模式是一种用于封装方法调用的编程模式,它与普通函数有诸多不同。命令模式具备参数化和传递方法调用的能力,可在需要时随时执行,还能将调用操作的对象与实现操作的对象解耦,极大地增强了具体类替换的灵活性。

1.1 命令模式的应用场景

  • 用户界面创建 :在创建用户界面时,命令模式非常有用,特别是需要无限撤销操作的场景。
  • 替代回调函数 :它可以替代回调函数,在对象间传递操作时提供更高的模块化程度。

1.2 命令的结构

在最简单的形式中,命令对象将两个要素绑定在一起:一个动作和一个可能希望调用该动作的对象。所有命令对象都有一个共同的操作—— execute ,用于调用其所绑定的动作。在大多数命令对象中,这个操作是名为 execute run 的方法。使用相同接口的所有命令对象都可以被同等对待,并且可以随意互换。

下面通过一个动态用户界面的示例来说明命令模式的典型用法。假设你有一家广告公司,要创建一个网页,让客户能够对其账户进行特定操作,比如启动和停止特定广告的运行。由于不清楚会有多少个广告,因此需要创建一个尽可能灵活的用户界面(UI)。可以使用命令模式将 UI 元素(如按钮)与操作进行松散耦合。

操作步骤:
  1. 定义接口 :首先,需要一个所有命令都必须响应的接口:
/* AdCommand interface. */
var AdCommand = new Interface('AdCommand', ['execute']);
  1. 创建命令类 :接下来,需要两个类,一个用于封装广告的启动方法,另一个用于封装广告的停止方法:
/* StopAd command class. */
var StopAd = function(adObject) { // implements AdCommand
    this.ad = adObject;
};
StopAd.prototype.execute = function() {
    this.ad.stop();
};

/* StartAd command class. */
var StartAd = function(adObject) { // implements AdCommand
    this.ad = adObject;
};
StartAd.prototype.execute = function() {
    this.ad.start();
};
  1. 实现 UI 代码 :以下 UI 为用户账户中的每个广告提供了两个按钮,一个用于启动广告轮播,另一个用于停止广告轮播:
/* Implementation code. */
var ads = getAds();
for(var i = 0, len = ads.length; i < len; i++) {
    // Create command objects for starting and stopping the ad.
    var startCommand = new StartAd(ads[i]);
    var stopCommand = new StopAd(ads[i]);
    // Create the UI elements that will execute the command on click.
    new UiButton('Start ' + ads[i].name, startCommand);
    new UiButton('Stop ' + ads[i].name, stopCommand);
}

UiButton 类的构造函数接受一个按钮标签和一个命令对象,然后在页面上创建一个按钮,当点击该按钮时调用命令的 execute 方法。这个模块不需要了解所使用的命令对象的具体实现,因为每个命令都实现了 execute 方法,所以可以传入任何类型的命令, UiButton 类都知道如何与之交互,这使得创建高度模块化和解耦的用户界面成为可能。

1.3 使用闭包创建命令

除了创建对象并为其添加 execute 方法外,还可以使用闭包来封装函数。当只需要创建一个只有一个方法的命令对象时,这种方法特别有效。可以直接将希望执行的方法包装在闭包中,然后直接作为函数执行,这样可以避免担心作用域和 this 关键字的绑定问题。

以下是使用闭包重写的上述示例:

/* Commands using closures. */
function makeStart(adObject) {
    return function() { 
        adObject.start();
    };
}

function makeStop(adObject) {
    return function() {
        adObject.stop();
    };
}

/* Implementation code. */
var startCommand = makeStart(ads[0]);
var stopCommand = makeStop(ads[0]);
startCommand(); // Execute the functions directly instead of calling a method.
stopCommand();

这些命令函数可以像命令对象一样被传递,并在需要时执行。它们可以作为创建完整类的简单替代方案,但在需要多个命令方法的情况下(如后续的撤销示例)则不适用。

1.4 命令模式中的角色

命令模式系统中有三个参与者:客户端、调用者和接收者。
- 客户端 :实例化命令并将其传递给调用者。在前面的示例中,客户端是 for 循环中的代码。通常,客户端代码会封装在一个对象中,但这不是必需的。
- 调用者 :接收并持有命令。在某些时候,它可能会调用命令的 execute 方法,或者将命令传递给另一个潜在的调用者。在示例中,调用者是由 UiButton 类创建的按钮,当用户点击按钮时,它会调用 execute 方法。
- 接收者 :实际执行动作的对象。当调用者调用 commandObject.execute() 时,该方法会执行 receiver.action() 。在示例中,接收者是广告对象,动作是 start stop 方法。

为了便于记忆,可以记住:客户端创建命令,调用者执行命令,接收者在命令执行时执行动作。除了客户端外,这些名称在一定程度上描述了它们的功能,有助于理解。

所有使用命令模式的系统都有客户端和调用者,但接收者并非总是必需的。可以创建复杂(但模块化程度较低)的命令,这些命令不调用接收者对象的方法,而是执行复杂的查询或命令。

1.5 使用接口与命令模式

命令模式需要某种类型的接口。该接口用于确保接收者实现所需的动作,并且命令对象实现正确的 execute 操作(该操作可以有任何名称,但通常是 execute run ,在特殊情况下是 undo )。如果没有这些检查,代码将变得脆弱,容易出现难以调试的运行时错误。

在代码中,声明一个单一的 Command 接口并在需要命令对象时使用它是很有用的。这样,所有命令对象都将使用相同的名称来表示 execute 操作,并且可以在不进行任何修改的情况下互换。接口可以如下所示:

/* Command interface. */
var Command = new Interface('Command', ['execute']);

可以使用以下代码检查命令是否实现了正确的 execute 操作:

/* Checking the interface of a command object. */
// Ensure that the execute operation is defined. If not, a descriptive exception
// will be thrown.
Interface.ensureImplements(someCommand, Command);
// If no exception is thrown, you can safely invoke the execute operation. 
someCommand.execute(); 

如果使用闭包创建命令函数,检查会更简单,只需检查命令是否确实是一个函数:

if(typeof someCommand != 'function') {
    throw new Error('Command isn't a function');
}

1.6 命令对象的类型

所有类型的命令对象都执行相同的任务:将调用操作的对象与实际执行操作的对象解耦。在这个定义范围内,存在两个极端情况。

类型 特点 示例代码
简单命令对象 是现有接收者动作(如广告对象的 start stop 方法)与调用者(如按钮)之间的简单绑定,具有最高的模块化程度,与客户端、接收者和调用者仅松散耦合。 javascript /* SimpleCommand, a loosely coupled, simple command class. */ var SimpleCommand = function(receiver) { // implements Command this.receiver = receiver; }; SimpleCommand.prototype.execute = function() { this.receiver.action(); };
复杂命令对象 封装了一组复杂的指令,实际上没有接收者,因为动作是在命令对象内部具体实现的。它包含执行动作所需的所有代码。 javascript /* ComplexCommand, a tightly coupled, complex command class. */ var ComplexCommand = function() { // implements Command this.logger = new Logger(); this.xhrHandler = XhrManager.createXhrHandler(); this.parameters = {}; }; ComplexCommand.prototype = { setParameter: function(key, value) { this.parameters[key] = value; }, execute: function() { this.logger.log('Executing command'); var postArray = []; for(var key in this.parameters) { postArray.push(key + '=' + this.parameters[key]); } var postString = postArray.join('&'); this.xhrHandler.request( 'POST', 'script.php', function() {}, postString ); } };
中间类型命令对象 execute 方法中既有一些实现代码,又包含接收者的动作,处于简单和复杂之间。 javascript /* GreyAreaCommand, somewhere between simple and complex. */ var GreyAreaCommand = function(receiver) { // implements Command this.logger = new Logger(); this.receiver = receiver; }; GreyAreaCommand.prototype.execute = function() { this.logger.log('Executing command'); this.receiver.prepareAction(); this.receiver.action(); };

每种类型的命令对象都有其用途,在项目中都有其适用的场景。简单命令对象通常用于解耦两个对象(接收者和调用者),而复杂命令对象通常用于封装原子或事务性指令。

以下是命令对象类型的 mermaid 流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A([命令对象]):::startend --> B(简单命令对象):::process
    A --> C(复杂命令对象):::process
    A --> D(中间类型命令对象):::process
    B --> E(松散耦合):::process
    B --> F(模块化程度高):::process
    C --> G(紧密耦合):::process
    C --> H(封装复杂指令):::process
    D --> I(部分实现代码):::process
    D --> J(包含接收者动作):::process

2. 示例:菜单项

2.1 示例概述

这个示例展示了最简单类型的命令如何用于构建模块化的用户界面。将构建一个用于创建桌面应用程序风格菜单栏的类,并使用命令对象让这些菜单执行各种操作。命令模式允许将调用者(菜单项)与接收者(实际执行动作的对象)解耦。菜单项不需要了解如何使用接收者对象,只需要知道所有命令对象都实现了 execute 方法。这意味着相同的命令对象也可以用于其他 UI 元素,如工具栏图标,而无需进行任何修改。

2.2 定义接口

由于在菜单中使用了组合模式,而组合对象严重依赖接口,因此为这个示例定义了三个接口:

/* Command, Composite and MenuObject interfaces. */
var Command = new Interface('Command', ['execute']);
var Composite = new Interface('Composite', ['add', 'remove', 'getChild', 'getElement']);
var MenuObject = new Interface('MenuObject', ['show']);

2.3 菜单组合类

2.3.1 MenuBar 类

MenuBar 类是一个组合类,用于持有所有的 Menu 实例:

/* MenuBar class, a composite. */
var MenuBar = function() { // implements Composite, MenuObject
    this.menus = {};
    this.element = document.createElement('ul');
    this.element.style.display = 'none';
};
MenuBar.prototype = {
    add: function(menuObject) {
        Interface.ensureImplements(menuObject, Composite, MenuObject);
        this.menus[menuObject.name] = menuObject;
        this.element.appendChild(this.menus[menuObject.name].getElement());
    },
    remove: function(name) {
        delete this.menus[name];
    },
    getChild: function(name) {
        return this.menus[name];
    },
    getElement: function() {
        return this.element;
    },
    show: function() {
        this.element.style.display = 'block';
        for(name in this.menus) { // Pass the call down the composite.
            this.menus[name].show();
        }
    }
};
2.3.2 Menu 类

Menu 类也是一个组合类,它为 MenuItem 实例执行与 MenuBar 类似的操作:

/* Menu class, a composite. */
var Menu = function(name) { // implements Composite, MenuObject
    this.name = name;
    this.items = {};
    this.element = document.createElement('li');
    this.element.innerHTML = this.name;
    this.element.style.display = 'none';
    this.container = document.createElement('ul');
    this.element.appendChild(this.container);
};
Menu.prototype = {
    add: function(menuItemObject) {
        Interface.ensureImplements(menuItemObject, Composite, MenuObject);
        this.items[menuItemObject.name] = menuItemObject;
        this.container.appendChild(this.items[menuItemObject.name].getElement());
    },
    remove: function(name) {
        delete this.items[name];
    },
    getChild: function(name) {
        return this.items[name];
    },
    getElement: function() {
        return this.element;
    },
    show: function() {
        this.element.style.display = 'block';
        for(name in this.items) { // Pass the call down the composite.
            this.items[name].show();
        }
    }
};

需要注意的是, Menu 类中的 items 属性用作查找表,而不是用于维护菜单项的顺序。顺序是使用 DOM 来维护的,每个菜单项在添加时会被追加。如果对这些项进行重新排序很重要,可以将 items 属性实现为数组。

2.3.3 MenuItem 类

MenuItem 类是调用者类,当用户点击 MenuItem 实例时,它会调用绑定的命令。为了实现这一点,首先要确保传递给构造函数的命令对象实现了 execute 方法,然后将其作为事件附加到 MenuItem 对象的锚标签上:

/* MenuItem class, a leaf. */
var MenuItem = function(name, command) { // implements Composite, MenuObject
    Interface.ensureImplements(command, Command);
    this.name = name;
    this.element = document.createElement('li');
    this.element.style.display = 'none';
    this.anchor = document.createElement('a');
    this.anchor.href = '#'; // To make it clickable.
    this.element.appendChild(this.anchor);
    this.anchor.innerHTML = this.name;
    addEvent(this.anchor, 'click', function(e) { // Invoke the command on click.
        e.preventDefault(); 
        command.execute();
    });
};
MenuItem.prototype = {
    add: function() {},
    remove: function() {},
    getChild: function() {},
    getElement: function() {
        return this.element;
    },
    show: function() {
        this.element.style.display = 'block';
    }
};

这正是命令模式的优势所在。可以创建一个非常复杂的菜单栏,其中包含多个菜单,每个菜单又包含多个菜单项。这些菜单项不需要知道如何执行它们所绑定的动作,只需要知道命令对象有一个 execute 方法。每个 MenuItem 都绑定到一个命令,由于该命令被封装在闭包中并作为事件监听器附加,因此无法更改。如果需要更改菜单项所绑定的命令,必须创建一个新的 MenuItem 对象。

2.4 命令类

MenuCommand 类是一个非常简单的命令类,构造函数接受一个参数:要作为动作调用的方法。由于 JavaScript 可以将方法的引用作为参数传递,因此命令类只需要存储这个引用,然后在调用 execute 方法时调用它。这本质上是一个包装函数的对象:

/* MenuCommand class, a command object. */
var MenuCommand = function(action) { // implements Command
    this.action = action;
};
MenuCommand.prototype.execute = function() {
    this.action();
};

如果动作方法内部使用了 this 关键字,则必须将其包装在一个匿名函数中,例如:

var someCommand = new MenuCommand(function() { myObj.someMethod(); }); 

2.5 整合所有内容

设置这个复杂架构的最终结果是,实现代码具有高度的松散耦合性,并且易于理解。需要创建一个 MenuBar 类的实例,并向其中添加 Menu MenuItem 对象。每个 MenuItem 对象都绑定了一个命令:

/* Implementation code. */
/* Receiver objects, instantiated from existing classes. */
var fileActions = new FileActions();
var editActions = new EditActions();
var insertActions = new InsertActions();
var helpActions = new HelpActions();

/* Create the menu bar. */
var appMenuBar = new MenuBar();

/* The File menu. */
var fileMenu = new Menu('File');
var openCommand = new MenuCommand(fileActions.open);
var closeCommand = new MenuCommand(fileActions.close);
var saveCommand = new MenuCommand(fileActions.save);
var saveAsCommand = new MenuCommand(fileActions.saveAs);
fileMenu.add(new MenuItem('Open', openCommand));
fileMenu.add(new MenuItem('Close', closeCommand));
fileMenu.add(new MenuItem('Save', saveCommand));
fileMenu.add(new MenuItem('Save As...', saveAsCommand));
appMenuBar.add(fileMenu);

/* The Edit menu. */
var editMenu = new Menu('Edit');
var cutCommand = new MenuCommand(editActions.cut);
var copyCommand = new MenuCommand(editActions.copy);
var pasteCommand = new MenuCommand(editActions.paste);
var deleteCommand = new MenuCommand(editActions.delete);
editMenu.add(new MenuItem('Cut', cutCommand));
editMenu.add(new MenuItem('Copy', copyCommand));
editMenu.add(new MenuItem('Paste', pasteCommand));
editMenu.add(new MenuItem('Delete', deleteCommand));
appMenuBar.add(editMenu);

/* The Insert menu. */
var insertMenu = new Menu('Insert');
// 后续可以继续添加菜单项和命令...

通过以上步骤,我们详细介绍了命令模式的概念、结构、不同类型的命令对象以及如何在实际项目中应用命令模式来构建模块化的用户界面。命令模式通过解耦调用者和接收者,提供了更高的灵活性和可维护性,是一种非常实用的编程模式。

3. 命令模式的优势与应用总结

3.1 命令模式的优势

命令模式在软件开发中具有显著的优势,以下是详细总结:
- 解耦调用者和接收者 :命令模式将调用操作的对象(调用者)与实际执行操作的对象(接收者)分离开来。调用者只需要知道如何调用命令的 execute 方法,而不需要了解接收者的具体实现。这种解耦使得系统的各个部分更加独立,提高了代码的可维护性和可扩展性。例如,在菜单示例中,菜单项(调用者)不需要知道具体的文件操作(接收者)是如何实现的,只需要调用命令的 execute 方法即可。
- 可参数化和传递方法调用 :命令模式允许将方法调用作为参数进行传递和存储。可以在需要的时候执行这些方法调用,实现了方法调用的延迟执行和灵活调度。例如,在广告管理的例子中,可以根据需要创建启动和停止广告的命令对象,并在合适的时机执行这些命令。
- 支持撤销和重做操作 :由于命令对象封装了操作的细节,因此可以很方便地实现撤销和重做功能。可以通过记录命令的执行历史,反向执行相应的命令来实现撤销操作,再次执行已撤销的命令来实现重做操作。虽然本文未详细介绍撤销操作的实现,但命令模式为其提供了良好的基础。
- 提高代码的模块化程度 :命令模式将不同的操作封装在不同的命令对象中,每个命令对象都有明确的职责。这种模块化的设计使得代码更加清晰,易于理解和维护。例如,在菜单系统中,每个菜单项对应的命令对象都独立负责执行特定的操作,使得菜单系统的功能可以方便地扩展和修改。

3.2 命令模式的应用场景

命令模式在许多场景中都有广泛的应用,以下是一些常见的应用场景:
| 应用场景 | 说明 |
| ---- | ---- |
| 用户界面设计 | 在创建用户界面时,命令模式可以将 UI 元素(如按钮、菜单项等)与具体的操作解耦。UI 元素只需要调用命令的 execute 方法,而不需要关心操作的具体实现。例如,在桌面应用程序的菜单栏中,每个菜单项都可以绑定一个命令对象,当用户点击菜单项时,执行相应的命令。 |
| 任务调度系统 | 命令模式可以用于实现任务调度系统,将任务封装成命令对象,通过调度器来管理和执行这些任务。调度器可以根据任务的优先级、执行时间等条件来安排任务的执行顺序。 |
| 游戏开发 | 在游戏开发中,命令模式可以用于处理玩家的输入和游戏中的各种动作。例如,玩家的按键操作可以映射为不同的命令对象,游戏引擎根据这些命令对象来更新游戏状态。 |
| 事务处理系统 | 在事务处理系统中,命令模式可以用于封装事务的操作。可以将一系列的操作封装在一个命令对象中,通过执行该命令对象来完成事务的处理。同时,命令模式还可以支持事务的回滚操作,提高系统的可靠性。 |

3.3 命令模式的注意事项

在使用命令模式时,需要注意以下几点:
- 接口的一致性 :为了确保命令对象可以互换使用,所有的命令对象都应该实现相同的接口。通常,这个接口包含一个 execute 方法。在代码中,可以使用接口检查来确保命令对象实现了正确的接口,避免运行时错误。
- 内存管理 :如果命令对象持有对接收者对象的引用,并且这些命令对象在系统中长时间存在,可能会导致内存泄漏。因此,在不需要使用命令对象时,应该及时释放它们的引用。
- 命令对象的复杂度 :虽然命令模式可以封装复杂的操作,但应该尽量保持命令对象的简单性。过于复杂的命令对象会增加代码的复杂度,降低代码的可读性和可维护性。可以将复杂的操作拆分成多个简单的命令对象来实现。

4. 命令模式的实现流程总结

为了更好地理解和应用命令模式,以下是实现命令模式的一般流程:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px

    A([开始]):::startend --> B(定义接口):::process
    B --> C(创建命令类):::process
    C --> D(创建调用者和接收者对象):::process
    D --> E(将命令对象传递给调用者):::process
    E --> F(调用者调用命令的 execute 方法):::process
    F --> G(命令对象调用接收者的操作):::process
    G --> H([结束]):::startend

4.1 定义接口

定义一个接口,规定所有命令对象必须实现的方法,通常是 execute 方法。例如:

/* Command interface. */
var Command = new Interface('Command', ['execute']);

4.2 创建命令类

创建具体的命令类,实现接口中定义的方法。每个命令类封装一个具体的操作,通常包含一个接收者对象,并在 execute 方法中调用接收者的相应操作。例如:

/* SimpleCommand, a loosely coupled, simple command class. */
var SimpleCommand = function(receiver) { // implements Command
    this.receiver = receiver;
};
SimpleCommand.prototype.execute = function() {
    this.receiver.action();
};

4.3 创建调用者和接收者对象

创建调用者对象(如按钮、菜单项等)和接收者对象(实际执行操作的对象)。例如,在菜单示例中, MenuItem 是调用者对象, FileActions 是接收者对象。

4.4 将命令对象传递给调用者

将创建好的命令对象传递给调用者对象,使得调用者可以在需要的时候调用命令的 execute 方法。例如:

var openCommand = new MenuCommand(fileActions.open);
var menuItem = new MenuItem('Open', openCommand);

4.5 调用者调用命令的 execute 方法

当调用者接收到用户的操作(如点击按钮、选择菜单项等)时,调用命令的 execute 方法。例如,在 MenuItem 类中,当用户点击菜单项时,调用绑定命令的 execute 方法:

addEvent(this.anchor, 'click', function(e) { // Invoke the command on click.
    e.preventDefault(); 
    command.execute();
});

4.6 命令对象调用接收者的操作

命令对象的 execute 方法被调用后,会调用接收者的相应操作,完成具体的任务。例如,在 MenuCommand 类中, execute 方法调用接收者的操作:

MenuCommand.prototype.execute = function() {
    this.action();
};

5. 总结与展望

5.1 总结

命令模式是一种强大的设计模式,通过封装方法调用,实现了调用者和接收者的解耦,提高了代码的模块化程度和可维护性。本文详细介绍了命令模式的概念、结构、实现方式和应用场景,通过广告管理和菜单系统的示例,展示了命令模式的实际应用。同时,还总结了命令模式的优势、注意事项和实现流程,帮助读者更好地理解和应用命令模式。

5.2 展望

命令模式在软件开发中有着广泛的应用前景,随着技术的不断发展,命令模式也将不断演进和完善。以下是一些可能的发展方向:
- 与其他设计模式的结合 :命令模式可以与其他设计模式(如观察者模式、状态模式等)结合使用,实现更加复杂和强大的系统。例如,结合观察者模式可以实现命令执行结果的通知机制,结合状态模式可以根据系统的不同状态来执行不同的命令。
- 在分布式系统中的应用 :在分布式系统中,命令模式可以用于实现远程过程调用(RPC)和任务调度。可以将命令对象序列化并在不同的节点之间传递,实现跨节点的操作调用和任务执行。
- 自动化测试和调试 :命令模式的封装性使得代码更加易于测试和调试。可以通过模拟命令对象的执行来进行单元测试,通过记录命令的执行历史来进行调试和故障排查。未来,可能会有更多的工具和框架利用命令模式的特点来提高软件开发的效率和质量。

通过深入理解和应用命令模式,开发者可以提高代码的质量和可维护性,更好地应对软件开发中的各种挑战。希望本文能够帮助读者掌握命令模式的核心思想和应用方法,在实际项目中发挥其优势。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值