设计模式
设计模式简介
看懂UML类图和时序图
UML统一建模语言
UML类图及类图之间的关系
类关系记忆技巧
如何正确使用设计模式
优秀设计的特征
面向对象设计原则
创建型设计模式
工厂模式
抽象工厂模式
简单工厂模式
静态工厂模式(Static Factory)
单例模式
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰器模式
外观模式
享元模式
代理模式
过滤器模式
注册模式(Registry)
行为型设计模式
责任链模式
命令模式
解释器模式
中介者模式
备忘录模式
迭代器模式
观察者模式
状态模式
策略模式
模板模式
访问者模式
规格模式(Specification)
J2EE 设计模式
MVC 模式
业务代表模式
组合实体模式
数据访问对象模式(DAO模式)
前端控制器模式
拦截过滤器模式
空对象模式
服务定位器模式
传输对象模式
数据映射模式(Data Mapper)
依赖注入模式(Dependency Injection)
流接口模式(Fluent Interface)
其他模式
对象池模式(Pool)
委托模式
资源库模式(Repository)
实体属性值模式(EAV 模式)
反面模式
归纳设计模式
本文档使用 MrDoc 发布
-
+
首页
命令模式
> 命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。 ![命令设计模式](/media/202203/2022-03-06_1139100.3676722971375267.png) 命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。 ## 问题 假如你正在开发一款新的文字编辑器, 当前的任务是创建一个包含多个按钮的工具栏, 并让每个按钮对应编辑器的不同操作。 你创建了一个非常简洁的 按钮类, 它不仅可用于生成工具栏上的按钮, 还可用于生成各种对话框的通用按钮。 尽管所有按钮看上去都很相似, 但它们可以完成不同的操作 (打开、 保存、 打印和应用等)。 你会在哪里放置这些按钮的点击处理代码呢? 最简单的解决方案是在使用按钮的每个地方都创建大量的子类。 这些子类中包含按钮点击后必须执行的代码。 你很快就意识到这种方式有严重缺陷。 首先, 你创建了大量的子类, 当每次修改基类 按钮时, 你都有可能需要修改所有子类的代码。 简单来说, GUI 代码以一种拙劣的方式依赖于业务逻辑中的不稳定代码。 还有一个部分最难办。 复制/粘贴文字等操作可能会在多个地方被调用。 例如用户可以点击工具栏上小小的 “复制” 按钮, 或者通过上下文菜单复制一些内容, 又或者直接使用键盘上的 Ctrl+C 。 我们的程序最初只有工具栏, 因此可以使用按钮子类来实现各种不同操作。 换句话来说, 复制按钮 CopyButton 子类包含复制文字的代码是可行的。 在实现了上下文菜单、 快捷方式和其他功能后, 你要么需要将操作代码复制进许多个类中, 要么需要让菜单依赖于按钮, 而后者是更糟糕的选择。 ## 解决方案 优秀的软件设计通常会将关注点进行分离, 而这往往会导致软件的分层。 最常见的例子: 一层负责用户图像界面; 另一层负责业务逻辑。 GUI 层负责在屏幕上渲染美观的图形, 捕获所有输入并显示用户和程序工作的结果。 当需要完成一些重要内容时 (比如计算月球轨道或撰写年度报告), GUI 层则会将工作委派给业务逻辑底层。 这在代码中看上去就像这样: 一个 GUI 对象传递一些参数来调用一个业务逻辑对象。 这个过程通常被描述为一个对象发送请求给另一个对象。 命令模式建议 GUI 对象不直接提交这些请求。 你应该将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。 命令对象负责连接不同的 GUI 和业务逻辑对象。 此后, GUI 对象无需了解业务逻辑对象是否获得了请求, 也无需了解其对请求进行处理的方式。 GUI 对象触发命令即可, 命令对象会自行处理所有细节工作。 下一步是让所有命令实现相同的接口。 该接口通常只有一个没有任何参数的执行方法, 让你能在不和具体命令类耦合的情况下使用同一请求发送者执行不同命令。 此外还有额外的好处, 现在你能在运行时切换连接至发送者的命令对象, 以此改变发送者的行为。 你可能会注意到遗漏的一块拼图——请求的参数。 GUI 对象可以给业务层对象提供一些参数。 但执行命令方法没有任何参数, 所以我们如何将请求的详情发送给接收者呢? 答案是: 使用数据对命令进行预先配置, 或者让其能够自行获取数据。 ![GUI 对象将命令委派给命令对象](/media/202203/2022-03-06_1149510.027693315587119294.png) 让我们回到文本编辑器。 应用命令模式后, 我们不再需要任何按钮子类来实现点击行为。 我们只需在 按钮 Button 基类中添加一个成员变量来存储对于命令对象的引用, 并在点击后执行该命令即可。 你需要为每个可能的操作实现一系列命令类, 并且根据按钮所需行为将命令和按钮连接起来。 其他菜单、 快捷方式或整个对话框等 GUI 元素都可以通过相同方式来实现。 当用户与 GUI 元素交互时, 与其连接的命令将会被执行。 现在你很可能已经猜到了, 与相同操作相关的元素将会被连接到相同的命令, 从而避免了重复代码。 最后, 命令成为了减少 GUI 和业务逻辑层之间耦合的中间层。 而这仅仅是命令模式所提供的一小部分好处! ## 目的 为了封装调用和解耦。 我们有一个调用程序和一个接收器。 这种模式使用「命令行」将方法调用委托给接收器并且呈现相同的「执行」方法。 因此,调用程序只知道调用「执行」去处理客户端的命令。接收器会从调用程序中分离出来。 这个模式的另一面是取消方法的 execute (),也就是 undo () 。命令行也可以通过最小量的复制粘贴和依赖组合(不是继承)被聚合,从而组合成更复杂的命令集。 ## 介绍 **意图:** 将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。 **主要解决:** 在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。 **何时使用:** 在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。 **如何解决:** 通过调用者调用接受者执行命令,顺序:调用者 → 命令 → 接受者。 **关键代码:** 定义三个角色: 1. received 真正的命令执行对象 2. Command 3. invoker 使用命令对象的入口 **应用实例:** 1. struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。 2. 文本编辑器:所有事件都是可以被解除、堆放,保存的命令。 3. Symfony2:SF2 命令可以从 CLI 运行,它的建立只需考虑到命令行模式。 4. 大型 CLI 工具使用子程序来分发不同的任务并将它们封装在「模型」中,每个模块都可以通过命令行模式实现(例如:vagrant)。 **优点:** 1. 降低了系统耦合度。 2. 新的命令可以很容易添加到系统中去。 **缺点:** 使用命令模式可能会导致某些系统有过多的具体命令类。 **使用场景:** 认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。 **注意事项:** 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。 命令模式结构示意图: ![](/media/202203/2022-03-06_1154360.641537238450061.png) ## 结构 ![命令设计模式的结构](/media/202203/2022-03-06_1154240.7080989505977037.png) - 发送者 (Sender)——亦称 “触发者 (Invoker)”——类负责对请求进行初始化, 其中必须包含一个成员变量来存储对于命令对象的引用。 发送者触发命令, 而不向接收者直接发送请求。 注意, 发送者并不负责创建命令对象: 它通常会通过构造函数从客户端处获得预先生成的命令。 - 命令 (Command) 接口通常仅声明一个执行命令的方法。 - 具体命令 (Concrete Commands) 会实现各种类型的请求。 具体命令自身并不完成工作, 而是会将调用委派给一个业务逻辑对象。 但为了简化代码, 这些类可以进行合并。 接收对象执行方法所需的参数可以声明为具体命令的成员变量。 你可以将命令对象设为不可变, 仅允许通过构造函数对这些成员变量进行初始化。 - 接收者 (Receiver) 类包含部分业务逻辑。 几乎任何对象都可以作为接收者。 绝大部分命令只处理如何将请求传递到接收者的细节, 接收者自己会完成实际的工作。 - 客户端 (Client) 会创建并配置具体命令对象。 客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。 此后, 生成的命令就可以与一个或多个发送者相关联了。 ### 实例 我们首先创建作为命令的接口 *Order* ,然后创建作为请求的 *Stock* 类。实体命令类 *BuyStock* 和 *SellStock* ,实现了 *Order* 接口,将执行实际的命令处理。创建作为调用对象的类 *Broker* ,它接受订单并能下订单。 *Broker* 对象使用命令模式,基于命令的类型确定哪个对象执行哪个命令。*CommandPatternDemo* 类使用 *Broker* 类来演示命令模式。 ![](https://www.runoob.com/wp-content/uploads/2014/08/20210830-command-1-command-1.svg) ## 伪代码 在本例中, 命令模式会记录已执行操作的历史记录, 以在需要时撤销操作。 ![命令模式示例的结构](/media/202203/2022-03-07_0016330.9260591782423147.png) 有些命令会改变编辑器的状态 (例如剪切和粘贴), 它们可在执行相关操作前对编辑器的状态进行备份。 命令执行后会和当前点备份的编辑器状态一起被放入命令历史 (命令对象栈)。 此后, 如果用户需要进行回滚操作, 程序可从历史记录中取出最近的命令, 读取相应的编辑器状态备份, 然后进行恢复。 客户端代码 (GUI 元素和命令历史等) 没有和具体命令类相耦合, 因为它通过命令接口来使用命令。 这使得你能在无需修改已有代码的情况下在程序中增加新的命令。 ``` // 命令基类会为所有具体命令定义通用接口。 abstract class Command is protected field app: Application protected field editor: Editor protected field backup: text constructor Command(app: Application, editor: Editor) is this.app = app this.editor = editor // 备份编辑器状态。 method saveBackup() is backup = editor.text // 恢复编辑器状态。 method undo() is editor.text = backup // 执行方法被声明为抽象以强制所有具体命令提供自己的实现。该方法必须根 // 据命令是否更改编辑器的状态返回 true 或 false。 abstract method execute() // 这里是具体命令。 class CopyCommand extends Command is // 复制命令不会被保存到历史记录中,因为它没有改变编辑器的状态。 method execute() is app.clipboard = editor.getSelection() return false class CutCommand extends Command is // 剪切命令改变了编辑器的状态,因此它必须被保存到历史记录中。只要方法 // 返回 true,它就会被保存。 method execute() is saveBackup() app.clipboard = editor.getSelection() editor.deleteSelection() return true class PasteCommand extends Command is method execute() is saveBackup() editor.replaceSelection(app.clipboard) return true // 撤销操作也是一个命令。 class UndoCommand extends Command is method execute() is app.undo() return false // 全局命令历史记录就是一个堆桟。 class CommandHistory is private field history: array of Command // 后进... method push(c: Command) is // 将命令压入历史记录数组的末尾。 // ...先出 method pop():Command is // 从历史记录中取出最近的命令。 // 编辑器类包含实际的文本编辑操作。它会担任接收者的角色:最后所有命令都会 // 将执行工作委派给编辑器的方法。 class Editor is field text: string method getSelection() is // 返回选中的文字。 method deleteSelection() is // 删除选中的文字。 method replaceSelection(text) is // 在当前位置插入剪贴板中的内容。 // 应用程序类会设置对象之间的关系。它会担任发送者的角色:当需要完成某些工 // 作时,它会创建并执行一个命令对象。 class Application is field clipboard: string field editors: array of Editors field activeEditor: Editor field history: CommandHistory // 将命令分派给 UI 对象的代码可能会是这样的。 method createUI() is // ... copy = function() { executeCommand( new CopyCommand(this, activeEditor)) } copyButton.setCommand(copy) shortcuts.onKeyPress("Ctrl+C", copy) cut = function() { executeCommand( new CutCommand(this, activeEditor)) } cutButton.setCommand(cut) shortcuts.onKeyPress("Ctrl+X", cut) paste = function() { executeCommand( new PasteCommand(this, activeEditor)) } pasteButton.setCommand(paste) shortcuts.onKeyPress("Ctrl+V", paste) undo = function() { executeCommand( new UndoCommand(this, activeEditor)) } undoButton.setCommand(undo) shortcuts.onKeyPress("Ctrl+Z", undo) // 执行一个命令并检查它是否需要被添加到历史记录中。 method executeCommand(command) is if (command.execute) history.push(command) // 从历史记录中取出最近的命令并运行其 undo(撤销)方法。请注意,你并 // 不知晓该命令所属的类。但是我们不需要知晓,因为命令自己知道如何撤销 // 其动作。 method undo() is command = history.pop() if (command != null) command.undo() ``` ## 应用场景 - 如果你需要通过操作来参数化对象, 可使用命令模式。 命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。 举个例子: 你正在开发一个 GUI 组件 (例如上下文菜单), 你希望用户能够配置菜单项, 并在点击菜单项时触发操作。 - 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。 同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。 - 如果你想要实现操作回滚功能, 可使用命令模式。 尽管有很多方法可以实现撤销和恢复功能, 但命令模式可能是其中最常用的一种。 为了能够回滚操作, 你需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。 这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 你可以使用备忘录模式来在一定程度上解决这个问题。 其次, 备份状态可能会占用大量内存。 因此, 有时你需要借助另一种实现方式: 命令无需恢复原始状态, 而是执行反向操作。 反向操作也有代价: 它可能会很难甚至是无法实现。 ## 实现方式 1. 声明仅有一个执行方法的命令接口。 2. 抽取请求并使之成为实现命令接口的具体命令类。 每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。 所有这些变量的数值都必须通过命令构造函数进行初始化。 3. 找到担任发送者职责的类。 在这些类中添加保存命令的成员变量。 发送者只能通过命令接口与其命令进行交互。 发送者自身通常并不创建命令对象, 而是通过客户端代码获取。 4. 修改发送者使其执行命令, 而非直接将请求发送给接收者。 5. 客户端必须按照以下顺序来初始化对象: - 创建接收者。 - 创建命令, 如有需要可将其关联至接收者。 - 创建发送者并将其与特定命令关联。 ## 优点 - 单一职责原则。 你可以解耦触发和执行操作的类。 - 开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。 - 你可以实现撤销和恢复功能。 - 你可以实现操作的延迟执行。 - 你可以将一组简单命令组合成一个复杂命令。 ## 缺点 - 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。 ## 与其他模式的关系 - 责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式: - 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理 - 命令在发送者和请求者之间建立单向连接。 - 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。 - 观察者允许接收者动态地订阅或取消接收请求。 - 责任链的管理者可使用命令模式实现。 在这种情况下, 你可以对由请求代表的同一个上下文对象执行许多不同的操作。 还有另外一种实现方式, 那就是请求自身就是一个命令对象。 在这种情况下, 你可以对由一系列不同上下文连接而成的链执行相同的操作。 - 你可以同时使用命令和备忘录模式来实现 “撤销”。 在这种情况下, 命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。 - 命令和策略模式看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。 - 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。 - 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。 - 原型模式可用于保存命令的历史记录。 - 你可以将访问者模式视为命令模式的加强版本, 其对象可对不同类的多种对象执行操作。 ## 示例代码 ### TS ``` /** * The Command interface declares a method for executing a command. */ interface Command { execute(): void; } /** * Some commands can implement simple operations on their own. */ class SimpleCommand implements Command { private payload: string; constructor(payload: string) { this.payload = payload; } public execute(): void { console.log(`SimpleCommand: See, I can do simple things like printing (${this.payload})`); } } /** * However, some commands can delegate more complex operations to other objects, * called "receivers." */ class ComplexCommand implements Command { private receiver: Receiver; /** * Context data, required for launching the receiver's methods. */ private a: string; private b: string; /** * Complex commands can accept one or several receiver objects along with * any context data via the constructor. */ constructor(receiver: Receiver, a: string, b: string) { this.receiver = receiver; this.a = a; this.b = b; } /** * Commands can delegate to any methods of a receiver. */ public execute(): void { console.log('ComplexCommand: Complex stuff should be done by a receiver object.'); this.receiver.doSomething(this.a); this.receiver.doSomethingElse(this.b); } } /** * The Receiver classes contain some important business logic. They know how to * perform all kinds of operations, associated with carrying out a request. In * fact, any class may serve as a Receiver. */ class Receiver { public doSomething(a: string): void { console.log(`Receiver: Working on (${a}.)`); } public doSomethingElse(b: string): void { console.log(`Receiver: Also working on (${b}.)`); } } /** * The Invoker is associated with one or several commands. It sends a request to * the command. */ class Invoker { private onStart: Command; private onFinish: Command; /** * Initialize commands. */ public setOnStart(command: Command): void { this.onStart = command; } public setOnFinish(command: Command): void { this.onFinish = command; } /** * The Invoker does not depend on concrete command or receiver classes. The * Invoker passes a request to a receiver indirectly, by executing a * command. */ public doSomethingImportant(): void { console.log('Invoker: Does anybody want something done before I begin?'); if (this.isCommand(this.onStart)) { this.onStart.execute(); } console.log('Invoker: ...doing something really important...'); console.log('Invoker: Does anybody want something done after I finish?'); if (this.isCommand(this.onFinish)) { this.onFinish.execute(); } } private isCommand(object): object is Command { return object.execute !== undefined; } } /** * The client code can parameterize an invoker with any commands. */ const invoker = new Invoker(); invoker.setOnStart(new SimpleCommand('Say Hi!')); const receiver = new Receiver(); invoker.setOnFinish(new ComplexCommand(receiver, 'Send email', 'Save report')); invoker.doSomethingImportant(); ``` 输出: ``` Invoker: Does anybody want something done before I begin? SimpleCommand: See, I can do simple things like printing (Say Hi!) Invoker: ...doing something really important... Invoker: Does anybody want something done after I finish? ComplexCommand: Complex stuff should be done by a receiver object. Receiver: Working on (Send email.) Receiver: Also working on (Save report.) ```
追风者
2022年3月29日 20:00
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码