设计模式的艺术之道–命令模式
声明:本系列为刘伟老师博客内容总结(http://blog.youkuaiyun.com/lovelion),博客中有完整的设计模式的相关博文,以及作者的出版书籍推荐
本系列内容思路分析借鉴了刘伟老师的博文内容,同时改用C#代码进行代码的演示和分析(Java资料过多 C#表示默哀).
本系列全部源码均在文末地址给出。
本系列开始讲解行为型模式,关注如何将现有类或对象组织在一起形成更加强大的结构。
- 行为型模式(Behavioral Pattern)
关注系统中对象之间的交互,研究系统在运行时对象之间的相互通信与协作,进一步明确对象的职责
不仅仅关注类和对象本身,还重点关注它们之间的相互作用和职责划分 - 类行为型模式
使用继承关系在几个类之间分配行为,主要通过多态等方式来分配父类与子类的职责 - 对象行为型模式
使用对象的关联关系来分配行为,主要通过对象关联等方式来分配两个或多个类的职责
11种常见的行为型模式
命令模式–请求发送者与接收者解耦
现实生活中,相同的开关可以通过不同的电线来控制不同的电器。
开关–请求发送者。
电灯–请求的最终接收者和处理者。
开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者。
1.1定义
-命令模式 (Command Pattern):将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。
- 用不同的请求对客户进行参数化。
- 对请求排队,记录请求日志,支持可撤销操作。
1.2情景实例
问题描述
- 自定义功能键
为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,例如功能键FunctionButton可以用于退出系统(由SystemExitClass类来实现),也可以用于显示帮助文档(由DisplayHelpClass类来实现)
初步思路
功能键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler充当请求的接收者,在发送者FunctionButton的onClick()方法中将调用接收者HelpHandler的display()方法。
//FunctionButton:功能键类,请求发送者
class FunctionButton {
private HelpHandler help; //HelpHandler:帮助文档处理类,请求接收者
//在FunctionButton的onClick()方法中调用HelpHandler的display()方法
public void onClick() {
help = new HelpHandler();
help.display(); //显示帮助文档
}
}
现存缺点(未来变化)
(1)请求发送者和请求接收者之间存在方法的直接调用,耦合度很高,更换请求接收者必须修改发送者的源代码,违反开闭原则
(2)FunctionButton类在设计和实现时功能已被固定,如果增加一个新的请求接收者,如果不修改原有的FunctionButton类,则必须增加一个新的与FunctionButton功能类似的类,这将导致系统中类的个数急剧增加。
(3)用户无法按照自己的需要来设置某个功能键的功能,一个功能键类的功能一旦固定,在不修改源代码的情况下无法更换其功能,系统缺乏灵活性。
如何改进
为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能,开发人员使用命令模式来设计“自定义功能键”模块。
命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法。
UML类图
关键实例源代码
namespace CommandSample
{
class FunctionButton
{
private Command command;
public Command Command
{
get { return command; }
set { command = value; }
}
public void Click()
{
Console.WriteLine("单击功能键!");
command.Execute();
}
}
abstract class Command
{
public abstract void Execute();
}
//类似的帮助命令省略
class ExitCommand : Command
{
private SystemExitClass seObj;
public ExitCommand()
{
seObj = new SystemExitClass();
}
public override void Execute()
{
seObj.Exit();
}
}
class SystemExitClass
{
public void Exit()
{
Console.WriteLine("退出系统!");
}
}
class Program
{
static void Main(string[] args)
{
FunctionButton fb = new FunctionButton();
Command command;
//读取配置文件
string commandStr = ConfigurationManager.AppSettings["command"];
//反射生成对象
command = (Command)Assembly.Load("CommandSample").CreateInstance(commandStr);
//设置命令对象
fb.Command = command;
fb.Click();
Console.Read();
}
}
}
1.3模式分析
动机和意图
- 如何将请求发送者和接收者完全解耦?
- 发送者与接收者之间怎么样解除直接引用关系?
- 发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求?
一般结构
- 命令模式包含4个角色:
- Command(抽象命令类):对具体命令的抽象提升,命令中包含公有的执行方法接口。
- ConcreteCommand(具体命令类)::实现父类的抽象执行方法,并且在内部调用接收者对象的某些方法。
- Invoker(调用者):客户端或者是需要调用命令发送的按钮或者UI之类。
- Receiver(接收者):具体的接收者对象,与命令关联,命令类会调用接收者的具体方法。
命令模式的本质是对请求进行封装。
一个请求对应于一个命令,将发出命令的责任和执行命令的责任分开。
命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
命令模式UML类图

改进后的优点
- 降低了系统的耦合度
- 新的命令可以很容易地加入到系统中,符合开闭原则
- 增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可
现存的缺点
使用命令模式可能会导致某些系统有过多的具体命令类
命令队列实现
有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者。
改造上述实例,增加一个命令队列。完整代码见源码下载。
namespace Command_Queue
{
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();
}
}
}
class Invoker
{
private CommandQueue commandQueue; //维持一个CommandQueue对象的引用
//构造注入
public Invoker(CommandQueue commandQueue)
{
this.commandQueue = commandQueue;
}
//设值注入
public void SetCommandQueue(CommandQueue commandQueue)
{
this.commandQueue = commandQueue;
}
//调用CommandQueue类的Execute()方法
public void Call()
{
Console.WriteLine("开始执行命令");
commandQueue.Execute();
}
}
class Program
{
static void Main(string[] args)
{
CommandQueue cmdqueue = new CommandQueue();
Command command1 = new HelpCommand();
Command command2 = new ExitCommand();
cmdqueue.AddCommand(command1);
cmdqueue.AddCommand(command2);
Invoker invoker = new Invoker(cmdqueue);
invoker.Call();
Console.Read();
}
}
}
撤销操作的实现
在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现。(后续讲到)。
通过一个简单案例来理解撤销操作。
菜鸟软件公司欲开发一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。
公司开发人员使用命令模式设计了结构图,其中计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,并且提供了可撤销加法运算的undo()方法。
// 只是举例说明 不具有通用性 撤销逆操作的实现 应该具体分析
class CalculatorForm
{
private AbstractCommand command;
public AbstractCommand Command
{
get { return command; }
set { command = value; }
}
public void Compute(int value)
{
int i = Command.Execute(value);
Console.WriteLine("执行运算,运算结果为:" + i);
}
public void Undo()
{
int i = Command.Undo();
Console.WriteLine("执行撤销,运算结果为:" + i);
}
}
abstract class AbstractCommand
{
public abstract int Execute(int value);
public abstract int Undo();
}
class AddCommand : AbstractCommand
{
private Adder adder = new Adder();
private int value;
public override int Execute(int value)
{
this.value = value;
return adder.Add(value);
}
public override int Undo()
{
return adder.Add(-value);
}
}
class Adder
{
private int num = 0;
public int Add(int value)
{
num += value;
return num;
}
}
public class Program
{
static void Main(string[] args)
{
CalculatorForm form = new CalculatorForm();
AbstractCommand command;
command = new AddCommand();
form.Command = command;
form.Compute(10);
form.Compute(5);
form.Compute(10);
form.Undo();
Console.Read();
}
}
思考:如果连续调用“form.undo()”两次,预测客户端代码的输出结果。
适用场景
(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
(2) 系统需要在不同的时间指定请求、将请求排队和执行请求(命令队列)
(3)系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作(命令队列)
(4)系统需要将一组操作组合在一起形成宏命令(命令队列)
实例源代码
GitHub地址
百度云地址:链接: https://pan.baidu.com/s/1gf1YW2B 密码: aewy
本文介绍了设计模式中的命令模式,旨在通过将请求封装为对象来实现请求发送者与接收者的解耦。内容涵盖了命令模式的定义、情景实例、模式分析,以及如何通过命令模式降低系统耦合度、实现请求的排队和撤销操作。文章还讨论了命令模式在系统设计中的适用场景和优缺点。

1215

被折叠的 条评论
为什么被折叠?



