总目录
前言
命令模式在日常中,也是比较常见的,就比如:妈妈和爸爸说,你去让孩子把地扫一下;这就是是一个命令,命令中的 下达命令的是妈妈,传达命令的是爸爸,接受命令做事的是孩子;那为什么还要爸爸传达呢?直接去让孩子做事不是更直接,这个好回答,就是因为妈妈省时省力啊!
1 基础介绍
- 定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户(客户程序,也是行为的请求者)进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式属于对象的行为型模式。命令模式是把一个操作或者行为抽象为一个对象,通过对命令的抽象化来使得发出命令的责任和执行命令的责任分隔开。命令模式的实现可以提供命令的撤销和恢复功能。
- 命令模式,顾名思义就是将命令抽象化,然后将请求者和接收者通过命令进行绑定。而命令的请求者只管下达命令,命令的接收者只管执行命令。从而实现了解耦,请求者和接受者二者相对独立。
- 命令模式通过加入命令请求者角色来实现 将发出命令的责任和执行命令的责任分割开,并协助发出命令者来传达命令,使得执行命令的接收者可以收到命令并执行命令。
- 命令模式的目的是解除命令发出者和接收者之间的紧密耦合关系,使二者相对独立,有利于程序的并行开发和代码的维护。
- 命令模式中的角色:
-
客户角色(Client):创建具体的命令对象,并且设置命令对象的接收者。注意这个不是我们常规意义上的客户端,而是在组装命令对象和接收者,或许,把这个Client称为装配者会更好理解,因为真正使用命令的客户端是从Invoker来触发执行。(主要是负责将执行者和命令类进行绑定,其次将命令类和调用者进行绑定。使其调用者可以通过命令类给执行者传递消息进行执行。)
-
命令角色(Command):声明了一个给所有具体命令类实现的抽象接口。
-
具体命令角色(ConcreteCommand):命令接口实现对象,是"虚"的实现;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作。
-
请求者角色(Invoker):要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用命令对象的入口。
-
接受者角色(Receiver):接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
-
2 使用场景
(1)、系统需要支持命令的撤销(undo)。命令对象可以把状态存储起来,等到客户端需要撤销命令所产生的效果时,可以调用undo方法把命令所产生的效果撤销掉。命令对象还可以提供redo方法,以供客户端在需要时,再重新实现命令效果。
(2)、系统需要在不同的时间指定请求、将请求排队。一个命令对象和原先的请求发出者可以有不同的生命周期。意思为:原来请求的发出者可能已经不存在了,而命令对象本身可能仍是活动的。这时命令的接受者可以在本地,也可以在网络的另一个地址。命令对象可以串行地传送到接受者上去。
(3)、如果一个系统要将系统中所有的数据消息更新到日志里,以便在系统崩溃时,可以根据日志里读回所有数据的更新命令,重新调用方法来一条一条地执行这些命令,从而恢复系统在崩溃前所做的数据更新。
(4)、系统需要使用命令模式作为"CallBack(回调)"在面向对象系统中的替代。Callback即是先将一个方法注册上,然后再以后调用该方法。
3 实现方式
下面我们以一个案例,循序渐进的来了解命令模式。
案例:有了小家庭里,有妈妈和爸爸,还有两个孩子,一个叫童童,一个叫睦睦
现在 妈妈准备做饭了,家里没有了,让 童童 去买瓶油回来!
1. 娃儿买东西V1.0
需求:妈妈让 童童 买油
csharp
public class TongTong
{
public void BuyOil()
{
Console.WriteLine("童童买好油了!");
}
}
客户端就相当于妈妈的角色
csharp
static void Main(string[] args)
{
TongTong tt = new TongTong();
tt.BuyOil();
Console.ReadKey();
}
需求变了,现在妈妈发现家里没有可乐了,让睦睦 去买可乐
csharp
public class MuMu
{
public void BuyCola()
{
Console.WriteLine("睦睦买好可乐了!");
}
}
现在我们需求变了,我们新增了一个类,并且还需将客户端的代码改一下
csharp
static void Main(string[] args)
{
TongTong tt = new TongTong();
tt.BuyOil();
MuMu mm = new MuMu();
mm.BuyCola();
Console.ReadKey();
}
我们发现只要我们需求发生一点改变,就需要改动代码,并且客户端直接调用实现类,耦合度太高了,这个违背了依赖倒置和开闭原则。
2. 娃儿买东西V2.0
我们发现在这个事件中,妈妈不管叫 童童还是睦睦 去买东西,去买东西的人和买的物品会发生变化。
不变的是买东西 这个命令是 他们都需要遵守的,因此我们将命令封装起来。
csharp
//定义一个命令的抽象类
public abstract class AbstractCommand
{
//规定实现类必须实现的方法
//该方法表示 执行 具体的命令
public abstract void Execute();
}
public class TongTongCommand : AbstractCommand
{
private TongTong _TongTong;
public TongTongCommand(TongTong tong)
{
_TongTong = tong;
}
public override void Execute()
{
_TongTong.BuyOil();
}
}
public class MuMuCommand : AbstractCommand
{
private MuMu _MuMu;
public MuMuCommand(MuMu muMu)
{
_MuMu = muMu;
}
public override void Execute()
{
_MuMu.BuyCola();
}
}
客户端就相当于妈妈的角色,此时我们需要实现让童童买油,让睦睦买可乐的需求,可以这样实现。
csharp
static void Main(string[] args)
{
//童童买水
TongTong tongTong = new TongTong();
AbstractCommand command = new TongTongCommand(tongTong);
command.Execute();
//睦睦买可乐
MuMu muMu = new MuMu();
command = new MuMuCommand(muMu);
command.Execute();
Console.ReadKey();
}
通过将命令定义成抽象类,规定实现类必须实现的执行命令的方法,我们实现了不同命令的解耦。
如果此时妈妈 需要童童 去买水,我们可以在TongTong 类中 新增了BuyWater的方法,然后修改TongTongCommand类即可。
3. 娃儿买东西V3.0
但是还有一个问题,每次要买点啥,都得妈妈亲自过来和童童或睦睦说,太浪费时间了,于是和一旁帮厨的爸爸说,一会儿我缺什么要买的,我就给你说下,至于你叫谁去我不管了,给我买回来就行!
csharp
public class BaBa
{
private AbstractCommand _AbstractCommand;
public BaBa(AbstractCommand command)
{
_AbstractCommand = command;
}
//告知相关的命令
public void Invoke()
{
_AbstractCommand.Execute();
}
}
此时于爸爸而言,不管执行,只管将妈妈的命令传达出去,你们命令中对应的人去做该做的事情就行。
此时与妈妈而言,不管过程,也不管谁去执行,只管告诉爸爸 一个命令(需要谁去做什么)就行。
与孩子而言,只管执行!
客户端调用:
csharp
static void Main(string[] args)
{
var tongTong = new TongTong();
var command = new TongTongCommand(tongTong);
BaBa baBa = new BaBa(command);
baBa.Invoke();
Console.ReadKey();
}
4. 代码汇总
csharp
public class TongTong
{
public void BuyOil()
{
Console.WriteLine("童童买好油了!");
}
}
public class MuMu
{
public void BuyCola()
{
Console.WriteLine("睦睦买好可乐了!");
}
}
//定义一个命令的抽象类
public abstract class AbstractCommand
{
//规定实现类必须实现的方法
//该方法表示 执行 具体的命令
public abstract void Execute();
}
public class TongTongCommand : AbstractCommand
{
private TongTong _TongTong;
public TongTongCommand(TongTong tong)
{
_TongTong = tong;
}
public override void Execute()
{
_TongTong.BuyOil();
}
}
public class MuMuCommand : AbstractCommand
{
private MuMu _MuMu;
public MuMuCommand(MuMu muMu)
{
_MuMu = muMu;
}
public override void Execute()
{
_MuMu.BuyCola();
}
}
public class BaBa
{
private AbstractCommand _AbstractCommand;
public BaBa(AbstractCommand command)
{
_AbstractCommand = command;
}
//告知相关的命令
public void Invoke()
{
_AbstractCommand.Execute();
}
}
客户端调用:
csharp
static void Main(string[] args)
{
var tongTong = new TongTong();
var command = new TongTongCommand(tongTong);
BaBa baBa = new BaBa(command);
baBa.Invoke();
Console.ReadKey();
}
后续有什么命令,那就新增一个命令实现类即可,实现了命令发起者 与 接收者之间的解耦。
4 优缺点分析
-
优点:
- 命令模式使得新的命令很容易被加入到系统里。
- 可以设计一个命令队列来实现对请求的Undo和Redo操作。
- 可以较容易地将命令写入日志。
- 可以把命令对象聚合在一起,合成为合成命令。合成命令式合成模式的应用。
- 降低了系统的耦合性。将调用操作对象和知道如何实现该操作对象的解耦。
-
缺点:
- 使用命令模式可能会导致系统有过多的具体命令类。这会使得命令模式在这样的系统里变得不实际。
结语
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
C#设计模式之十四命令模式(Command Pattern)【行为型】
C#设计模式(15)------命令模式(Command Pattern)
c#中命令模式详解