• 作者:老汪软件技巧
  • 发表时间:2024-08-21 00:02
  • 浏览量:

导读

命令模式(Command Pattern)是一种行为设计模式,旨在将请求或操作封装成一个对象,从而使你可以用不同的请求、队列、日志和支持可撤销的操作来参数化对象。JavaScript 作为一种灵活的编程语言,非常适合应用命令模式来设计可扩展、可维护的系统。本文将探讨命令模式的核心概念,并展示如何在 JavaScript 中实现这一模式。

核心概念

命令模式的主要思想是将方法调用、请求或操作封装到一个独立的命令对象中。这种封装允许系统在不明确调用方法的情况下对命令进行参数化,并提供以下优势:

解耦调用者与接收者:调用者无需知道如何处理请求,只需将命令发送给接收者即可。支持可撤销操作:命令对象可以存储状态信息,从而支持操作的撤销与重做。扩展性强:增加新命令只需添加新的命令类,符合开放封闭原则。命令模式的结构

命令模式通常由以下几个部分组成:

命令接口(Command Interface) :定义命令的执行方法,例如 execute()。具体命令(Concrete Command) :实现命令接口,执行具体的操作。具体命令通常会持有接收者对象,并在执行时调用接收者的方法。接收者(Receiver) :执行实际操作的类。调用者(Invoker) :负责调用命令对象的类,它只知道如何执行命令,而不关心命令的具体实现。客户端(Client) :创建具体命令对象并设置调用者。应用场景

命令模式适用于以下场景:

需要参数化对象以执行命令:例如,实现操作的延迟执行或在运行时决定执行的命令。支持可撤销操作:例如,编辑器中的撤销/重做功能。支持日志功能:例如,系统可以将命令的执行记录到日志中,以支持系统崩溃后的恢复。支持事务系统:例如,多个操作需要被当作一个操作来执行。命令模式的优缺点优点:缺点:在 JavaScript 中实现命令模式

下面通过一个简单的例子展示如何在 JavaScript 中实现命令模式。假设我们有一个文本编辑器,它可以执行各种操作(如输入文本、删除文本),并且这些操作可以被撤销和重做。

// 1. 命令接口
class Command {
    execute() {}
    undo() {}
}
// 2. 接收者
class TextEditor {
    constructor() {
        this.content = '';
    }
    write(text) {
        this.content += text;
    }
    delete(n) {
        this.content = this.content.slice(0, -n);
    }
    getContent() {
        return this.content;
    }
}
// 3. 具体命令
class WriteCommand extends Command {
    constructor(editor, text) {
        super();
        this.editor = editor;
        this.text = text;
    }
    execute() {
        this.editor.write(this.text);
    }
    undo() {
        this.editor.delete(this.text.length);
    }
}
class DeleteCommand extends Command {
    constructor(editor, n) {
        super();
        this.editor = editor;
        this.n = n;
    }
    execute() {
        this.deletedText = this.editor.getContent().slice(-this.n);
        this.editor.delete(this.n);
    }
    undo() {
        this.editor.write(this.deletedText);
    }
}
// 4. 调用者
class CommandInvoker {
    constructor() {
        this.history = [];
    }
    executeCommand(command) {
        command.execute();
        this.history.push(command);
    }
    undo() {
        const command = this.history.pop();
        if (command) {
            command.undo();
        }
    }
}
// 5. 客户端
const editor = new TextEditor();
const invoker = new CommandInvoker();
const writeCommand = new WriteCommand(editor, 'Hello, World!');
invoker.executeCommand(writeCommand);
console.log(editor.getContent()); // 输出: Hello, World!
invoker.undo();
console.log(editor.getContent()); // 输出: 
const deleteCommand = new DeleteCommand(editor, 6);
invoker.executeCommand(deleteCommand);
console.log(editor.getContent()); // 输出: Hello,
invoker.undo();
console.log(editor.getContent()); // 输出: Hello, World!

代码解读

可以看到,接收者 TextEditor 是实际干活执行命令的。调用者则它只知道如何执行命令,而不关心命令的具体实现,以及谁执行命令。这就是核心概念中说的“解耦调用者与接收者”。

具体的命令实现命令接口,执行具体的操作。准确的说具体命令通常会持有接收者对象,它接受不同的参数,然后将参数传递给调用接收者的具体方法,并在执行时调用接收者的方法。具体的命令算是接受者的一个 API “包装器”。

客户端是实际的应用者,它既创建具体命令对象,又需要设置调用者。它应该算是“中间商”了。

命令模式的应用实践

又要拿我的 outline.js 项目举例了。来看看 outline.js 项目的 API 文档页面:

界面中的 Toolbar 工具栏对象,需要同时调用 Navigator、Reader、Drawer 和 Outline 对象的不同 API 接口。

我没有为每个按钮绑定具体的执行函数:

addListeners() {
  const buttons = this.attr('buttons') || []
  const $el = this.$el
  if (!buttons || buttons.length < 1) {
    return this
  }
  on($el, `.outline-toolbar__button`, 'click', this.onExecute, this, true)
  this.$on('toolbar:update', this.onToolbarUpdate)
  this.$on('toolbar:add:button', this.onAddButton)
  this.$on('toolbar:remove:button', this.onRemoveButton)
  this.$on('toolbar:toggle', this.toggle)
  return this
}

onExecute() 方法执行的是注册的命令:

execute(name) {
  if (this.isDisabled(name)) {
    return this
  }
  this.commands.execute(name)
  return this
}
onExecute(evt) {
  const $button = evt.delegateTarget
  let cmd = ''
  if ($button) {
    cmd = $button.getAttribute('data-cmd')
    if (cmd) {
      this.execute(cmd)
    }
  }
  return this
}

在 outline.js 的项目中,我简单实现了 Commands 和 Command 两个命令模块:

Commands 模块

Commands 模块用来统一管理在 Toolbar 模块中注册的各种具体的命令。

js命令模式_设计模式命令行模式_

import isFunction from './utils/types/isFunction'
class Commands {
  constructor() {
    this.commands = []
  }
  get(name) {
    return this.commands.find((cmd) => cmd.name === name)
  }
  add(command) {
    this.commands.push(command)
    return this
  }
  del(name) {
    const commands = this.commands
    const command = commands.find((cmd) => cmd.name === name)
    const index = command ? commands.indexOf(command) : -1
    if (index > -1) {
      commands.splice(index, 1)
    }
    return this
  }
  clear() {
    this.commands = []
    return this
  }
  execute(name) {
    const command = this.commands.find((cmd) => cmd.name === name)
    if (isFunction(command?.execute)) {
      command.execute()
    }
    return this
  }
}
export default Commands

Command 模块

Command 模块就是最基础的命令模块了,用来生成独立的命令。

import isFunction from './utils/types/isFunction'
class Command {
  constructor(name, action) {
    this.name = name
    if (isFunction(action)) {
      this.action = action
    }
  }
  execute() {
    this.action()
    return this
  }
}
export default Command

注意,我的这个 Command 的实现跟前文的有些不同,它并没有接受接收者对象,而只是接受了将接收者的功能封装过的综合行为功能函数 action。但是实际的作用与前文示例的一致,都是执行时命令时,会调用接受者的具体方法执行任务。

分析 outline.js 应用场景中的命令模式的结构

简单分析一下使用命令模式后,outline.js 项目中各个模块扮演的角色。Toolbar 现在的身份就是 调用者 它只知道如何执行命令,而不关心命令的具体实现,以及谁执行命令。

execute(name) {
  if (this.isDisabled(name)) {
    return this
  }
  this.commands.execute(name)
  return this
}

Navigator、Reader、Drawer 和 Outline 等模块就是 接收者 了,它们负责执行实际的命令。来看看在 Outline 模块初始化 Toolbar 模块时创建的命令:

// 向下按钮
const DOWN = {
  name: 'down',
  icon: 'down',
  size: 20,
  action: {
    context: this,
    // toBottom() 就是具体的命令了
    handler: this.toBottom
  }
}

toBottom() 一次要执行多个操作,对应前文的命令模式应用场景:支持事务系统,多个操作需要被当作一个操作来执行。它会执行控制 Toolbar 模块的按钮向下按钮隐藏,Navigation 模块高亮显示当前的章节导航,并且会控制 Outline 模块的定位逻辑。

toBottom() {
  const afterScroll = this.attr('afterScroll')
  const $scrollElement = this.$scrollElement
  const toolbar = this.toolbar
  const navigator = this.navigator
  const count = this.count()
  const top = Math.floor(
    $scrollElement.scrollHeight - $scrollElement.clientHeight
  )
  const afterDown = () => {
    const $main = navigator.$main
    toolbar.hide('down')
    toolbar.show('up')
    if (count > 0) {
      navigator.highlight(count - 1)
      scrollTo($main, $main.scrollHeight)
      navigator.playing = false
    }
    if (isFunction(afterScroll)) {
      afterScroll.call(toolbar, 'bottom')
    }
  }
  if (count > 0) {
    navigator.playing = true
  }
  this.scrollTo(top, afterDown)
  return this
}

只是这里的 toBottom 命令不是严格的使用 Command 对象创建的实例对象,但这并不能改变它作为实质具体命令的本质。实际上在 Toolbar 模块中,还是会将 toBottom() 方法使用 Command 对象包装成具体命令的。

仔细分析我们可以发现 Toolbar 模块此时还是接收者。而 Outline 模块它也还有个身份,就是 客户端。它既创建具体命令(toBottom),又需要设置调用者(Toolbar)。因为所有的 Toolbar 执行具体命令都是由 Outline 模块创建,并且 Toolbar 模块也是它负责初始化的。

使用命令模式带来的好处

outline.js 的应用场景中没有撤销的操作,但 解耦调用者与接收者 和 扩展性强 的优势就体现的特别明显。

使用命令模式后,以后 outline.js 项目中再扩展任何新的模块,需要与 Toolbar 进行交互,只用借助命令模块生成新模块的命令(或者执行方法)传递给 Toolbar 模块就 Ok 了。

add(button) {
  const $el = this.$el
  const $fragment = document.createDocumentFragment()
  const buttons = this.attr('buttons') || []
  const { name, disabled, context } = button
  const command = this._getCommand(button)
  const _add = (button) => {
    const $button = _createButton(button)
    $fragment.appendChild($button)
    buttons.push(button)
    this.buttons.push({
      $el: $button,
      name,
      disabled: disabled || false,
      context: context || this,
      command
    })
    if (command) {
      this.commands.add(command)
    }
  }
  if (isObject(button)) {
    _add(button)
  } else if (isArray(button)) {
    button.forEach((item) => {
      if (isObject(item)) {
        _add(item)
      }
    })
  }
  $el.appendChild($fragment)
  return this
}

如示例代码中显示的,outline.js 模块自带的添加 Toolbar 模块按钮的功能 add() 方法也只是创建新的具体命令就可以了。

_getCommand(button) {
  const _self = this
  const { action, name } = button
  let command = null
  let handler = null
  let context
  let listener
  if (!action) {
    return command
  }
  handler = action.handler
  context = action.context || this
  if (isFunction(handler)) {
    listener = handler
  } else if (isString(handler)) {
    listener = function () {
      _self.$emit(handler, name)
    }
  }
  if (isFunction(listener)) {
    command = new Command(name, listener.bind(context))
  }
  return command
}

仔细查看 outline.js 项目中的命令,你会发现,之前在 Outline 模块初始化 Toolbar 模块时候传递的(具体命令)方法,现在还是用 Command 模块包装成了标准的具体命令对象了。

使用命令模式带来的问题

显而易见的,我又多整出了 Commands 和 Command 两个新的模块,代码的复杂度是又增加了一些。没有办法,相比较引入带来的好处,这点问题不算什么。

并且 outline.js 这个项目整到现在这个规模,本身也不能算简单了。

总结

命令模式在 JavaScript 中为开发者提供了一种强大且灵活的设计方式,用于封装和执行操作。通过将请求封装为命令对象,系统可以实现解耦、可撤销操作以及更强的扩展性。无论是在复杂的应用程序中,还是在需要支持撤销功能的简单工具中,命令模式都能发挥重要作用。

掌握命令模式,能让你的 JavaScript 项目更加模块化和易于维护。