设计模式的征途—19.命令(Command)模式
在生活中,我们装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关闭,例如电灯或换气扇。在购买开关时,用户并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、换气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制换气扇或者其他电器设备。相同的开关可以通过不同的电线来控制不同的电器,如下图所示。
在软件开发中也存在很多与开关和电器类似的请求发送者和接受者对象,例如一个按钮,它可能是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接受者。为了降低系统的耦合度,将请求的发送者和接收者解耦,可以使用一种被称为命令模式的设计模式来设计系统。
命令模式(Command) | 学习难度:★★★☆☆ | 使用频率:★★★★☆ |
一、自定义功能按键的设计
1.1 需求背景
M公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。M公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”。为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,如下图所示。
通过上图的界面,用户就可以将功能键和相应功能绑定在一起,还可以根据需求来修改功能键的设置,而且系统在未来可能还会增加一些新的功能或功能键。
1.2 初始设计
M公司开发人员打算使用如下code来实现功能键与功能处理类之间的调用关系:
public class FunctionButton
{
private HelpHandler handler; public void OnClick()
{
handler = new HelpHandler();
handler.Display();
}
}
在上述代码中,功能按键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler则充当请求的接收者,在发送者FunctionButton的OnClick()方法中将调用接收者HelpHandler的Display()方法。显然,如果直接使用上述代码,将会有以下几个问题:
(1)请求发送者和请求接收者存在直接调用 => 耦合度太高!更换请求接收者必须修改发送者的源代码!
(2)FunctionButton类在设计和实现时功能已被固定 => 增加新的请求接收者要么修改FunctionButton类要么新增一个新的请求接收者类
(3)用户无法按照自己的需要来设置某个功能键的功能 => 无法在修改源代码情况下更换功能,缺乏灵活性!
二、命令模式概述
2.1 命令模式简介
命令(Command)模式:将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事物(Transaction)模式。
2.2 命令模式结构
命令模式的核心在于引入了命令类,通过命令类来降低请求发送者和接收者的耦合度,请求发送者只需要指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如下图所示。
其中,包含以下几个角色:
(1)Command(抽象命令类):一个抽象类或接口,声明了执行请求的Execute()方法,通过这些方法可以调用请求接收者的相关操作。
(2)ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了抽象命令类中声明的方法。在实现Execute()方法时,将调用接收者对象的相关操作(Action)。
(3)Invoker(调用者):请求发送者,通过命令对象来执行请求。
(4)Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
命令模式的本质在于:对请求进行封装,一个请求对应一个命令,将发出命令的责任和执行命令的责任分割开,使得请求的一方不必了解接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
三、重构自定义功能键的设计
3.1 重构后的设计
其中,FBSettingWindow是“功能键设置”界面类,FunctionButton充当调用者,Command充当抽象命令类,MinimizeCommand、HelpCommand充当具体命令类,WindowHandler和HelpHandler充当请求接收者。
3.2 具体代码实现
(1)功能键设置窗口
/// <summary>
/// 功能键设置窗口类
/// </summary>
public class FBSettingWindow
{
// 窗口标题
public string Title { get; set; }
// 所有功能键集合
private IList<FunctionButton> functionButtonList = new List<FunctionButton>(); public FBSettingWindow(string title)
{
this.Title = title;
} public void AddFunctionButton(FunctionButton fb)
{
functionButtonList.Add(fb);
} public void RemoveFunctionButton(FunctionButton fb)
{
functionButtonList.Remove(fb);
} // 显示窗口及功能键
public void Display()
{
Console.WriteLine("显示窗口:{0}", this.Title);
Console.WriteLine("显示功能键:"); foreach (var fb in functionButtonList)
{
Console.WriteLine(fb.Name);
} Console.WriteLine("------------------------------------------");
}
}
(2)请求发送者:FunctionButton
/// <summary>
/// 请求发送者:功能键
/// </summary>
public class FunctionButton
{
// 功能键名称
public string Name { get; set; }
// 维持一个抽象命令对象的引用
private Command command; public FunctionButton(string name)
{
this.Name = name;
} // 为功能键注入命令
public void SetCommand(Command command)
{
this.command = command;
} // 发送请求的方法
public void OnClick()
{
Console.WriteLine("点击功能键:");
if (command != null)
{
command.Execute();
}
}
}
(3)抽象命令类:Command
/// <summary>
/// 抽象命令类
/// </summary>
public abstract class Command
{
public abstract void Execute();
}
(4)具体命令类:HelpCommand与MinimizeCommand
/// <summary>
/// 具体命令类:帮助命令
/// </summary>
public class HelpCommand : Command
{
private HelpHandler hander; public HelpCommand()
{
hander = new HelpHandler();
} // 命令执行方法,将调用请求接受者的业务方法
public override void Execute()
{
if (hander != null)
{
hander.Display();
}
}
} /// <summary>
/// 具体命令类:最小化命令
/// </summary>
public class MinimizeCommand : Command
{
private WindowHandler handler; public MinimizeCommand()
{
handler = new WindowHandler();
} // 命令执行方法,将调用请求接受者的业务方法
public override void Execute()
{
if (handler != null)
{
handler.Minimize();
}
}
}
(5)请求接收者:WindowHandler和HelpHandler
/// <summary>
/// 请求接受者:帮助文档处理类
/// </summary>
public class WindowHandler
{
public void Minimize()
{
Console.WriteLine("正在最小化窗口至托盘...");
}
} /// <summary>
/// 请求接受者:帮助文档处理类
/// </summary>
public class HelpHandler
{
public void Display()
{
Console.WriteLine("正在显示帮助文档...");
}
}
(6)客户端测试
public class Program
{
public static void Main(string[] args)
{
// Step1.模拟显示功能键设置窗口
FBSettingWindow window = new FBSettingWindow("功能键设置窗口"); // Step2.假如目前要设置两个功能键
FunctionButton buttonA = new FunctionButton("功能键A");
FunctionButton buttonB = new FunctionButton("功能键B"); // Step3.读取配置文件和反射生成具体命令对象
Command commandA = (Command)AppConfigHelper.GetCommandAInstance();
Command commandB = (Command)AppConfigHelper.GetCommandBInstance(); // Step4.将命令注入功能键
buttonA.SetCommand(commandA);
buttonB.SetCommand(commandB); window.AddFunctionButton(buttonA);
window.AddFunctionButton(buttonB);
window.Display(); // Step5.调用功能键的业务方法
buttonA.OnClick();
buttonB.OnClick(); Console.ReadKey();
}
}
这里为了提高系统的灵活性,将具体命令类配置在了配置文件中,并通过帮助类AppConfigHelper来读取配置并反射生成对象。其中配置文件设置如下:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="HelpCommand" value="Manulife.ChengDu.DesignPattern.Command.HelpCommand, Manulife.ChengDu.DesignPattern.Command" />
<add key="MinimizeCommand" value="Manulife.ChengDu.DesignPattern.Command.MinimizeCommand, Manulife.ChengDu.DesignPattern.Command" />
</appSettings>
</configuration>
AppConfigHelper类的实现如下,这里不再详述。
public class AppConfigHelper
{
public static string GetCommandAName()
{
string factoryName = null;
try
{
factoryName = System.Configuration.ConfigurationManager.AppSettings["HelpCommand"];
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return factoryName;
} public static object GetCommandAInstance()
{
string assemblyName = AppConfigHelper.GetCommandAName();
Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type);
return instance;
} public static string GetCommandBName()
{
string factoryName = null;
try
{
factoryName = System.Configuration.ConfigurationManager.AppSettings["MinimizeCommand"];
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return factoryName;
} public static object GetCommandBInstance()
{
string assemblyName = AppConfigHelper.GetCommandBName();
Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type);
return instance;
}
}
编译后运行,输出结果如下图所示:
此时,如果需要修改功能键,例如某个功能键可以实现“自动截屏”,只需要增加一个新的具体命令类,在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入到某个功能键即可,原有代码无需修改,符合开闭原则。
四、命令模式总结
4.1 主要优点
(1)降低了系统的耦合度 => 请求发送者与接受者不存在直接引用
(2)方便地增加新的命令到系统中 => 无须修改源代码,从而符合开闭原则
4.2 主要缺点
使用命令模式可能会导致某些系统有过多的具体命令类。 => 因为针对每一个对请求接收者的调用操作都需要设计一个具体命令,因此在某些系统中可能需要提供大量的具体命令类。
4.3 应用场景
系统需要将请求调用者和请求接收者解耦 => 那就快用命令模式吧骚年!
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》
设计模式的征途—19.命令(Command)模式的更多相关文章
- 设计模式C++描述----19.命令(Command)模式
一. 举例说明 我们知道,在多线程程序中,多个用户都给系统发 Read 和 Write 命令.这里有几点需要说明: 1. 首先明确一点,所有的这些 Read 和 Write 命令都是调用一个库函数. ...
- 命令(Command)模式
命令模式又称为行动(Action)模式或者交易(Transaction)模式. 命令模式把一个请求或者操作封装到一个对象中.命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可 ...
- python 设计模式之命令(Command)模式
#写在前面 也了解了不少设计模式了,他们都有一个通病,那就是喜欢把简单的东西复杂化.比如在不同的类中加个第三者.哈哈哈,简单变复杂是有目的的,那就是降低耦合度,增强可维护性,提高代码复用性,使代码变得 ...
- 十五、命令(Command)模式--行为型模式(Behavioral Pattern)
命令模式又称为行动(Action)模 式或交易(Transaction)模式.命令模式把一个请求或者操作封装到一个对象中. 命令模式是对命令的封装.命令模式把发出命令的责任和执行命令的责任分割开,委派 ...
- 设计模式的征途(C#实现)—文章目录索引
1.预备篇 UML类图10分钟快速入门 2.创建型模式 ① 设计模式的征途-01.单例(Singleton)模式 ② 设计模式的征途-02.简单工厂(Simple Factory)模式 ③ 设计模式的 ...
- junit设计模式--命令者模式
命令模式的意图 将一个请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化: 对请求排队或记录请求日志,以及支持可撤销的操作: 命令模式告诉我们可以为一个操作生成一个对象并给出它的一个执行方法 ...
- java常用设计模式十二:命令模式
一.概述 定义:命令(Command)模式又叫作动作(Action)模式或事务(Transaction)模式,是一种对象的行为模式.将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化:对 ...
- Java设计模式(22)命令模式(Command模式)
Command模式是最让我疑惑的一个模式,我在阅读了很多代码后,才感觉隐约掌握其大概原理,我认为理解设计模式最主要是掌握起原理构造,这样才对自己实际编程有指导作用.Command模式实际上不是个很具体 ...
- C#设计模式之十五命令模式(Command Pattern)【行为型】
一.引言 今天我们开始讲"行为型"设计模式的第二个模式,该模式是[命令模式],又称为行动(Action)模式或交易(Transaction)模式,英文名称是:Command P ...
随机推荐
- 【LeetCode】112. Path Sum
题目: Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up ...
- MacTex XeLaTex xdvipdfmx:fatal: pdf_ref_obj(): passed invalid object. 报错的解决方法
在使用MacTex配合TexStudio编译beamer的时候,爆出如下错误, xdvipdfmx:fatal: pdf_ref_obj(): passed invalid object. 结果尝试其 ...
- 基于Metronic的Bootstrap开发框架经验总结(15)-- 更新使用Metronic 4.75版本
在基于Metronic的Bootstrap开发框架中,一直都希望整合较新.较好的前端技术,结合MVC的后端技术进行项目的开发,随着时间的推移,目前Metronic也更新到了4.75版本,因此着手对这个 ...
- tp5框架的获取器
tp5的获取器功能很强大,一下子就喜欢上了,你可以在模块里任意定义表里不存在的字段,在前台调用很方便.话不多说直接上demo: 1.命名规则 get + 属性名的驼峰命名+ Attr 直接就能在m ...
- Java 常用排序算法实现--快速排序、插入排序、选择、冒泡
public class ArrayOperation { //二分查找算法 public static int branchSearch(int[] array, int searc ...
- Android源码博文集锦2
Android精选源码 android简单易用的Gallery android漂亮的加载效果 这可能是RxJava 2.x 最好的入门教程示例代码 android图片可拖拽排序 android用几行代 ...
- Java文件操作(IO流)
File 在java中用来表示文件和文件夹的具体类. File的构建 File的常见方法 字节流: 对象序列化: 字符流: 转接流: InputStreamReader. ...
- DataReader To List
用了一段时间的Dapper,感觉Dapper比Ado.net好的地方就是转换成实体,到处查资料,写了以下方法,直接可以用. using (var conn = new SqlConnection(&q ...
- 20170722_php_单例模式
<?php class myClass{ private static $obj = null; private function __construc(){ } public static f ...
- RabbitMq学习一入门篇(hello world)
简介 RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python.Ruby..NET.Java,也是众多消息队列中表现不俗的一员,作用就是提高系统的并发 ...