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. 命令数量多:如果具体命令种类过多,可能会导致命令类数量的急剧增加。

总结

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