目录
- 命令模式概述
- 命令模式的结构与实现
- 命令模式的应用实例
- 实现命令队列
- 记录请求日志
- 实现撤销操作
- 宏命令
- 命令模式的优缺点与适用
命令模式概述
在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
开关与电灯、排气扇示意图:
现实生活
- 相同的开关可以通过不同的电线来控制不同的电器
- 开关<- ->请求发送者
- 电灯<- ->请求的最终接收者和处理者
- 开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者
软件开发
- 按钮<- ->请求发送者
- 事件处理类<- ->请求的最终接收者和处理者
- 发送者与接收者之间引入了新的命令对象(类似电线),将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法
- 相同的按钮可以对应不同的事件处理类
模式动机:
- 将请求发送者和接收者完全解耦
- 发送者与接收者之间没有直接引用关系
- 发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求
命令模式定义:
- 命令模式:将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。
- Command Pattern: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
- 对象行为型模式
注:
- 别名为动作(Action)模式或事务(Transaction)模式
- “用不同的请求对客户进行参数化”
- “对请求排队”
- “记录请求日志”
- “支持可撤销操作”
命令模式结构与实现
命令模式的结构:
命令模式包含以下4个角色:
- Command(抽象命令类)
抽象命令类一般是一个接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接受者的相关操作。
- ConcreteCommand(具体命令类)
具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收对象,绑定接收者对象的动作。在实现execute方法时,将调用接收者对象的相关操作(Action)。
- Invoker(调用者)
调用者即请求的发送者,又称为请求者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类存在关联关系。在程序运行时将调用具体命令对象的execute方法,间接调用接受者的相关操作。
- Receiver(接收者)
接收者执行与请求相关的操作,它具体实现对请求的业务处理。
- Client(客户类)
在客户类中需要创建发送者对象和具体命令类对象,在创建具体命令对象时指定其对应的接收者,发送者和接收者之间无直接关系,通过具体命令对象实现间接调用。
模式分析:
- 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
- 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。
- 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。
- 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。
- 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联。
模式实现:
典型的抽象命令类代码:
public abstract class Command
{
public abstract void execute();
}
典型的具体命令类代码:
public class ConcreteCommand extends Command
{
private Receiver receiver;
public void execute()
{
receiver.action();
}
}
典型的调用者类代码:
public class Invoker
{
private Command command;
public Invoker(Command command)
{
this.command=command;
}
public void setCommand(Command command)
{
this.command=command;
}
//业务方法,用于调用命令类的方法
public void call()
{
command.execute();
}
}
典型的接收者类代码:
public class Receiver
{
public void action()
{
//具体操作
}
}
命令模式时序图:
命令模式的应用实例
实例一:电视机遥控器
电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。
实例一类图:
实例二:功能键设置
为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,如功能键FunctionButton可以用于退出系统(SystemExitClass),也可以用于打开帮助界面(DisplayHelpClass)。用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,相同的功能键可以对应不同的功能。
实例二类图:
结构与分析:
- 如果需要更换具体命令类,无须修改源代码,只需修改配置文件,完全符合开闭原则
- 每一个具体命令类对应一个请求的处理者(接收者),通过向请求发送者注入不同的具体命令对象可以使相同的发送者对应不同的接收者,从而实现“将一个请求封装为一个对象,用不同的请求对客户进行参数化”,客户端只需要将具体命令对象作为参数注入请求发送者,无须直接操作请求的接收者
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="command" value="CommandSample.HelpCommand"/>
</appSettings>
</configuration>
实现命令队列
动机:
- 当一个请求发送者发送一个请求时,有不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理
- 增加一个CommandQueue类,由该类负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者
- 批处理
using System.Collections.Generic;
namespace CommandSample
{
class CommandQueue
{
//定义一个List来存储命令队列
private List<Command> commands = new List<Command>();
public void AddCommand(Command command)
{
commands.Add(command);
}
public void RemoveCommand(Command command)
{
commands.Remove(command);
}
//循环调用每一个命令对象的Execute()方法
public void Execute()
{
foreach (object command in commands)
{
((Command)command).Execute();
}
}
}
}
记录请求日志
动机:
- 将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中
- 为系统提供一种恢复机制
- 可以用于实现批处理
- 防止因为断电或者系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令的重复执行
实现:
- 将发送请求的命令对象通过序列化写到日志文件中
- 命令类必须使用属性[Serializable]标记为可序列化
撤销操作的实现
实例
- 可以通过对命令类进行修改使得系统支持撤销(Undo)操作和恢复(Redo)操作
设计一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。
实现:
- 加法类:Adder(请求接收者)
- 抽象命令类:AbstractCommand
- 加法命令类:AddCommand(具体命令类)
- 计算器界面类:CalculatorForm(请求发送者)
- 客户端测试类:Program
宏命令
动机:
- 宏命令(Macro Command)又称为组合命令(Composite Command),它是组合模式和命令模式联用的产物
- 宏命令是一个具体命令类,它拥有一个集合,在该集合中包含了对其他命令对象的引用
- 当调用宏命令的Execute()方法时,将递归调用它所包含的每个成员命令的Execute()方法。一个宏命令的成员可以是简单命令,还可以继续是宏命令
- 执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理
结构:
命令模式优缺点与适用环境
命令模式的优点
- 降低了系统的耦合度
- 新的命令可以很容易地加入到系统中,符合开闭原则
- 可以比较容易地设计一个命令队列或宏命令(组合命令)
- 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案
命令模式的缺点
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
模式适用环境
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起,即支持宏命令。
模式应用:
- Java语言使用命令模式实现AWT/Swing GUI的委派事件模型 (Delegation Event Model, DEM) 。在AWT/Swing中,Frame、Button等界面组件是请求发送者,而AWT提供的事件监听器接口和事件适配器类是抽象命令接口,用户可以自己写抽象命令接口的子类来实现事件处理,即实现具体命令类,而在具体命令类中可以调用业务处理方法来实现该事件的处理。对于界面组件而言,只需要了解命令接口即可,无须关心接口的实现,组件类并不关心实际操作,而操作由用户来实现。
- 很多系统都提供了宏命令功能,如UNIX平台下的Shell编程,可以将多条命令封装在一个命令对象中,只需要一条简单的命令即可执行一个命令序列,这也是命令模式的应用实例之一。