命令模式(Command)
看了很多命令模式方面的资料,感觉最经典的实例就是餐厅的那个例子,
确实这个餐厅的例子在一定层面上很好的解释了命令模式以及命令模式的真正用途,
当然其也存在一定的问题(下面会提到)
所以下面也还是从这个餐厅来说起。
首先来看一幅图:
上面这副类图反映的是,一个顾客去一个餐馆,而这个餐馆就只有厨师一个人(没有服务员),
干脆不这样看吧,就直接说是一个顾客去一个地摊上吃东西(总所周知,地摊上一般就一个厨师),
这样的话,顾客如果想点单(直接在客户端进行点单)的话,
其会直接和厨师说(因为只有厨师一个人,不和厨师说和谁说去啊),
说要点什么什么食品即客户下订单,等客户点完食品之后,厨师就会去做食品了,
现在看来这样是很正常的,也没有什么问题,不过还请继续往下看。
上面的厨师类 Kitchener 中呢,定义了四个方法,
分别是做蛋糕,做面包,做盒饭,做汉堡,
来考虑一种情况,那就是,如果,有一天,这个地摊生意特别好,来了很多顾客,人山人海(这里夸张点),
每个人都点了一大通食品,而这个厨师就一个人,它要接收订单,又要做食品,估计他也手忙脚乱了,
在 OO 设计原则中,很明显,这又违背了单一职责原则,因为厨师忙不过来,所以,它有可能记错订单,
把顾客 A 要的蛋糕记成了要汉堡,而把顾客 B 要的汉堡写成了盒饭,还有一点就是,由于忙不过来,
有可能厨师把头给忙昏了,蛋糕才熟了一半就拿出了烤箱,盒饭中菜还没熟就出锅了,
这样的话,顾客还不把那厨师骂死去。
上面的这个例子呢,反映的就是在软件系统中,“行为请求者”和“行为执行者”通常呈现一种紧耦合。
(关于判断为紧耦合的条件,在本篇博文的最后面将会有详细的介绍)
在上面的例子中,顾客就是”行为的请求者“,而厨师是“行为执行者”,二者之间是紧耦合。
有时候,这种紧耦合将是致命的问题,上面的例子就反映出来了,最后,顾客等了半天得不到想要的产品,
而厨师呢估计是被顾客骂死了,问题大了吧?
那么如何才能解决上面出现的这个问题呢,由于上面的问题是因为“行为请求者“和”执行请求者“之间紧耦合所致,
所以,我们应该从如何将”行为请求者“和”行为执行者“解耦下手:
而对于客户过多时会造成厨师忙不过来的情况的问题,是可以这样考虑的,
因为厨师集做食品和接订单这两大功能于一体,很明显,针对厨师来说,是违背了单一职责原则的,
所以我需要将这两项功能分离开来,常见的做法,当然就是增加服务员了,
可以由服务员来接订单,然后由服务员将订单传递给厨师,厨师就可以按照这份订单来做食品了。
继续往下谈论的话,顾客下了订单后,
它不必知道为他做食品的是哪一个厨师(顾客尽管下单就 OK 了,反正有人给他做出来的),
而服务员的话,是用来传递订单的,
而厨师接了订单之后,它也不需要知道他在为那个客户做食品,
至于如何做食品,用什么原料做食品那就是厨师自己的事情了。
通过在顾客和厨师之间添加一个服务员(实质上是传递服务员记下的订单,也就是后面将要说的命令),
用来解除顾客和厨师之间的紧耦合。
下面就来看改造后的类图了
在上面这副类图中呢,就是由顾客对服务员说要点什么食品,然后服务员根据顾客点的食品来生成制作食品的命令(也就是订单),
比如,顾客点了蛋糕,那么服务员便会调用 SetCommand 来设置一个命令,即 CakeCommand 命令,
而后,服务员便会调用 NotifyExecute 方法来通知厨师有一个烹饪命令到达,
在这里有一点小问题,可能在现实生活中,服务员它需要关心订单里用户点的食品和这些食品将由哪位厨师来完成,
而这个却正是使用餐馆模式来分析命令模式的一个不足点,因为如果将餐馆模型考虑进命令模式的话,
这个服务员将不再关心由哪位厨师完成,由哪位厨师完成这个是由订单来(也就是命令)决定的,
其厨师会被保存在命令中进行传递,
(这里确实和现实中有点不一致,如果难以理解的话,
请在后面的代码中重点关注一下 Waiter 和 Kitchener 这两个类的代码,
您会发现,在 Waiter 中绝对看不到 Kitchener 的身影)
服务员将彻底的和厨师解耦,因为在命令模式里的话,服务员根本不知道有厨师存在,
它只是将订单放到订单柜台上,然后就不用管了。
而厨师就更不需要知道服务员的存在了,因为厨师只看订单不看人的。
下面就来看上面这个 Demo 的代码实现
先来看厨师类
using System;
namespace Command
{
public class Kitchener
{
//厨师拥有四种技能
public void MakeCake()
{
Console.WriteLine("制作蛋糕");
}
public void MakeBread()
{
Console.WriteLine("制作面包");
}
public void MakeBento()
{
Console.WriteLine("制作快餐");
}
public void MakeHamburger()
{
Console.WriteLine("制作汉堡");
}
}
}
再来看烹饪命令抽象类及其具体类
namespace Command
{
public abstract class CuisineCommand
{
/// <summary>
/// 抽象命令类中必须维护一个厨师类
/// 也就是在这个命令对象中必须维护该命令的接收者
/// </summary>
protected Kitchener kitchener;
public CuisineCommand(Kitchener kitchener)
{
this.kitchener = kitchener;
}
public abstract void MakeFood();
}
}
namespace Command
{
public class CakeCommand:CuisineCommand
{
/// <summary>
/// 调用父类的构造函数
/// </summary>
/// <param name="kitchener"></param>
public CakeCommand(Kitchener kitchener)
: base(kitchener)
{
}
/// <summary>
/// 发出制作蛋糕的命令
/// </summary>
public override void MakeFood()
{
this.kitchener.MakeCake();
}
}
}
namespace Command
{
public class BreadCommand : CuisineCommand
{
public BreadCommand(Kitchener kitchener) :
base(kitchener)
{
}
public override void MakeFood()
{
kitchener.MakeBread();
}
}
}
namespace Command
{
public class BentoCommand:CuisineCommand
{
public BentoCommand(Kitchener kitchener)
: base(kitchener)
{
}
public override void MakeFood()
{
this.kitchener.MakeBento();
}
}
}
namespace Command
{
public class HamburgerCommand:CuisineCommand
{
public HamburgerCommand(Kitchener kitchener)
: base(kitchener)
{
}
public override void MakeFood()
{
this.kitchener.MakeHamburger();
}
}
}
下面还有一个比较重要的类就是服务员
namespace Command
{
public class Waiter
{
//服务员类中必须要实现存储命令
private CuisineCommand cuisineCommand;
public Waiter(CuisineCommand cuisineCommand)
{
this.cuisineCommand = cuisineCommand;
}
//设置命令
public void SetCommand(CuisineCommand cuisineCommand)
{
this.cuisineCommand = cuisineCommand;
}
//通知厨师
public void NotifyExecute()
{
//在通知厨师中,实际上是调用了命令的 MakeFood 方法
//也就是,事实上并非由服务员通知厨师,而是由订单来通知厨师
//订单上写明了厨师
this.cuisineCommand.MakeFood();
}
}
}
下面来看一下客户端代码吧
using System;
namespace CommandTest
{
class Program
{
static void Main(string[] args)
{
//首先要实例化一个厨师
Command.Kitchener kitchener =
new Command.Kitchener();
//然后实例化一个命令对象
//这个命令对象中必须确定谁是接收者
Command.CuisineCommand cake =
new Command.CakeCommand(kitchener);
//实例化一个服务员
//这个服务员来生成订单
Command.Waiter waiter = new Command.Waiter(cake);
//通过服务员来通知厨师制作食品
waiter.NotifyExecute();
Command.CuisineCommand bread =
new Command.BreadCommand(kitchener);
waiter = new Command.Waiter(bread);
waiter.NotifyExecute();
Console.ReadLine();
}
}
}
效果截图为:
其实呢,上面介绍的这个餐厅问题就是一个典型的命令模式的应用了
下面就给出命令模式的结构图
下面就来介绍一下在命令模式中的各个角色:
首先是客户(Client):客户创建一个具体的命令对象并且要确定其接收者。
抽象命令角色(AbstractCommand):声明一个给所有具体命令类的抽象接口。
具体命令角色(ConcreteCommand):定义一个接收者和行为之间的弱耦合,
并且要实现 Execute 方法,来负责调用接收者的相应操作。
请求者角色(Invoker):负责调用命令对象执行请求。
接收者角色(Receiver):负责具体实施和执行一个请求。
而在餐馆模型中,上面提到的角色也是很明确的,
客户就是顾客,抽象命令角色就是抽象厨艺命令类—CuisineCommand ,
而具体命令角色就是具体厨艺命令类—如 CakeCommand 等,
请求者角色则是由服务员扮演,
最后接收者就是由厨师来扮演了。
如果还不能理解的话,
我就再举一个在《Java 与模式》上介绍命令模式时举得例子---玉帝传美猴王上天:
首先是玉帝命令太白金星召唤美猴王上天担任官员,并给了太白金星一道圣旨,
然后,太白金星便去花果山召唤美猴王了。
在这个故事里面呢,玉帝其实就是客户端,美猴王则是命令的接收者,而命令就是圣旨了,
而太白金星就是来传递命令的,或者是发生命令的,也就是请求者 Invoker。
在这个里面呢,玉帝它只管下命令就是了,它根本就不需要关心这个命令是怎么传递到美猴王那里的,
而太白金星则只是将圣旨(也就是命令)传递给美猴王,至于美猴王如何处理,那是美猴王的事,
它要大闹天宫还是当弼马温都是它自己的事情。
上面说了这么多了,也是时候给出命令模式的定义了
命令模式,命令模式把一个请求或者是一个操作封装到一个对象中,
而成使你可以用不同的请求对客户端参数化,
也可以实现对请求排队或者是记录请求日志,同时还可以提供命令的撤销和恢复功能。
命令模式是对命令的封装,命令模式把发出命令的责任和执行命令这两个功能块分离,
将这两个功能分别委托给不同的对象。
每一个命令都是一个操作:请求的一方发出请求其要求执行一个操作,接收的一方收到请求,并执行操作,
命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,
更不必知道请求时怎么被接收,以及操作是否被执行,何时被执行,以及时怎么被执行的。
(这里可以体现在 Invoker 中根本看不到任何 Receiver 的影子)
命令模式的一个用途:队列请求和撤销请求
这里使用还是利用前面的餐馆模型来分析,在前面制作出来的 Demo 其实有不足的地方的,
那就是,你每一次都只能保存一个命令,也就是每一张订单上都只有一道食品,
如果一个顾客要点三道菜的话,那请求者还得同时发三个请求才能实现一名顾客点多道菜。
同时,如果用户点了食品后突然又不想要这份食品了,那么就需要撤销先前点的这份食品(也就是撤销命令),
所以必须改进,这里只需要修改 Waiter (在命令模式中扮演请求者 Invoker 这个角色)类和 Main 函数就 OK 了,
修改成如下:
Waiter 类
using System.Collections.Generic;
namespace Command
{
public class Waiter
{
//服务员类中必须要实现存储命令
private IList<CuisineCommand> cuisineCommand = new List<CuisineCommand>();
//往一份订单里头添加多个命令
public void SetCommand(CuisineCommand cuisineCommand)
{
this.cuisineCommand.Add(cuisineCommand);
}
//从一份订单中撤销一条命令
public void UndoCommand(CuisineCommand cuisineCommand)
{
this.cuisineCommand.Remove(cuisineCommand);
}
//通知厨师
public void NotifyExecute()
{
//在通知厨师中,实际上是调用了命令的 MakeFood 方法
//也就是,事实上并非由服务员通知厨师,而是由订单来通知厨师
//订单上写明了厨师
foreach (CuisineCommand command in cuisineCommand)
{
command.MakeFood();
}
}
}
}
Main 函数
using System;
namespace CommandTest
{
class Program
{
static void Main(string[] args)
{
//首先要实例化一个厨师
Command.Kitchener kitchener =
new Command.Kitchener();
//然后实例化一个命令对象
//这个命令对象中必须确定谁是接收者
Command.CuisineCommand cake =
new Command.CakeCommand(kitchener);
Command.CuisineCommand bread =
new Command.BreadCommand(kitchener);
Command.CuisineCommand bento =
new Command.BentoCommand(kitchener);
//实例化一个服务员
//这个服务员来生成订单
Command.Waiter waiter = new Command.Waiter();
//客户点了蛋糕,面包,盒饭
waiter.SetCommand(cake);
waiter.SetCommand(bread);
waiter.SetCommand(bento);
//客户突然说不点面包了
waiter.UndoCommand(bread);
//通过服务员来通知厨师制作食品
waiter.NotifyExecute();
Console.ReadLine();
}
}
}
下面来看一下效果
从这效果中就可以看出已经实现了一次订单多个命令以及撤销命令的操作了。
再来看一个命令模式的用途:日志请求
有的时候,我们需要将所有的动作都记录在日志中,比如,在 Oracle 数据库中,其恢复功能就做的非常不错,
当开启了日志功能时,
Oracle 会将所有的插入,删除,修改这些动作都记录在日志中,
如果那一次你不小心误删了数据,那么您还是可以从日志中恢复过来的,
而要实现日志请求这项功能的话,您必须在请求者(Invoker)中添加两个方法,即 Store()和 Load(),
然后,当每次执行命令时,便可以使用 Store()将历史记录储存在磁盘中,
当那一次给搞错了的时候,便可以使用 Load()来重新加载存储在磁盘中的历史记录以实现恢复。
下面再来总结一下命令模式的特点:
一,命令模式将发出请求的对象(Invoker)和执行请求的对象(Receiver)解耦。
二,发出请求的对象和执行请求的对象之间是通过命令对象进行沟通的。
三,命令对象封装了一个或者是一组动作。
四,命令模式比较容易实现一个命令队列(上面改进后实现一次订单多个命令就是如此)。
五,允许是否要撤销命令的执行(比如点菜后又不点这个菜了)。
六,比较容易实现对请求的撤销和重做(比如我点菜后又不点了,可想了一下还是点这个菜吧)。
七,由于对命令类进行了抽象,所以增加新的具体命令类非常容易。
八,可以比较方便的实现日志请求。
九,命令模式一个比较明显的缺点就是会导致系统有过多的具体命令类(鬼晓得会有多少种命令啊)。
附录--耦合度(来自《软件工程导论》)
耦合是对一个软件结构内不同模块之间互连程度的度量,耦合强弱取决于模块间接口的复杂程度,
进入或者访问一个模块的点,以及通过接口的数据。
在软件设计中,应该追求尽可能松散耦合的系统。
如果两个模块中的每一个都能独立地工作而不需要另一个模块的存在,
那么它们彼此完全独立,这意味着两个模块间无任何连接,耦合程度最低,
但是,一个系统中不可能所有模块之间都是没有任何连接的。
如果两个模块彼此间通过参数交换信息,而且交换的信息仅仅是数据,那么这种耦合称为数据耦合。
如果传递的信息中有控制信息(尽管有时这种控制信息以数据的形式出现),则这种耦合称为控制耦合。
数据耦合是低耦合,而控制耦合是中等程度的耦合,其增加了系统的复杂程度,
控制耦合往往是多余的,在把模块适当分解之后通常可以用数据耦合来替代。
如果被调用的模块需要使用作为参数传递进来的数据结构中的所有元素,
那么,把整个数据结构作为参数传递就是完全正确的。
但是当把整个数据结构作为参数传递而被调用的模块只需要使用其中一部分数据元素时,就出现了特征耦合,
在这种情况下,被调用的模块可以使用的数据多余它确实需要的数据,
这将导致对数据的访问失去控制, 从而给计算机犯罪提供了机会。
当两个或多个模块通过一个公共数据环境相互作用时,它们之间的耦合称为公共环境耦合,
公共环境耦合可以是全局变量,共享的通信区,内存的公共覆盖区,任何存储介质上的文件,物理设备等,
公共环境耦合的复杂程度随耦合的模块个数而变化,当耦合的模块个数增加时复杂程度显著增加,
如果只有两个模块有公共环境,那么这种耦合有下面两种可能:
(1)一个模块往公共环境送数据,另一个模块从公共环境取数据,这是数据耦合的一种形式,是比较松散的耦合。
(2)两个模块都既往公共环境送数据和取数据,这种耦合比较紧密,介于数据耦合和控制耦合之间。
如果两个模块共享的数据过多,都通过参数传递可能很不方便,这是可以利用公共环境耦合。
最高程度的耦合是内容耦合,如果出现下列情况之一,两个模块间就发生了内容耦合:
(1)一个模块访问另一个模块的内部数据,
(2)一个模块不通过正常入口而转到另一个模块的内部,
(3)两个模块有一部分程序代码重叠,
(4)一个模块有多个入口(这就意味着一个模块有几种功能)
设计原则:
尽量使用数据耦合,少用控制耦合和特征耦合,限制公共环境耦合的范围,完全不用内容耦合。