Published on

命令模式详解

Authors
  • avatar
    Name
    青雲
    Twitter

命令模式(Command Pattern)是一种行为型设计模式,它将请求或操作封装成一个对象,从而使得可以用不同的请求、队列或日志来参数化其他对象。同时,它还支持可撤销的操作。

命令模式的核心在于,将请求封装成一个独立的对象,使得调用和处理解耦,可以实现更灵活的请求处理方式。

为什么需要命令模式?

日常生活中,我们用遥控器来操作电视:开机、关机、调高音量、切换频道等。我们可以把遥控器看作客户端,电视看作是接收者,遥控器上的每个按钮对应一个命令。按下某个按钮就会向电视发出一个命令,比如“开机”或“切换到频道5”。

在这种情况下:

  1. 遥控器 - 相当于命令发起者或调用者(Invoker),由它触发命令请求。
  2. 按钮 - 每个按钮都可以被视为一个命令对象(Concrete Command),它封装了对电视(receiver)的操作(比如开机、调音量)。
  3. 电视 - 作为命令接收者(Receiver),执行与命令对象相关联的操作(例如打开)。
  4. 命令接口(Command)- 提供执行操作的接口,具体命令(如开机命令、调节音量命令)实现这个接口,并且在内部指定了接收者和操作。

在实际开发中,令模式可以解决以下几个问题:

  1. 请求发送者与接收者解耦:命令模式将请求的发送者与实际执行请求的对象解耦,从而提高灵活性。
  2. 支持撤销和重做:命令可以存储一个操作的历史记录,从而支持操作的撤销和重做。
  3. 支持日志记录:通过将操作记录下来,可以实现系统的日志记录功能。
  4. 支持队列请求:命令对象可以保存在队列中,从而支持请求的排队处理。

基本概念

命令模式的核心在于将请求封装成一个对象,包含以下主要角色:

  1. 命令接口(Command):定义执行请求的方法。
  2. 具体命令(ConcreteCommand):实现命令接口,执行具体的操作。
  3. 接收者(Receiver):真正执行处理请求的类。
  4. 调用者(Invoker):触发命令执行的类。
  5. 客户(Client):创建命令并设置调用者和接收者。

实现示例

假设我们有一个智能家居系统,可以通过命令模式控制家里的电灯和风扇。

定义命令接口

// 命令接口,定义执行请求的方法
interface Command {
  execute(): void;
  undo(): void;
}

定义具体命令

// 电灯命令
class LightOnCommand implements Command {
  private light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  execute(): void {
    this.light.on();
  }

  undo(): void {
    this.light.off();
  }
}

class LightOffCommand implements Command {
  private light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  execute(): void {
    this.light.off();
  }

  undo(): void {
    this.light.on();
  }
}

// 风扇命令
class FanOnCommand implements Command {
  private fan: Fan;

  constructor(fan: Fan) {
    this.fan = fan;
  }

  execute(): void {
    this.fan.on();
  }

  undo(): void {
    this.fan.off();
  }
}

class FanOffCommand implements Command {
  private fan: Fan;

  constructor(fan: Fan) {
    this.fan = fan;
  }

  execute(): void {
    this.fan.off();
  }

  undo(): void {
    this.fan.on();
  }
}

定义接收者

// 电灯类
class Light {
  on(): void {
    console.log('The light is on');
  }

  off(): void {
    console.log('The light is off');
  }
}

// 风扇类
class Fan {
  on(): void {
    console.log('The fan is on');
  }

  off(): void {
    console.log('The fan is off');
  }
}

定义调用者

// 调用者类
class RemoteControl {
  private onCommands: Command[] = [];
  private offCommands: Command[] = [];
  private undoCommand: Command;

  setCommand(slot: number, onCommand: Command, offCommand: Command): void {
    this.onCommands[slot] = onCommand;
    this.offCommands[slot] = offCommand;
  }

  onButtonPressed(slot: number): void {
    this.onCommands[slot].execute();
    this.undoCommand = this.onCommands[slot];
  }

  offButtonPressed(slot: number): void {
    this.offCommands[slot].execute();
    this.undoCommand = this.offCommands[slot];
  }

  undoButtonPressed(): void {
    this.undoCommand.undo();
  }
}

使用命令模式控制智能家居设备

const light = new Light();
const fan = new Fan();

const lightOnCommand = new LightOnCommand(light);
const lightOffCommand = new LightOffCommand(light);
const fanOnCommand = new FanOnCommand(fan);
const fanOffCommand = new FanOffCommand(fan);

const remoteControl = new RemoteControl();

remoteControl.setCommand(0, lightOnCommand, lightOffCommand);
remoteControl.setCommand(1, fanOnCommand, fanOffCommand);

// 控制电灯
remoteControl.onButtonPressed(0);
remoteControl.offButtonPressed(0);
remoteControl.undoButtonPressed();

// 控制风扇
remoteControl.onButtonPressed(1);
remoteControl.offButtonPressed(1);
remoteControl.undoButtonPressed();
The light is on
The light is off
The light is on
The fan is on
The fan is off
The fan is on

应用场景

撤销/重做操作

在文本编辑器、图形编辑器等内容创作工具,需要支持撤销(Undo)和重做(Redo)功能。命令模式允许将每次编辑作为命令对象进行存储,从而方便实现撤销和重做。

文本编辑器的撤销/重做功能

// 定义命令接口
interface Command {
  execute(): void;
  undo(): void;
}

// 文本编辑器可以添加和删除文本
class TextEditor {
  private text: string = '';

  addText(newText: string): void {
    this.text += newText;
  }

  removeText(length: number): void {
    this.text = this.text.slice(0, -length);
  }

  getText(): string {
    return this.text;
  }
}

// 具体命令类:添加文本命令
class AddTextCommand implements Command {
  private editor: TextEditor;
  private text: string;

  constructor(editor: TextEditor, text: string) {
    this.editor = editor;
    this.text = text;
  }

  execute(): void {
    this.editor.addText(this.text);
  }

  undo(): void {
    this.editor.removeText(this.text.length);
  }
}

// 管理命令的历史记录
class TextEditorHistory {
  private commands: Command[] = [];
  private redoStack: Command[] = [];

  executeCommand(command: Command): void {
    command.execute();
    this.commands.push(command);
    this.redoStack = [];
  }

  undo(): void {
    const command = this.commands.pop();
    if (command) {
      command.undo();
      this.redoStack.push(command);
    }
  }

  redo(): void {
    const command = this.redoStack.pop();
    if (command) {
      command.execute();
      this.commands.push(command);
    }
  }
}

// 使用示例
const editor = new TextEditor();
const history = new TextEditorHistory();

const addHello = new AddTextCommand(editor, 'Hello ');
history.executeCommand(addHello);

const addWorld = new AddTextCommand(editor, 'World!');
history.executeCommand(addWorld);

console.log(editor.getText()); // 输出: Hello World!

history.undo();
console.log(editor.getText()); // 输出: Hello 

history.redo();
console.log(editor.getText()); // 输出: Hello World!

每次编辑作为命令对象存储在历史记录中,通过命令对象的 execute 和 undo 方法,轻松实现撤销和重做功能。

操作的队列执行

在处理一系列异步操作(如API请求)时,命令模式可以将每个操作封装为命令对象,以便按顺序执行和管理。

API请求的队列执行

// 定义命令接口
interface Command {
  execute(): Promise<void>;
}

// 具体命令类:API请求命令
class APIRequestCommand implements Command {
  private url: string;

  constructor(url: string) {
    this.url = url;
  }

  async execute(): Promise<void> {
    const response = await fetch(this.url);
    const data = await response.json();
    console.log(data);
  }
}

// 管理命令的队列
class CommandQueue {
  private queue: Command[] = [];

  addCommand(command: Command): void {
    this.queue.push(command);
  }

  async processQueue(): Promise<void> {
    while (this.queue.length > 0) {
      const command = this.queue.shift();
      if (command) {
        await command.execute();
      }
    }
  }
}

// 使用示例
const queue = new CommandQueue();
queue.addCommand(new APIRequestCommand('https://jsonplaceholder.typicode.com/posts/1'));
queue.addCommand(new APIRequestCommand('https://jsonplaceholder.typicode.com/posts/2'));

queue.processQueue().then(() => console.log('All requests processed.'));

命令对象添加到队列中并按顺序执行,方便管理和扩展。此外,队列中也可以动态添加和移除命令对象。

事件处理系统

复杂的 Web 应用或游戏需要处理大量事件和用户交互。使用命令模式将事件处理逻辑封装成命令,根据不同事件触发不同命令对象,使事件处理结构更加清晰。

游戏事件处理

// 定义命令接口
interface Command {
  execute(): void;
}

// 具体命令类:Jump和Fire命令 
class JumpCommand implements Command {
  execute(): void {
    console.log('Player jumps!');
  }
}

class FireCommand implements Command {
  execute(): void {
    console.log('Player fires!');
  }
}

// 控制游戏操作的调用者
class GameController {
  private commands: Map<string, Command> = new Map();

  setCommand(action: string, command: Command): void {
    this.commands.set(action, command);
  }

  handleAction(action: string): void {
    const command = this.commands.get(action);
    if (command) {
      command.execute();
    }
  }
}

// 使用示例
const controller = new GameController();
controller.setCommand('jump', new JumpCommand());
controller.setCommand('fire', new FireCommand());

// 模拟用户输入
controller.handleAction('jump');
controller.handleAction('fire');

不同事件触发相应命令对象的执行,使得事件处理逻辑清晰、易于扩展。

组件间通信

前端框架(如 React、Vue)中,父子组件或兄弟组件间的通信可以通过命令模式管理,将通信行为封装为命令对象,使组件间的数据流动更加明确。

// 定义命令接口
interface Command {
  execute(): void;
}

// 组件A
class ComponentA {
  updateData(data: string): void {
    console.log(`ComponentA data updated to: ${data}`);
  }
}

// 具体命令类:更新数据命令
class UpdateDataCommand implements Command {
  private component: ComponentA;
  private data: string;

  constructor(component: ComponentA, data: string) {
    this.component = component;
    this.data = data;
  }

  execute(): void {
    this.component.updateData(this.data);
  }
}

// 组件B
class ComponentB {
  private command: Command;

  setUpdateCommand(command: Command): void {
    this.command = command;
  }

  updateData(): void {
    if (this.command) {
      this.command.execute();
    }
  }
}

// 使用示例
const componentA = new ComponentA();
const componentB = new ComponentB();

const updateCommand = new UpdateDataCommand(componentA, 'New Data');
componentB.setUpdateCommand(updateCommand);

// 模拟事件触发
componentB.updateData();

通信行为封装为命令对象,使组件间的通信行为明确、可维护。

批处理操作

多个对象执行相同操作(如批量删除选中的列表项),通过命令模式封装操作命令,对选定对象集合执行命令,简化代码逻辑。

批量删除操作

// 定义命令接口
interface Command {
  execute(): void;
}

// 具体命令类:删除项命令
class DeleteItemCommand implements Command {
  private item: string;

  constructor(item: string) {
    this.item = item;
  }

  execute(): void {
    console.log(`Item deleted: ${this.item}`);
  }
}

// 批处理记录类
class BatchProcessor {
  private commands: Command[] = [];

  addCommand(command: Command): void {
    this.commands.push(command);
  }

  executeCommands(): void {
    this.commands.forEach(command => command.execute());
    this.commands = [];
  }
}

// 使用示例
const processor = new BatchProcessor();

processor.addCommand(new DeleteItemCommand('Item 1'));
processor.addCommand(new DeleteItemCommand('Item 2'));
processor.addCommand(new DeleteItemCommand('Item 3'));

processor.executeCommands();

命令对象用于批处理操作,封装单一操作后,命令对象进行集合执行,简化代码逻辑。

开源库中的应用

Redux

Redux 是一种状态管理库,广泛应用于 React 和其他 JavaScript 应用中。Redux 的 Action 和 Reducer 就采用了命令模式的理念。

Action 本质上是命令,描述了要进行的变化。Reducer 执行这些命令,根据当前状态和 action 返回新的状态。

// Action 类型
interface Action {
  type: string;
  payload?: any;
}

// Action Creators
const addUser = (user: string): Action => ({
  type: 'ADD_USER',
  payload: user
});

// Initial State
const initialState = {
  users: []
};

// Reducer
const userReducer = (state = initialState, action: Action) => {
  switch (action.type) {
    case 'ADD_USER':
      return {
        ...state,
        users: [...state.users, action.payload]
      };
    default:
      return state;
  }
};

Cypress

Cypress 是一个前端测试框架,广泛应用于现代 Web 应用的自动化测试。Cypress 使用命令模式来封装测试步骤,将每个测试操作作为一个命令对象。

// 每个测试步骤就是一个命令
describe('Cypress Test', () => {
  it('should display the correct title', () => {
    // 访问页面
    cy.visit('https://example.com');

    // 输入搜索内容
    cy.get('input[name="q"]').type('Cypress');

    // 提交表单
    cy.get('form').submit();

    // 断言结果
    cy.title().should('include', 'Cypress');
  });
});

MobX-state-tree

MobX-state-tree 是一个基于 MobX 的状态管理库,使用命令模式来管理状态的变化。它将每个操作封装成树节点上的命令,并将记录每个命令的变化,以便支持撤销和重做功能。

import { types, onAction } from "mobx-state-tree";

// 模型定义
const Todo = types.model("Todo", {
  title: types.string,
  done: types.boolean
}).actions(self => ({
  toggle() {
    self.done = !self.done;
  }
}));

const RootStore = types.model("RootStore", {
  todos: types.array(Todo)
});

const store = RootStore.create({
  todos: [{ title: "Learn MST", done: false }]
});

onAction(store, (call) => {
  console.log(`Action ${call.name} was called`);
});

// 执行命令
store.todos[0].toggle(); // Action toggle was called

优缺点

优点

  1. 解耦发送者和接收者:发送请求的对象与执行请求的对象解耦,从而提高系统的灵活性。
  2. 支持撤销和重做:命令模式可以记录命令,支持操作的撤销和重做。
  3. 易于扩展:可以方便地增加新的命令类型,而不会影响其他的类。
  4. 支持日志记录和队列请求:命令模式可以记录日志,从而实现提供回放功能,并支持将请求排队执行。

缺点

  1. 增加复杂性:需要定义多个命令类和调用者类,增加了系统的复杂性。
  2. 命令数量多:如果具体命令种类过多,可能会导致命令类数量的急剧增加。

总结

命令模式是一种非常实用且灵活的模式,通过将请求封装成对象,实现了请求发送者与接收者的解耦,能够支持撤销和重做、记录日志、队列请求等功能,使得系统的灵活性与可维护性大大提高。在各种实际应用中,命令模式非常适合复杂操作的处理、事件系统的管理、组件间的通信以及批处理操作的实现。