JavaScript 中的命令设计模式

2025-05-25

JavaScript 中的命令设计模式

在Medium上找到我

在 JavaScript 中,人们喜欢使用的最流行的设计模式之一是命令设计模式,该模式允许开发人员将请求某些内容的对象与想要调用其所需方法的对象分开。

如果这是您第一次听说命令模式,希望通过阅读这篇文章,您可以很好地理解它是什么、它是如何工作的以及为什么我们在某些情况下需要它们。

什么是命令设计模式?

设计模式通常分为三种不同类型的类别,在这种情况下,命令模式属于行为模式。

原因在于它的目的是封装具有双重责任的对象,即决定调用哪些方法以及内部发生什么。

从视觉角度来看,这可能看起来像:

命令模式视觉1

工作原理

因此,本质上,它的职责是将通信拆分为单独的对象,以便它们变得松散耦合,同时仍然保持最终目标。

参与此模式的参与者通常称为:

客户

客户端的责任是创建命令对象并将其传递给调用者

祈求者

调用者从客户端接收命令对象,其唯一职责是调用(或调用)命令

接收者

然后,接收者接收命令并根据接收到的命令寻找要调用的方法。

看起来怎么样

我们刚刚看到了一个或多个对象在应用命令模式之前在代码中的行为图。应用命令模式后,代码如下所示:

从长远来看,一个明显庞大而复杂的对象最终会变得更容易管理,因为一个对象与另一个对象的职责被隔离在各自的私有世界中,而不是混杂在一起。

按照惯例,命令对象通常会定义一个类似 的方法,execute该方法负责调用方法,按照惯例,该方法被称为调用者。而持有这些方法的对象通常被称为接收者

为什么我们需要命令模式

使用命令模式的最大好处在于将执行操作的代码与负责处理操作的代码分离。当你感觉代码在不同的阶段多次处理同一个操作时,不妨尝试一下命令模式。话虽如此,这些命令对象在某些特殊情况下也能带来一些好处,例如能够分别集中处理每个动作/操作。这意味着在我们之前的例子中,我们的对象只需要一个 .eat()命令、一个.jump()命令、再一个.run()命令。

何时使用

可以充分利用命令模式的一些示例情况如下:

  • 撤消/重置
    • 由于每个动作/操作的所有处理都由命令集中完成,因此它们通常适合实现应用程序的撤消/重置。
  • 您需要一个命令,其生命周期与原始请求无关。
  • 此外,如果您想排队,请在不同的时间指定和执行请求。
  • 您需要撤消/重做操作。可以存储命令的执行情况,以便撤消其效果。Command 类实现撤消和重做方法非常重要。
  • 您需要围绕基于原始操作的高级操作构建一个系统。

现实世界的例子

现在让我们假设我们正在启动一个新的青蛙管理应用程序,旨在帮助您记录和管理青蛙随着年龄增长而变化的列表。

在这个应用程序中,我们将有一个Frog类,实例化一些有用的属性和方法来帮助实现这一点:

// Creates and returns a frog api which can help us track activities of each frog
function createFrog(options) {
  const _opts = {
    name: options.name,
    sex: options.sex,
    age: options.age,
  }

  const foodsEaten = []
  const wordsSpoken = []

  return {
    getOption(key) {
      return _opts[key]
    },
    getFoodsConsumed() {
      return foodsEaten
    },
    getWordsSpoken() {
      return wordsSpoken
    },
    eat(food) {
      console.log(`Frog "${_opts.name}" is eating: ${food.name} (${food.type})`)
      foodsEaten.push(food)
    },
    talk(words) {
      console.log(words)
      wordsSpoken.push(...words)
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们可以通过实例化来创建多只青蛙了:

const mikeTheFrog = createFrog({ name: 'mike', sex: 'male', age: 1 })
const sallyTheOtherFrog = createFrog({ name: 'sally', sex: 'female', age: 4 })
const michelleTheLastFrog = createFrog({
  name: 'michelle',
  sex: 'female',
  age: 10,
})
Enter fullscreen mode Exit fullscreen mode

让我们假装让我们的青蛙应用程序活起来:

index.js

const api = {
  fetchFrogs: function() {
    return Promise.resolve([
      { id: 1, name: 'mike', sex: 'male', age: 1 },
      { id: 2, name: 'sally', sex: 'female', age: 2 },
      { id: 3, name: 'michelle', sex: 'female', age: 9 },
    ])
  },
  saveToDb: function(frogs) {
    // Just pretend this is actually saving to a real database
    console.log(`Saving ${frogs.length} frogs to our database...`)
    return Promise.resolve()
  },
}

async function init() {
  try {
    const frogs = await api.fetchFrogs()
    return frogs.map((data) => createFrog(data))
  } catch (error) {
    console.error(error)
    throw error
  }
}

function createFrogsManager() {
  const frogs = []

  return {
    addFrog(frog) {
      frogs.push(frog)
      return this
    },
    getFrogs() {
      return frogs
    },
    getMaleFrogs() {
      return frogs.filter((frog) => {
        return frog.getOption('sex') === 'male'
      })
    },
    getFemaleFrogs() {
      return frogs.filter((frog) => {
        return frog.getOption('sex') === 'female'
      })
    },
    feedFrogs(food) {
      frogs.forEach((frog) => {
        frog.eat(food)
      })
      return this
    },
    save: function() {
      return Promise.resolve(api.saveToDb(frogs))
    },
  }
}

function Food(name, type, calories) {
  this.name = name
  this.type = type
  this.calories = calories
}

const fly = new Food('fly', 'insect', 1.5)
const dragonfly = new Food('dragonfly', 'insect', 4)
const mosquito = new Food('mosquito', 'insect', 1.8)
const apple = new Food('apple', 'fruit', 95)

init()
  .then((frogs) => {
    const frogsManager = createFrogsManager()
    // Add each fetched frog to our managing list so we can manage them
    frogs.forEach((frog) => {
      frogsManager.addFrog(frog)
    })

    const genders = {
      males: frogsManager.getMaleFrogs(),
      females: frogsManager.getFemaleFrogs(),
    }
    // Lets feed the frogs and then save this new data to the database
    frogsManager
      .feedFrogs(fly)
      .feedFrogs(mosquito)
      .save()
    console.log(
      'We reached the end and our database is now updated with new data!',
    )
    console.log(
      `Fed: ${genders.males.length} male frogs and ${genders.females.length} female frogs`,
    )
    frogsManager.getFrogs().forEach((frog) => {
      console.log(
        `Frog ${frog.getOption('name')} consumed: ${frog
          .getFoodsConsumed()
          .map((food) => food.name)
          .join(', ')}`,
      )
    })
  })
  .catch((error) => {
    console.error(error)
  })
Enter fullscreen mode Exit fullscreen mode

结果:

命令设计模式测试产品青蛙代码

我们的应用程序变得非常有价值!

现在请记住,我们没有在代码中应用命令设计模式 - 但是代码运行得非常好,如果我们的青蛙应用程序不会变得更大,我们就可以没事了。

现在让我们仔细看看我们的createFrogsManagerAPI。我们可以看到,它提供了一个 API,可以通过提供便捷的实用程序来跟踪多只青蛙的活动,从而管理随时间变化的青蛙列表。

然而,如果仔细观察,就会发现一些潜在的问题可能会在未来困扰我们。

我们首先看到的是,我们的 API与执行我们想要使用的方法createFrogsManager紧密耦合。最终的代码利用这个接口并直接调用其方法,完全依赖于返回的 API。这个 API 负责调用和处理每个操作。

例如,让我们讨论一下返回给我们使用的这两个方法:

getMaleFrogs() {
  return frogs.filter((frog) => {
    return frog.getOption('sex') === 'male'
  })
},
getFemaleFrogs() {
  return frogs.filter((frog) => {
    return frog.getOption('sex') === 'female'
  })
}
Enter fullscreen mode Exit fullscreen mode

如果将来获取每只青蛙性别的路径稍微改变一下会怎么样?

所以不要这样:

function createFrog(options) {
  const _opts = {
    name: options.name,
    sex: options.sex,
    age: options.age,
  }

  const foodsEaten = []
  const wordsSpoken = []

  return {
    getOption(key) {
      return _opts[key]
    },
    getFoodsConsumed() {
      return foodsEaten
    },
    getWordsSpoken() {
      return wordsSpoken
    },
    eat(food) {
      console.log(`Frog "${_opts.name}" is eating: ${food.name} (${food.type})`)
      foodsEaten.push(food)
    },
    talk(words) {
      console.log(words)
      wordsSpoken.push(...words)
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

它变成了这样:

function createFrog(options) {
  const _opts = {
    name: options.name,
    gender: options.gender,
    age: options.age,
  }

  const foodsEaten = []
  const wordsSpoken = []

  return {
    getOption(key) {
      return _opts[key]
    },
    getFoodsEaten() {
      return foodsEaten
    },
    getWordsSpoken() {
      return wordsSpoken
    },
    eat(food) {
      console.log(`Frog "${_opts.name}" is eating: ${food.name} (${food.type})`)
      foodsEaten.push(food)
    },
    talk(words) {
      console.log(words)
      wordsSpoken.push(...words)
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

几天过去了,一切平静。没有收到任何投诉,所以一切肯定没问题。毕竟,我们的服务器一直全天候运行,用户也一直在使用我们的应用程序。

然后,两周后,一位顾客打电话给我们的客户服务部门,报告说她的青蛙全部死了,并将她的损失归咎于我们的平台,因为她完全信任我们,相信我们的智能算法会帮助她做出正确的决策,妥善管理这些青蛙。

我们的开发人员立即收到通知,并被要求调试情况,看看是否存在可能引发这一可怕事件的代码故障。

经过仔细检查,我们运行了测试代码并意识到我们的代码实际上报告了不正确的信息

JavaScript 数据不匹配中的预应用命令设计模式

啥?!不可能!

一位开发人员指出,问题在于.sex青蛙对象的键被重命名为.gender

const _opts = {
  name: options.name,
  gender: options.gender,
  age: options.age,
}
Enter fullscreen mode Exit fullscreen mode

我们必须去查找并更改使用先前按键引用的代码,以便它再次正常工作:

getMaleFrogs() {
  return frogs.filter((frog) => {
    return frog.getOption('gender') === 'male'
  })
},
getFemaleFrogs() {
  return frogs.filter((frog) => {
    return frog.getOption('gender') === 'female'
  })
    }
Enter fullscreen mode Exit fullscreen mode

哦,如果你还没注意到的话,我们的代码还有另一个问题。getFoodsConsumed里面的方法似乎createFrog也被改成了getFoodsEaten

以前的:

getFoodsConsumed() {
  return foodsEaten
}
Enter fullscreen mode Exit fullscreen mode

当前的:

getFoodsEaten() {
  return foodsEaten
}
Enter fullscreen mode Exit fullscreen mode

另一种情况是,如果createFrogsManagerAPI 的某些方法被重命名,比如.save改为.saveFrogs.getFrogs改为,该怎么办.getAllFrogs?这意味着我们代码中所有使用这些方法的部分都需要手动更新为新名称!

所以,我们在示例中遇到的一个主要问题是,我们必须修复所有受此更改影响的代码!这变成了一场捉迷藏游戏。但其实没必要这样。

那么命令模式如何帮助扭转这种局面呢?

在本文的开头我们提到,命令模式允许开发人员将请求某些内容的对象想要调用所需方法的对象分开。

另外,在本文开头我们提到了将会涉及的三个参与者。他们是客户端调用者接收者

以下是它的表示:

命令设计模式视觉表现结局

让我们重构createFrogsManager使用命令的方法:

function createFrogsManager() {
  const frogs = []

  return {
    execute(command, ...args) {
      return command.execute(frogs, ...args)
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

这正是我们真正需要的,因为我们将让命令来完成工作。

我们将继续创建Command构造函数,我们将使用它为 api 的每个方法创建具体的命令:

function Command(execute) {
  this.execute = execute
}
Enter fullscreen mode Exit fullscreen mode

现在已经解决了,让我们继续执行具体的命令:

function AddFrogCommand(frog) {
  return new Command(function(frogs) {
    frogs.push(frog)
  })
}

function GetFrogsCommand() {
  return new Command(function(frogs) {
    return frogs
  })
}

function FeedFrogsCommand(food) {
  return new Command(function(frogs) {
    frogs.forEach((frog) => {
      frog.eat(food)
    })
  })
}

function SaveCommand() {
  return new Command(function(frogs) {
    api.saveToDb(
      frogs.map((frog) => ({
        name: frog.name,
        gender: frog.gender,
        age: frog.age,
      })),
    )
  })
}
Enter fullscreen mode Exit fullscreen mode

有了这个,我们就可以像这样使用它:

function Food(name, type, calories) {
  this.name = name
  this.type = type
  this.calories = calories
}

const mikeTheFrog = createFrog({
  name: 'mike',
  gender: 'male',
  age: 2,
})

const sallyTheFrog = createFrog({
  name: 'sally',
  gender: 'female',
  age: 1,
})

const frogsManager = createFrogsManager()
frogsManager.execute(new AddFrogCommand(mikeTheFrog))
frogsManager.execute(new FeedFrogsCommand(new Food('apple', 'fruit', 95)))
frogsManager.execute(new FeedFrogsCommand(new Food('fly', 'insect', 1)))
frogsManager.execute(new AddFrogCommand(sallyTheFrog))
frogsManager.execute(new SaveCommand())
const updatedFrogs = frogsManager.execute(new GetFrogsCommand())
Enter fullscreen mode Exit fullscreen mode

结果:

命令设计模式将青蛙保存到数据库

我想提一下,在视觉上,接收器是空白的,因为在 JavaScript 中,所有函数和对象基本上都是命令本身,我们.execute通过直接调用命令来演示这一点:

function createFrogsManager() {
  const frogs = []

  return {
    execute(command, ...args) {
      return command.execute(frogs, ...args)
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

结论

这篇文章到此结束!希望你觉得这篇文章很有价值,并期待未来有更多精彩内容!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/the-command-design-pattern-in-javascript-505g
PREV
JavaScript 中复合模式的威力
NEXT
在 React 中上传文件并保持 UI 完全同步