Design Patterns in Web Development - #1 Command Introduction Command Pattern Code examples design-patterns

2025-05-26

Web 开发中的设计模式 - #1 命令

介绍

命令模式

代码示例

设计模式

点击此处查看更新版本

介绍

正如简介中透露的,第一篇文章将讨论命令模式。该模式是“四人帮”中的经典模式之一,属于行为模式

行为模式

顾名思义,行为模式关注的是对象的行为。

佐伊德伯格

与其他类型的模式不同,行为模式不仅是对象和类的模式,也是它们之间通信的模式。它们的主要目的是使用旨在简化复杂控制流的抽象来概述和分配应用程序中组件之间的职责。

最后这句话非常复杂,值得用现实生活中的例子来说明。

假设你在一家餐厅,想吃一块鲜嫩多汁的T骨牛排(我想现在我肯定有事要做)。一个办法就是站起来,走进厨房,让厨师为你准备一块牛排。这时你才意识到,厨房里挤满了人,他们的想法和你一样,最终却把厨房里的工作人员弄得一团糟。只有一件事比这更糟:你的前男友/女友,没错,就是那个喜欢下毒的人,就是厨师。

悲伤的熊猫

事实上,顾客只关心获取食物。直接与厨师沟通并不能达到这个目的,实际上只会带来问题。同时,这种直接沟通在有多个请求时无法扩展,即使有多个监听器来监听这些请求,也无法扩展。这是一个完美的例子,说明了耦合在软件开发中可能带来的问题。

不过好消息是,甚至在软件开发发明之前,人类就已经找到了解决这个令人讨厌的问题的方法:下订单。

为了便于讨论,我们假设厨房门上安装了一个邮箱。无论何时你想吃东西,你只需把所有需要的东西写在一张纸上,然后把订单寄出去即可。

这个简单的技巧神奇地解决了我们的问题。我们不必知道是谁在烹饪我们的食物。例如,我们甚至不知道是否有人在烹饪我们的食物,或者他们是否购买并转售。这意味着灵活性的极大提升(但或许也会让顾客对采用这种方式的餐厅失去一些信任)。此外,这还改善了厨房的整个流程,因为他们可以确定优先级、同时准备、扔进垃圾桶、记录订单或对订单进行任何他们想做的事情。

每个人(包括熊猫)从此过上了幸福的生活

哦,顺便说一下,这是命令模式。

命令模式

显示代码

这是关于什么的?

让我们从独一无二的 GoF 的一句话开始吧。

意图
将请求封装为对象,从而让您使用不同的请求参数化客户端、排队或记录请求,并支持可撤消的操作。

本质上,命令模式就是将例程封装在一个对象中。在上面的例子中,我们将食物请求封装在一个对象中,这个对象就是我们用来下订单的那张纸。这个封装对象就是我们所说的,因此模式2Command得名

效果

应用命令主要有两个作用:降低命令的调用者和执行者之间的耦合度,使例程成为第一类对象。

上面例子中的情况足以让你相信,即使在计算机科学之外,耦合也可能很危险。

过度依赖的女友

如果您没有心情去想那些偏执的熟人,那么您还可以考虑一下,如果您的餐点需要由两个团队(一个专门做牛排,一个专门做配菜)烹制,那么您获取餐点所必须完成的程序基本上是没有变化的。

同时,厨房工作人员并不关心命令是来自服务员、电话、订单还是其他什么。只要他们收到可以执行的命令,就没问题。

这只是我们将例程转换为对象所带来的好处的一部分。最棒的是……等等……它们是对象!这意味着您可以将例程当作对象来操作,例如,您可以存储它们以记录事务历史记录,可以延迟执行,如果管道中出现问题,您可以忽略它们,您可以扩展它们以添加调试检查,等等!

太棒了!我这辈子会用到这个吗?

不。

只是在开玩笑

在某些情况下,Command不仅极其方便,而且几乎是必要的。

回调

每次一个命令的执行者和发布者不仅互相不认识,而且他们也无法提前知道对方。

假设你正在开发一个精美的 UI 工具包。你当然在开发一些需要复用的东西,所以如果你构建了一个Button组件,你希望它能够执行任何操作,而不是硬编码。

“嘿,兄弟!我们有回调函数!”是的,我知道,但世界上并不是每个人都有这么幸运,每天都用 JavaScript 工作(抱歉,有点偏见)。当你想(或必须)严格遵循面向对象时,这就是实现回调的方法。

交易和日志

将所有命令作为第一类对象允许您存储它们,从而创建交易历史记录。

这在需要交易历史记录的系统中非常方便,例如银行系统。此外,你还能获得另一个令人愉悦的附加功能:只需回溯交易历史记录,即可在任何时间点重建系统状态,如果出现问题,这将极大地简化你的工作。

当然,您也可以反过来做:您可以将命令列表作为要执行的任务队列,就像餐厅示例中那样,而不是在执行命令后将其存储为已发生事件的参考。

如果您需要更多的“劳动力”,您只需要为该队列添加更多的消费者,从而使您的应用程序整体上更具可扩展性。

撤消/重做

将操作的执行变成一个对象,可以让你创建一个具有两种方法的对象:executeundo。 第一个方法用于执行某项操作,而后者用于撤消你刚刚执行的操作。

将上述有关交易的信息加起来,您就可以轻松构建和撤消/重做历史记录。

编码前的最后一次努力......

在深入代码示例之前,我们需要先了解一些术语,以便我们能够互相理解。我将使用与 GoF 完全相同的语言,这样如果你想从 GoF 开始理解会更容易。

此模式的参与者是:

  • 接收者
    • 知道如何执行命令;
  • 命令
    • 声明执行操作的接口;
  • 具体命令
    • 定义接收器和要执行的动作之间的绑定;
    • 调用接收器上的方法来完成请求;
  • 客户
    • 创建具体命令并设置其接收器;
  • 祈求者
    • 发出执行命令的请求;

在餐厅示例中,我们将有:

  • Cook作为接收者
  • Order作为具体命令
  • Restaurant作为客户
  • Customer作为祈求者

一些伪代码看起来更严肃一些:

interface Command {
    function execute()
}

// Concrete Command
class Order implements Command {
    Cook cook;
    Meal meal;

    execute() {
        cook.prepare(meal);
    }
}

// Receiver
interface Cook {
    function prepare(Meal meal)
}

// Invoker
class Customer {
    Order order;
    Meal meal;

    mailOrder(Order order) {
        order.execute()
    }
}

// Client
class Restaurant {
    Cook cook;
    Customer customer;

    main() {
        order = new Order(cook, customer.meal)
        customer.mailOrder(order)
    }
}

Enter fullscreen mode Exit fullscreen mode

代码示例

您可以在此处找到这些示例的更详细版本

GitHub 徽标 shikaan /设计模式

设计模式在实际代码中的使用示例







前端:UI 工具包

接上文第一个例子,这是一个在前端使用命令模式的简单示例。我选择不使用任何框架,因为这个理念足够通用,甚至可以应用于原生 JavaScript。

在这个例子中,我们将创建并渲染一个Button组件(调用器),它将执行一个OpenAlertCommand(具体命令)。窗口(接收者)实际上负责执行这个任务,而应用程序(客户端)则负责包装所有事情。

// Concrete Command
class OpenAlertCommand {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.alert('Gotcha!')
}
}
// Receiver
class Window {
alert(message) {
window.alert(message)
}
}
// Invoker
class Button {
constructor(label, command) {
this.label = label
this.command = command
this.node = document.createElement('button')
this.build()
}
build() {
this.node.innerText = this.label
this.node.onclick = () => this.onClickHandler()
}
onClickHandler() {
this.command.execute()
}
}
// Client
export class Application {
constructor(node) {
this.node = node
}
init() {
const receiver = new Window()
const command = new OpenAlertCommand(receiver)
const button = new Button('Submit', command)
this.node.appendChild(button.node)
}
}
view raw app.js hosted with ❤ by GitHub
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>UI Kit</title>
</head>
<body>
<main id="app"></main>
<script async src="./index.js"></script>
</body>
</html>
view raw index.html hosted with ❤ by GitHub
import {Application} from './app'
const appNode = document.getElementById('app')
const application = new Application(appNode)
application.init()
view raw index.js hosted with ❤ by GitHub

你可能会争辩说,不用这个模式做同样的事情,只需要不到 10 行代码。你确实说得对,但是,由于我们之前讨论过的原因,这种模式的扩展性更好,并且在有新的需求时会更灵活。

repo中,我们实际上证明了它的灵活性,并在此示例中添加了其他一些内容:我们使用两个不同的接收器重复使用带有相同命令的相同按钮,我们使用相同的按钮同时触发两个不同的命令。

后端:Python 中的 CQRS

这里有一篇关于此事的很好的介绍文章

以下示例包含一个用 Python 编写的超级简单的CQRS 应用程序。它是一款银行应用,用户只能在其中进行存款操作并获取所有存款的列表。所有内容都存储在内存中,并在进程结束后立即消失。

尽管该应用程序的架构非常简单,但它包含了将其称为 CQRS 应用程序所需的一切。

图表

系紧安全带,因为这里我们有两个并发的命令模式实现:一个用于写入(命令),一个用于读取(查询)。不过两者都共享同一个客户端。

1) 应用程序(客户端)创建Deposit命令并调用handle_deposit命令处理程序(命令调用器)上的方法
2) WriteStore(命令接收器)保存数据
3) 命令处理程序立即触发事件以通知 ReadStore(查询接收器)进行更新
4) 然后应用程序(客户端)创建GetLastDeposit查询并调用handleQueryHandler(查询调用器)上的方法
5) 然后 ReadStore(查询接收器)将值保存到查询中
6) 查询中存储的结果返回给用户

当然,代码可以在repo中找到。Python 不是我的主要语言,所以如果你发现有什么问题,请随时提交 pull request 或在那里开一个 issue。

最后的话

嗯,这真是太长了。希望你至少读了我写的一半 :D 和往常一样,如果你有任何关于如何改进这个系列的反馈,请告诉我。

直到下次!


1.这种模式实际上改变了顾客和厨师的行为(通常意义上的英语行为)。希望这足以让你永远记住什么是行为模式。

2.各位语言爱好者可能想知道,意大利语餐厅里的“order”其实是“comanda”。只用一个词就能记住句型和例句。太棒了。

文章来源:https://dev.to/shikaan/design-patterns-in-web-development---1-command-2jf
PREV
Web开发中的设计模式简介 让我们开始吧!结语
NEXT
每个开发人员都喜欢的十大 JavaScript 模式