理解设计模式:使用 StockTrader 和 R2D2(星球大战)示例的命令模式!
经典的设计模式有23种,详见原著《设计模式:可复用面向对象软件的元素》。这些模式为软件开发中经常遇到的特定问题提供了解决方案。
在本文中,我将描述命令模式;以及如何以及何时应用它。
命令模式:基本思想
在面向对象编程中,命令模式是一种行为设计模式,其中使用一个对象来封装执行某个操作或稍后触发某个事件所需的所有信息。这些信息包括方法名称、拥有该方法的对象以及方法参数的值——维基百科
将请求封装为对象,从而允许您使用不同的请求参数化客户端、排队或记录请求,并支持可撤消的操作 - 设计模式:可重用面向对象软件的元素*
在此模式中,抽象的 Command 类被声明为用于执行操作的接口。Command 类定义了一个名为“execute”的方法,每个具体命令都必须实现该方法。此“execute”方法是 Receiver 对象和操作之间的桥梁。Receiver 知道如何执行与请求相关的操作(任何类都可以是 Receiver)。此模式中另一个相关的组件是 Invoker 类,它负责请求必须执行的命令。
此模式的 UML 图如下:
在以下情况下应使用命令模式:
-
您需要一个命令拥有独立于原始请求的生命周期。此外,如果您需要排队,请在不同的时间指定和执行请求。
-
您需要撤消/重做操作。可以存储命令的执行情况,以便撤消其效果。Command 类实现撤消和重做方法非常重要。
-
您需要围绕基于原始操作的高级操作构建一个系统。
命令模式有几个优点,总结如下几点:
-
它将调用操作的类与知道如何执行操作的对象分离
-
它允许您通过提供队列系统来创建命令序列
-
实现扩展以添加新命令很容易,并且无需更改现有代码即可完成。
-
您还可以使用命令模式定义回滚系统,就像在向导示例中一样,我们可以编写回滚方法。
-
严格控制命令的调用方式和时间。
-
由于命令简化了代码,因此代码更易于使用、理解和测试。
现在我将向您展示如何使用 JavaScript/TypeScript 实现此模式。在我们的案例中,我设计了一个名为 Agent 的类,它定义了以下属性:stockTrade;以及一个 placeOrder 操作。该类是客户端/上下文与 StockTrader 之间的桥梁。placeOrder 方法负责决定应该执行什么操作。例如,如果 orderType 是 buy 或 sell,则该方法应该调用 StockTrader 中的操作。下面的 UML 图展示了我刚才描述的场景。
客户端和代理代码如下:
最相关的代码异味是 placeOrder 方法,它与 StockTrade 的操作/命令耦合。有多种技术可以避免这种代码异味。在本例中,命令模式是一个很好的解决方案,因为我们想要记录命令的历史记录。
最后,StockTrade 类如下:
得到的结果如下图所示:
命令模式 — 示例 1:股票市场 — 解决方案
将命令与 Agent 类解耦的想法是为每个命令创建一组类。然而,这些命令共享一个通用接口,这允许我们根据每个具体的命令执行相应的操作。
这就是我们创建 Order 抽象类的原因,该抽象类包含一个名为“execute”的抽象方法。此方法将被 Agent 类(调用者)调用。此外,Agent 类将包含一个命令列表,用于获取命令的历史记录。
这样,代理就委托了它来决定对其接收的对象执行哪个操作。主要的变化是 Agent 类将不再接收原始属性(字符串)作为参数,因为这些属性没有语义值。取而代之的是,Agent 类现在将接收一个命令对象作为参数,该对象提供语义值。
使用命令模式的新UML图如下所示:
与客户端关联的代码如下:
在这种情况下,每个订单都通过依赖注入 (DI) 接收 StockTrade。代理使用 placeOrder 方法调用命令,该方法通过执行方法执行操作。
与代理相关的代码如下:
您可能会注意到,通过使用 order.execute 方法避免了 if-elseif-else 控制结构,该方法将责任委托给每个命令。
与订单和每个订单相关的代码如下:
此命令中未修改 StockTrade 类。因此,修改后程序执行的结果如下图所示:
npm 运行 example1-问题
npm 运行 example1-命令-解决方案1
另一个使用命令模式解决的有趣示例是,当机器人需要执行多个命令时。例如,向著名的机器人 R2D2 发出SaveSecret、Clean和Move
等命令。在以下 UML 图中,您可以看到这种情况:
与客户端相关的代码如下:
在这个例子中,有三个命令(saveSecretCommand、cleanCommand 和 moveCommand)、两个服务(StoreService 和 R2D2Service)和一个代理(R2D2)。
代理使用executeCommand方法调用订单,该方法接收两个参数:1)命令;2)执行前一个命令的参数。
因此,与 R2D2 相关的代码如下:
R2D2 有一个命令列表,可以通过 listCommands 方法列出,并使用命令数据结构进行存储。最后,executeCommand 方法负责调用每个命令的执行方法。
因此,下一步是创建与命令(抽象类)和每个具体命令相关的代码:
最后,每个命令调用负责该操作的服务,在这种情况下,我们使用了两个不同的服务来表明并非所有命令都将责任委托给同一个服务或类。
得到的结果如下图所示:
我创建了一个 npm 脚本,在应用命令模式后运行此处显示的示例。
npm 运行示例2-命令-解决方案-1
命令模式可以避免项目的复杂性,因为您将命令封装在特定的类中,这些类可以随时添加/删除或更改(包括执行时)。
最重要的不是像我展示的那样去实现这个模式,而是能够识别这个特定模式能够解决的问题,以及何时应该或不应该实现该模式。这一点至关重要,因为具体实现会根据你使用的编程语言而有所不同。
这篇文章的 GitHub 分支是https://github.com/Caballerog/blog/tree/master/command-pattern
最初于 2019 年 5 月 23 日发布于https://www.carloscaballero.io。
文章来源:https://dev.to/carlillo/understanding-design-patterns-command-pattern-using-stocktrader-and-r2d2-starwars-examples-3ifl