一、基础概念
**命令模式的本质是【封装请求】**命令模式的关键是把请求封装成为命令对象,然后就可以对这个命令对象进行一系列的处理(如:参数化配置、可撤销操作、宏命令、队列请求、日志请求等)。
**命令模式的定义:**将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式的说明:
1、在命令模式中,会定义一个命令的接口,用来约束所有的命令对象,然后提供具体的命令实现,每个命令实现对象是对客户端某个请求的封装。
2、在命令模式中,命令对象并不知道如何处理命令,会有相应的接收者对象来真正执行命令。
|--------|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 序号 | 命令模式要素 | 说明 |
| 1 | 命令模式的关键 | 命令模式的关键之处就是【把请求封装成为对象】(即:命令对象,并定义了统一的执行操作接口) |
| 2 | 命令模式的组装和调用 | 在命令模式中经常会有一个命令的组装者,用它来维护命令的"虚"实现和真实实现之间的关系:如果是超级智能的命令【即:命令对象自己完全实现好了,不需要接收者】那就是命令模式的退回,不需要接收者,自然也就不需要组装者了)真正的用户就是具体化请求的内容,然后提交请求进行触发就可以了(真正的用户会通过Invoker来触发命令)【在实际开发过程中,Client和Invoker可以融合在一起,由客户在使用命令模式的时候,先进行命令对象和接收者的组装,组装完成后,就可以调用命令执行请求了】 |
| 3 | 命令模式的接收者 | 接收者可以是任意的类,对它没有什么特殊要求,这个对象知道如何真正执行命令的操作,执行时是从Command的实现类里面转调过来。 一个接收者对象可以处理多个命令,接收者和命令之间没有约定的对应关系(接收者提供的方法个数、名称、功能和命令中的可以不一样,只要能够通过调用接受者的方法来实现命令的功能就可以了) |
| 4 | 智能命令 | 在标准的命令模式里面,命令的实现类是没有真正实现命令要求的功能的【真正执行命令的功能是接收者】;如果命令的实现对象比较智能,它自己就能真正地实现命令要求的功能,不再需要调用接收者,这种情况就称为智能命令;也可以有半智能的命令,命令对象知道部分实现,其他的还是需要调用接收者来完成 |
| 5 | 发起请求的对象和真正实现的对象是解耦的 | 请求究竟是谁处理?如何处理?发起请求的对象是不知道的【即:发起请求的对象和真正 实现的对象是解耦的】发起请求的对象只管发出命令,其他的就不管了 |
| 6 | 命令模式的调用过程 | 分为两个阶段: 一阶段是组装命令对象和接收者对象; 二阶段则是触发调用Invoker,来让命令真正执行; |
[认识命令模式]
|----|----------------------------------------------------------------------------------------------------------|
| 序号 | 命令模式的优点说明 |
| 1 | 更松散的耦合 命令模式使得发起命令的对象(客户端)和具体实现命令的对象(接收者)完全解耦【即: 发起命令的对象完全不知道具体实现对象是谁,也不知道如何实现】 |
| 2 | 更动态的控制 命令模式把请求封装起来,可以动态地对它进行参数化、队列化和日志化等操作,使得系统更加灵活 |
| 3 | 很自然的复合命令 命令模式中的命令对象能够很容易地组合为复合命令(如宏命令)从而使得系统操作简单,功能更强大 |
| 4 | 更好的扩展性 由于发起命令的对象和具体的实现完全解耦,因此扩展新命令就很容易,只需要实现新的命令对象,然后在装配的时候,把具体的实现对象设置到命对象中,然后可以使用这个命令对象,已有的实现完全不用变化 |
[命令模式的优点]
何时选用命令模式:
1、需要抽象出需要执行的动作,并参数化这些对象,可选用命令模式;
2、需要再不同的时刻指定、排列和执行请求,可选用命令模式;
3、需要支持取消操作,可选用命令模式;
4、需支持当系统崩溃时,能将系统的操作功能重新执行一遍,可选用命令模式;
5、需要事物的系统中(如数据库事务),可选用命令模式。
二、命令模式示例
2.1、 命令模式之电脑如何开机
背景:对于使用电脑的客户来说,开机确实很简单,只要按下启动按钮等待就可以了【但是,当我们按下启动按钮以后,谁来处理?如何处理?都经历了怎样的过程?才能让电脑真正的启动起来,供我们使用】对于电脑的启动过程我们先有一个简单的了解:
1、按下启动按钮,电源开始向主板和其他设备供电;
2、主板的基本输入输出系统[BIOS]开始加电后自检;
3、主板的BIOS会依次寻找显卡等其他设备的BIOS,并让他们自检或初始化;
4、开始检测CPU、内存、硬盘、GPU、即插即用设备等;
5、BIOS更新ESCD【扩展系统配置数据】(即:ESCD是BIOS和操作系统交换硬件配置数据的一种手段);
6、以上内容完成后,BIOS按照用户的配置进行系统引导,进入操作系统里面,等待操作系统装载并初始化完毕,就会显示我们熟悉的系统登录界面。
总结起来就是:加载电源-->设备自检-->装载系统 ;但是这些详细的工作步骤是谁来完成的?如何完成?【其实真正完成这些工作的是主板;那使用电脑的用户和主板是如何关联的呢?在现实生活中,我们是把开关机按钮的线连接到主板上,这样当用户按下按钮的时候,就相当于给主板发送命令,让主板去完成电脑启动前的一系列工作】从使用电脑的角度来看,开机就是按下按钮,我才不需要管使用什么样的主板,如何接收命令,接收谁的命令,接下来怎么处理等细节【总结起来用户就是只用发出命令,不关心请求的真正接收者是谁,也不关心如何实现】。

2.1.1、定义主板接口
因为主板是真正接收和实现用户命令请求的因此先来定义主板接口,约束命令对象:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 主板接口
/// </summary>
internal interface IMainBoard
{
//具有开机功能
void Open();
}//Interface_end
}
2.1.2、创建具体的主板对象继承主板接口实现具体的功能
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 技嘉主板
/// </summary>
internal class GigaMainBoard : IMainBoard
{
/// <summary>
/// 真正的开机命令实现
/// </summary>
public void Open()
{
Console.WriteLine("【技嘉】主板正在开机,请稍等");
Console.WriteLine("接通电源。。。。。。");
Console.WriteLine("设备检查。。。。。。");
Console.WriteLine("装载系统。。。。。。");
Console.WriteLine("设备正常启动。。。。");
Console.WriteLine("系统已经启动完成,请登录");
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
internal class MsiMainBoard
{
/// <summary>
/// 真正的开机命令实现
/// </summary>
public void Open()
{
Console.WriteLine("【微星】主板正在开机,请稍等");
Console.WriteLine("接通电源。。。。。。");
Console.WriteLine("设备检查。。。。。。");
Console.WriteLine("装载系统。。。。。。");
Console.WriteLine("设备正常启动。。。。");
Console.WriteLine("系统已经启动完成,请登录");
}
}//Class_end
}
2.1.3、定义命令接口
因为对于用户来说,电脑开机我就只用按下开机按钮就可以了【这个动作抽象出来就是用户发出开机的命令,其他的就不用管了】为了扩展多种命令,我们的接口就只定义一个方法就是执行。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 命令接口
/// </summary>
internal interface ICommand
{
//执行命令的操作
void Execute();
}//Interface_end
}
2.1.4、创建具体的命令继承命令接口并实现
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
///开机命令的实现类
///拥有真正开机命令的实现(通过调用接受者的方法来实现命令)
/// </summary>
internal class OpenCommand : ICommand
{
//持有真正实现命令的接受者------主板对象
private IMainBoard mainBoard = null;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="mainBoard">主板对象</param>
public OpenCommand(IMainBoard mainBoard)
{
this.mainBoard = mainBoard;
}
public void Execute()
{
//对于命令对象,根本不知道如何开机,会调用主板对象,让主板对象完成开机功能
this.mainBoard.Open();
}
}//Class_end
}
2.1.5、创建机箱对象用于传递用户下达的命令给主板操作
由于用户根本不知道主板是什么,且不想直接直接通过操作主板才实现电脑的开机,只希望简单的按下按钮就可以启动,所以我们在用户与主板之间创建一个中间对象【机箱】来同时对接用户和主板【由于用户给机箱下达命令是通过按按钮的方式,则需要给机箱定义对应的按钮功能】
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 机箱对象,本身有按钮,持有按钮对应的命令对象
/// </summary>
internal class Box
{
//开机命令对象
private ICommand openCommand = null;
/// <summary>
/// 设置开机命令对象
/// </summary>
/// <param name="command">命令对象</param>
public void SetOpenCommand(ICommand openCommand)
{
this.openCommand = openCommand;
}
/// <summary>
/// 提供给客户使用,接收并响应用户请求,相当于开机按钮被按下的方法
/// </summary>
public void OpenButtonPressed()
{
//按下按钮,执行命令
this.openCommand.Execute();
}
}//Class_end
}
2.1.6、客户使用按钮启动系统
现在我们都具备了实现功能的主板、命令、及其作为用户下达命令与主板关联的机箱;但目前还都是零散的各个配件,无法使用;我们需要将这些配件组成一个整体提供给用户使用【现实生活中国,这个组装的工作是由装机工程师完成的,我们这里为了简单,就直接在客户端那里封装一个方法使用】。
cs
namespace CommandPattern
{
internal class Program
{
static void Main(string[] args)
{
OpenButtonPressedTest();
Console.ReadLine();
}
/// <summary>
/// 测试客户按下机箱的开机按钮启动系统
/// </summary>
private static void OpenButtonPressedTest()
{
Console.WriteLine("------客户按下机箱的开机按钮启动系统------");
/*1-把命令和真正的实现组合起来,相当于组装机器*/
IMainBoard mainBoard = new GigaMainBoard();
OpenCommand openCommand = new OpenCommand(mainBoard);
/*2-为机箱上的开机按钮设置对应的命令,让按钮知道该做什么*/
Box box = new Box();
box.SetOpenCommand(openCommand);
/*3-模拟用户按下机箱上的按钮*/
box.OpenButtonPressed();
}
}//Class_end
}
2.1.7、运行结果

2.2、命令模式的参数化配置
命令模式的参数化配置是指【可以使用不同的命令对象,去参数化配置客户的请求】;
在电脑如何开机的问题中,客户按下一个按钮,到底是开机还是重启,那就需要看参数化配置的是哪一个具体的按钮对象(若参数化的是开机命令对象,那就执行开机功能;若参数化的是充气命令对象,那执行的就是重启功能)。
2.2.1、定义主板接口
新增一个重启按钮的方法:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 主板接口
/// </summary>
internal interface IMainBoard
{
//具有开机功能
void Open();
//2-主板具有重启功能
void Reboot();
}//Interface_end
}
2.2.2、创建具体的主板对象继承主板接口实现具体的功能
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 技嘉主板
/// </summary>
internal class GigaMainBoard : IMainBoard
{
/// <summary>
/// 真正的开机命令实现
/// </summary>
public void Open()
{
Console.WriteLine("【技嘉】主板正在开机,请稍等");
Console.WriteLine("接通电源。。。。。。");
Console.WriteLine("设备检查。。。。。。");
Console.WriteLine("装载系统。。。。。。");
Console.WriteLine("设备正常启动。。。。");
Console.WriteLine("系统已经启动完成,请登录");
}
/// <summary>
/// 真正的重启命令实现
/// </summary>
public void Reboot()
{
Console.WriteLine("技嘉主板现在正在重启机器,请等候...");
Console.WriteLine("机器已经正常启动,请登录。。。");
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
internal class MsiMainBoard
{
/// <summary>
/// 真正的开机命令实现
/// </summary>
public void Open()
{
Console.WriteLine("【微星】主板正在开机,请稍等");
Console.WriteLine("接通电源。。。。。。");
Console.WriteLine("设备检查。。。。。。");
Console.WriteLine("装载系统。。。。。。");
Console.WriteLine("设备正常启动。。。。");
Console.WriteLine("系统已经启动完成,请登录");
}
/// <summary>
/// 真正的重启命令实现
/// </summary>
public void Reboot()
{
Console.WriteLine("微星主板现在正在重启机器,请等候...");
Console.WriteLine("机器已经正常启动,请登录。。。");
}
}//Class_end
}
2.2.3、定义命令接口
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 命令接口
/// </summary>
internal interface ICommand
{
//执行命令的操作
void Execute();
}//Interface_end
}
2.2.4、创建具体的命令继承命令接口并实现
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
///开机命令的实现类
///拥有真正开机命令的实现(通过调用接受者的方法来实现命令)
/// </summary>
internal class OpenCommand : ICommand
{
//持有真正实现命令的接受者------主板对象
private IMainBoard mainBoard = null;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="mainBoard">主板对象</param>
public OpenCommand(IMainBoard mainBoard)
{
this.mainBoard = mainBoard;
}
public void Execute()
{
//对于命令对象,根本不知道如何开机,会调用主板对象,让主板对象完成开机功能
this.mainBoard.Open();
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 重启机器命令,继承ICommand接口
/// 持有重启机器命令的真正实现,通过调用接收者方法来实现命令
/// </summary>
internal class RebootCommand : ICommand
{
//持有真正实现命令的接收者------主板对象
private IMainBoard mainBoard = null;
/// <summary>
/// 构造方法
/// </summary>
/// <param name="mainBoard">主板对象</param>
public RebootCommand(IMainBoard mainBoard)
{
this.mainBoard = mainBoard;
}
public void Execute()
{
//对于命令对象,根本不知道如何重启机器,会调用主板对象让主板去完成重启机器的功能
this.mainBoard.Reboot();
}
}//Class_end
}
2.2.5、创建机箱对象用于传递用户下达的命令给主板操作
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern
{
/// <summary>
/// 机箱对象,本身有按钮,持有按钮对应的命令对象
/// </summary>
internal class Box
{
//开机命令对象
private ICommand openCommand = null;
/// <summary>
/// 设置开机命令对象
/// </summary>
/// <param name="command">命令对象</param>
public void SetOpenCommand(ICommand openCommand)
{
this.openCommand = openCommand;
}
/// <summary>
/// 提供给客户使用,接收并响应用户请求,相当于开机按钮被按下的方法
/// </summary>
public void OpenButtonPressed()
{
//按下按钮,执行命令
this.openCommand.Execute();
}
//重启机器命令对象
private ICommand rebootCommand = null;
//设置重启命令对象
public void SetRebootCommand(ICommand rebootCommand)
{
this.rebootCommand = rebootCommand;
}
/// <summary>
/// 提供给客户使用,接收并响应用户请求,相当于重启按钮被按下的方法
/// </summary>
public void RebootButtonPressed()
{
//按下按钮,执行命令
this.rebootCommand.Execute();
}
}//Class_end
}
2.2.6、客户使用按钮启动系统
cs
using CommandPattern.Macros;
namespace CommandPattern
{
internal class Program
{
static void Main(string[] args)
{
ParameterButtonPressedTest();
Console.ReadLine();
}
/// <summary>
/// 测试客户按下机箱的参数化配置按钮启动系统
/// </summary>
private static void ParameterButtonPressedTest()
{
Console.WriteLine("------测试客户按下机箱的参数化配置按钮启动系统------");
/*1-把命令和真正的实现组合起来,相当于组装机器*/
IMainBoard mainBoard = new GigaMainBoard();
OpenCommand openCommand = new OpenCommand(mainBoard);
RebootCommand rebootCommand = new RebootCommand(mainBoard);
/*2-为机箱上的开机按钮设置对应的命令,让按钮知道该做什么*/
Box box = new Box();
//这里先正确配置开机按钮对应开机命令,重启按钮对应重启命令
box.SetOpenCommand(openCommand);
box.SetRebootCommand(rebootCommand);
/*3-模拟用户按下机箱上的按钮*/
Console.WriteLine("------正确按钮命令配置------");
Console.WriteLine(">>>按下开机按钮>>>");
box.OpenButtonPressed();
Console.WriteLine(">>>按下重启按钮>>>");
box.RebootButtonPressed();
Console.WriteLine("\n");
//这里错误配置参数命令
box.SetOpenCommand(rebootCommand);
box.SetRebootCommand(openCommand);
Console.WriteLine("------错误按钮命令配置------");
Console.WriteLine(">>>按下开机按钮>>>");
box.OpenButtonPressed();
Console.WriteLine(">>>按下重启按钮>>>");
box.RebootButtonPressed();
}
}//Class_end
}
2.2.7、运行结果

2.3、命令模式之可撤销操作
可撤销的含义就是:放弃当前的操作,回到未执行该操作的状态;实现撤销的操作有两种思路:
**思路一:**补偿式操作(也称反操作式)【即:如被撤销的操作是加的功能,那撤销的实现就变为了减的功能;同理若被撤销的是打开功能,那撤销的实现就是关闭功能】;
**思路二:**存储是恢复【即:把操作前的状态记录下来,然后要撤销操作的时候直接恢复回去就可以了】。
比如:我们现在需要实现一个最简单的加减法运算,且需要实现可撤销操作。
2.3.1、定义加减法运算的接口
该接口定义了可以执行加法、减法操作、设置计算初始值、获取计算结果方法:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.UndoOPC
{
/// <summary>
/// 操作运算的接口
/// </summary>
internal interface IOperation
{
//获取计算完成后的结果
int GetResult();
//设置计算开始的初始值
void SetResult(int result);
//执行加法
void Addition(int num);
//执行减法
void Subtraction(int num);
}//Interface_end
}
2.3.2、定义对象继承接口实现具体的加法减法
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.UndoOPC
{
/// <summary>
/// 运算类,真正实现加减法运算
/// </summary>
internal class Opreation : IOperation
{
//记录运算结果
private int result = 0;
public void Addition(int num)
{
//实现加法功能
result += num;
}
public int GetResult()
{
return result;
}
public void SetResult(int result)
{
this.result = result;
}
public void Subtraction(int num)
{
//实现减法功能
result -= num;
}
}//Class_end
}
2.3.3、定义命令接口
正常来说命令接口只需要定义执行方法就可以,但这里还需要撤销功能,则还需要定义撤销方法:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.UndoOPC
{
/// <summary>
/// 命令接口
/// </summary>
internal interface ICommand
{
//执行命令对应的操作
void Execute();
//执行撤销命令
void Undo();
}//Interface_end
}
2.3.4、具体的加法、减法命令实现
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.UndoOPC
{
/// <summary>
/// 具体的加法命令实现对象
/// </summary>
internal class AddCommand : ICommand
{
//持有具体执行计算的对象
private IOperation operation = null;
//需操作的数据
private int num = 0;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="operation">操作对象</param>
/// <param name="num">需操作的数据</param>
public AddCommand(IOperation operation, int num)
{
this.operation = operation;
this.num = num;
}
public void Execute()
{
//转调接收者去真正执行功能,此处的命令是做加法
this.operation.Addition(num);
}
public void Undo()
{
//转调接收者去真正执行功能,命令本身是做加法,那么撤销的时候就是做减法了
this.operation.Subtraction(num);
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.UndoOPC
{
/// <summary>
/// 具体的减法命令实现对象
/// </summary>
internal class SubCommand : ICommand
{
//持有真正执行计算的对象
private IOperation operation = null;
//操作的数据
private int num = 0;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="operation">操作对象</param>
/// <param name="num">需操作的数据</param>
public SubCommand(IOperation operation,int num)
{
this.operation = operation;
this.num = num;
}
public void Execute()
{
//转调接收者去真正执行功能,此处的命令是做减法
this.operation.Subtraction(num);
}
public void Undo()
{
//转调接收者去真正执行功能,命令本身是做减法,那么撤销的时候就是做加法了
this.operation.Addition(num);
}
}//Class_end
}
2.3.5、定义计算器
计算器相当于Invoker,持有多个命令对象,计算器是可以实现可撤销操作的地方(要想实现操作的可撤销操作,就需要把操作过的命令都记录下来,形成命令的历史列表,撤销的时候从最后一个开始执行撤销)。
什么时候向命令历史列表添加值呢?(在每次操作加法、减法按钮按下的时候添加)。
什么时候移除命令列表里的值呢?(在撤销的时候移除命令历史列表的值,同时给恢复历史命令表里增加值);
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.UndoOPC
{
internal class Calculator
{
//持有执行加法的命令对象
private ICommand addCommand = null;
//持有执行减法的命令对象
private ICommand SubCommand = null;
//命令的操作历史记录,在撤销的时候使用
private List<ICommand> undoCommands = new List<ICommand>();
//命令的操作记录,在恢复时使用
private List<ICommand> redoCommands = new List<ICommand>();
//设置执行加法的命令对象
public void SetAddCommand(ICommand addCommand)
{
this.addCommand = addCommand;
}
/// <summary>
/// 提供给客户使用,执行加法功能
/// </summary>
public void AddPressed()
{
this.addCommand.Execute();
//把操作记录到历史记录里面
undoCommands.Add(this.addCommand);
}
//设置减法命令对象
public void SetSubCommand(ICommand subCommand)
{
this.SubCommand = subCommand;
}
/// <summary>
/// 提供给客户使用,执行减法功能
/// </summary>
public void SubPressed()
{
this.SubCommand.Execute();
//把操作记录到历史记录里面
undoCommands.Add(this.SubCommand);
}
/// <summary>
/// 提供给客户使用,执行撤销功能
/// </summary>
public void UndoPressed()
{
if (this.undoCommands.Count > 0)
{
//1-取出撤销记录表里的最后一个命令来撤销
ICommand command = this.undoCommands[this.undoCommands.Count - 1];
command.Undo();
//2-如果还有恢复的功能,那就把这个命令记录到恢复的历史记录里面
this.redoCommands.Add(command);
//3-然后把撤销记录表里的最后一个命令删除
this.undoCommands.Remove(command);
}
else
{
Console.WriteLine("很抱歉,没有可撤销的命令了!");
}
}
/// <summary>
/// 提供给客户使用,执行恢复功能
/// </summary>
public void RedoPressed()
{
if (this.redoCommands.Count>0)
{
//1-取出恢复命令记录表的最后一条记录来恢复
ICommand command = this.redoCommands[this.redoCommands.Count-1];
command.Execute();
//2-把这个命令记录到可撤销的历史记录里面
this.undoCommands.Add(command);
//3-把恢复命令记录表里面的最后一个命令删除掉
this.redoCommands.Remove(command);
}
}
}//Class_end
}
2.3.6、组装命令和接收者并测试
cs
namespace CommandPattern
{
internal class Program
{
static void Main(string[] args)
{
UndoAndRedoOfAddSubTest();
Console.ReadLine();
}
/// <summary>
/// 测试加减法的撤销和恢复功能
/// </summary>
private static void UndoAndRedoOfAddSubTest()
{
Console.WriteLine("------测试加减法的撤销和恢复功能------");
/*1-组装命令和接收者*/
//创建接收者
UndoOPC.IOperation operation = new UndoOPC.Opreation();
//创建命令对象,并组装命令和接收者
UndoOPC.AddCommand addCommand = new UndoOPC.AddCommand(operation,6);
UndoOPC.SubCommand subCommand = new UndoOPC.SubCommand(operation, 2);
/*2-把命令设置到持有者【计算器里面】*/
UndoOPC.Calculator calculator = new UndoOPC.Calculator();
calculator.SetAddCommand(addCommand);
calculator.SetSubCommand(subCommand);
/*3-模拟按下按钮*/
calculator.AddPressed();
Console.WriteLine($"一次加法运算后的结果是【{operation.GetResult()}】");
calculator.SubPressed();
Console.WriteLine($"一次减法运算后的结果是【{operation.GetResult()}】");
/*4-测试撤销*/
calculator.UndoPressed();
Console.WriteLine($"撤销一次后的结果是【{operation.GetResult()}】");
calculator.UndoPressed();
Console.WriteLine($"再撤销一次后的结果是【{operation.GetResult()}】");
/*4-测试恢复*/
calculator.RedoPressed();
Console.WriteLine($"恢复一次后的结果是【{operation.GetResult()}】");
calculator.RedoPressed();
Console.WriteLine($"再恢复一次后的结果是【{operation.GetResult()}】");
}
}//Class_end
}
2.3.7、运行结果

2.4、命令模式之宏命令
什么是宏命令?(简单的说就是包含多个命令的命令,是一个命令的组合)。
需求场景:回忆一下你去饭店吃饭的过程:
1、你进入一家饭店,找到座位坐下;
2、服务员走过来,递给你菜谱;
3、你开始点菜,服务员记录菜单(菜单是三联的,你菜点完后,服务员就会把菜单分为三份,一份给后厨、一份给收银台、一份保留备查);
4、点完才厚,你只用在座位上等候,后厨会按照菜单做菜;
5、每做好一份菜,就会由服务员送到你的桌上;
6、然后你就可以大快朵颐了。
通过以上的步骤可以清楚的看到,到饭店点餐是一个典型的命令模式应用,作为客户的你,只需要发出命令(你需要吃什么菜,每道菜相当于一个命令对象)服务员会记录你点的每道菜,然后把菜传递给后厨,后厨拿到菜单,会按照菜单进行饭菜的制作,后厨就相当于接收者是真正命令执行者,厨师菜知道每道菜的具体实现;而服务员就比较特殊,在不考虑更复杂的管理(如:后厨管理时,负责命令和接收者的组装就是服务员【比如:你点了凉菜、热菜,你其实不知道这些菜到底是谁来完成的,你只管发出命令,但是具体凉菜到哪里做?由谁做?是由服务员将菜单根据菜品不同分别将凉菜菜单送到凉菜部、热菜送到热菜部,然后再由对应的凉菜、热菜厨师现做的】服务员就是一个组装者)。

在前面的实现的命令模式中都是客户发出一个命令,然后就立马执行了;但是我们在饭店点餐,并不是你点一个菜厨师就开始做;而是服务员会等你点完菜,当你说"点完了"的时候,服务员才会启动命令执行(此时,执行命令的时候就不止一个命令了,而是一堆命令,这堆命令就是你点的多个菜)这个点好的各个菜品就是宏命令。
2.4.1、定义厨师接口
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 厨师接口
/// </summary>
internal interface ICooker
{
//示意做菜的方法
void Cook(string name);
}//Interface_end
}
2.4.2、定义具体类型的厨师继承厨师接口实现具体的烹饪方法
厨师分为两类:一类是做热菜的师傅,另一类是做凉菜的师傅:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 做热菜的厨师对象
/// </summary>
internal class HotCooker : ICooker
{
public void Cook(string name)
{
Console.WriteLine($"热菜厨师正在做【{name}】");
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 做凉菜的厨师
/// </summary>
internal class CoolCooker : ICooker
{
public void Cook(string name)
{
Console.WriteLine($"凉菜师傅在做【{name}】");
}
}//Class_end
}
2.4.3、定义命令接口
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 命令接口
/// </summary>
internal interface ICommand
{
//执行命令对应的操作
void Execute();
}//Interface_end
}
2.4.4、定义具体的菜品命令继承接口实现具体的菜
注意:如下的具体菜品命令对象与标准的命令模式实现有一点区别,即标准命令模式实现的时候是通过构造方法传入接收者对象;我们这边菜品实现的时候改为了通过Set方法的方法来设置接收者对象,这样可以动态的切换接收者对象,而不用重构对象。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 萝卜炖排骨命令
/// </summary>
internal class CarrotRribsCommand : ICommand
{
//持有具体操作的厨师对象
private ICooker cooker = null;
/// <summary>
/// 设置具体做菜的厨师对象
/// </summary>
/// <param name="cook">做菜的厨师</param>
public void SetCooker(ICooker cooker)
{
this.cooker = cooker;
}
public void Execute()
{
this.cooker.Cook("萝卜炖排骨");
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 北京烤鸭对象
/// </summary>
internal class PekingDuckCommand : ICommand
{
//持有真正做菜的厨师对象
private ICooker cooker = null;
/// <summary>
/// 设置具体做菜的厨师
/// </summary>
/// <param name="cooker">做菜的厨师</param>
public void SetCooker(ICooker cooker)
{
this.cooker = cooker;
}
public void Execute()
{
this.cooker.Cook("北京烤鸭");
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 蒜泥白肉对象
/// </summary>
internal class GarlicMeatCommand : ICommand
{
//持有真正做菜的厨师对象
private ICooker cooker = null;
/// <summary>
/// 设置做菜的厨师对象
/// </summary>
/// <param name="cooker">做菜的厨师</param>
public void SetCooker(ICooker cooker)
{
this.cooker = cooker;
}
public void Execute()
{
this.cooker.Cook("蒜泥白肉");
}
}//Class_end
}
2.4.5、定义菜单对象
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 菜单对象【是宏命令对象(包含多个命令)】
/// </summary>
internal class MenuCommand : ICommand
{
//用来记录组合本菜单的多道菜品(即多个命令对象)
private List<ICommand> commands = new List<ICommand>();
/// <summary>
/// 点菜,把菜品加入到菜单中
/// </summary>
/// <param name="dishCommand">菜品</param>
public void AddCommand(ICommand command)
{
commands.Add(command);
}
public void Execute()
{
//执行菜单其实就是循环执行菜单里面的每个菜
foreach (var command in commands)
{
command.Execute();
}
}
}//Class_end
}
2.4.6、定义服务员对象
这里的服务员对象相当于标准命令模式中的Client【创建具体的命令对象,并设置命令对象的接收者】加上Invoker【要求命令对象执行请求、通常会持有命令对象,可以持有多个命令对象;这是客户端真正触发命令并要求命令执行对应操作的地方,相当于使用命令对象的入口】。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 服务员对象
/// </summary>
internal class Waiter
{
//持有一个宏命令对象【菜单】
private MenuCommand menuCommand = new MenuCommand();
/// <summary>
/// 客户点菜
/// </summary>
/// <param name="command">菜品</param>
public void OrderDish(ICommand command)
{
//客户传过来的命令对象是没有接收者组装的【需要服务员自己组装】
ICooker hotCooker = new HotCooker();
ICooker coolCooker=new CoolCooker();
//判断到底是组合凉菜师傅还是热菜师傅
if (command is CarrotRribsCommand)
{
((CarrotRribsCommand)command).SetCooker(hotCooker);
}
if (command is PekingDuckCommand)
{
((PekingDuckCommand)command).SetCooker(hotCooker);
}
if (command is GarlicMeatCommand)
{
((GarlicMeatCommand)command).SetCooker(coolCooker);
}
//添加菜单中
menuCommand.AddCommand(command);
}
/// <summary>
/// 客户点菜完毕,表示需要执行命令了【即厨师真正开始做菜了】
/// </summary>
public void OrderOver()
{
this.menuCommand.Execute();
}
}//Class_end
}
2.4.7、客户点菜测试
cs
namespace CommandPattern
{
internal class Program
{
static void Main(string[] args)
{
CustomerOrderDishTest();
Console.ReadLine();
}
/// <summary>
/// 测试客户点菜
/// </summary>
private static void CustomerOrderDishTest()
{
//1-创建服务员
Macros.Waiter waiter=new Macros.Waiter();
//2-创命令对象【即需要点的菜品】
Macros.ICommand carrotRribs = new Macros.CarrotRribsCommand();
Macros.ICommand pekingDuck = new Macros.PekingDuckCommand();
Macros.ICommand GarlicMeat = new Macros.GarlicMeatCommand();
//3-点菜
waiter.OrderDish(carrotRribs);
waiter.OrderDish(pekingDuck);
waiter.OrderDish(GarlicMeat);
//4-点菜完毕
waiter.OrderOver();
}
}//Class_end
}
2.4.8、运行结果

2.5、命令模式之队列请求
所谓的队列请求,就是命令对象进行排队,组成工作队列,然后依次取出命令对象来执行。
继续我们在饭店点餐的例子,其实子后厨,会收到很多菜单,一般是按照菜单传递到后厨的先后顺序来进行处理,对每张菜单,假定也是按照菜品的先后顺序进行制作,那么在后厨就形成了一个菜品队列(即:很多个用户点的菜品命令队列);后厨有很多厨师,每个厨师都从这个命令队列里面取出一个命令,然后按照命令做出菜来,就相当于多个线程在同时处理一个队列请求【后厨就是一个典型的队列请求例子】(厨师只是负责从队列里面取出一个菜品处理,然后在取下一个在处理,仅此而已,厨师并不关心顾客是谁)。
2.5.1、定义命令接口和实现具体的菜品命令
命令接口规范了命令的行为:除了执行命令外,还需要为命令对象设置接收者方法,还增加一个返回发出命令的桌号(我们这里为了简单就只做热菜)
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 命令接口
/// </summary>
internal interface ICommand
{
//执行命令对应的操作
void Execute();
//设置命令的接收者
void SetCooker(ICooker cooker);
//返回发起请求的桌号【即;点菜的桌号】
int GetTableNumber();
}//Interface_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 萝卜炖排骨命令
/// </summary>
internal class CarrotRribsCommand : ICommand
{
//持有具体操作的厨师对象
private ICooker cooker = null;
/// <summary>
/// 设置具体做菜的厨师对象
/// </summary>
/// <param name="cook">做菜的厨师</param>
public void SetCooker(ICooker cooker)
{
this.cooker = cooker;
}
//点菜的桌号
private int tableNum = 0;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="tableNum">点菜的桌号</param>
public CarrotRribsCommand(int tableNum)
{
this.tableNum = tableNum;
}
/// <summary>
/// 获取点菜的桌号
/// </summary>
/// <returns></returns>
public int GetTableNumber()
{
return this.tableNum;
}
public void Execute()
{
this.cooker.Cook("萝卜炖排骨",tableNum);
}
}//Class_end
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 北京烤鸭对象
/// </summary>
internal class PekingDuckCommand : ICommand
{
//持有真正做菜的厨师对象
private ICooker cooker = null;
/// <summary>
/// 设置具体做菜的厨师
/// </summary>
/// <param name="cooker">做菜的厨师</param>
public void SetCooker(ICooker cooker)
{
this.cooker = cooker;
}
public void Execute()
{
this.cooker.Cook("北京烤鸭",tableNum);
}
//点菜的桌号
private int tableNum = 0;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="tableNum">点菜的桌号</param>
public PekingDuckCommand(int tableNum)
{
this.tableNum = tableNum;
}
/// <summary>
/// 获取点菜的桌号
/// </summary>
/// <returns></returns>
public int GetTableNumber()
{
return this.tableNum;
}
}//Class_end
}
2.5.2、厨师接口
厨师接口的烹饪菜品方法需要添加发出命令的桌号,这样在多线程出书信息的时候,才知道到底是在给哪个桌子做菜:
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 厨师接口
/// </summary>
internal interface ICooker
{
/// <summary>
//示意做菜的方法
/// </summary>
/// <param name="name">菜品名称</param>
/// <param name="tableNum">点菜的桌号</param>
void Cook(string name,int tableNum);
}//Interface_end
}
2.5.3、构建命令对象队列
我们这的命令对象队列不使用Queue,直接使用List来模拟队列实现:
cs
private static void ListQueue()
{
List<string> strings=new List<string>();
for (int i = 0; i < 5; i++)
{
strings.Add(i.ToString());
Console.WriteLine(i);
}
Console.WriteLine();
int count = strings.Count;
for (int i = 0; i < count; i++)
{
string str = strings.First();
Console.WriteLine($"当前读取的值是【{str}】");
string str2 = strings[0];
Console.WriteLine($"当前需要移除的值是【{str2}】");
strings.RemoveAt(0);
}
}

cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 命令队列类
/// </summary>
internal class CommondQueue
{
//用来存储命令对象的队列
private static List<ICommand> commands = new List<ICommand>();
/// <summary>
/// 服务员传过来一个新的菜单,需要同步(因为同时会有很多的服务员传入菜单,而同时又有很多厨师从队列里取菜单)
/// </summary>
/// <param name="menuCommand">菜单命令</param>
public static void AddMenu(MenuCommand menuCommand)
{
//一个菜单对象包含很多命令对象
foreach (var command in menuCommand.GetCommandList())
{
lock (command)
{
commands.Add(command);
}
}
}
//厨师从命令队列里面获取命令对象进行处理,也需要同步
public static ICommand GetOneCommand()
{
ICommand command = null;
if (commands.Count>0)
{
lock (commands.First())
{
//模拟队列的先进先出
command = commands.First();
//同时从队列里面去掉这个命令对象
commands.RemoveAt(0);
}
}
return command;
}
}//Class_end
}
2.5.4、菜单向命令队列传递菜品命令,服务员点菜形成菜单
我们有了命令队列,此时就是需要服务员记录菜品为菜单,等待顾客点完菜品;现在执行菜单就相当于把菜品之间传递给后厨(也就是要把菜单里面的所有命令对象加入到命令队列里面)。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 菜单对象【是宏命令对象(包含多个命令)】
/// </summary>
internal class MenuCommand : ICommand
{
//用来记录组合本菜单的多道菜品(即多个命令对象)
private List<ICommand> commands = new List<ICommand>();
/// <summary>
/// 点菜,把菜品加入到菜单中
/// </summary>
/// <param name="dishCommand">菜品</param>
public void AddCommand(ICommand command)
{
commands.Add(command);
}
public void SetCooker(ICooker cooker)
{
//什么也不用做
}
public int GetTableNumber()
{
//什么也不做
return 0;
}
/// <summary>
/// 获取菜单中的多个命令对象
/// </summary>
/// <returns></returns>
public List<ICommand> GetCommandList()
{
return this.commands;
}
public void Execute()
{
//执行菜单就是把菜单传递给后厨(即添加到命令队列中,供后厨厨师自取)
CommondQueue.AddMenu(this);
}
}//Class_end
}
由于后面考虑了后厨管理,此时服务员不知道菜单的真正接收者是谁了(到底是哪个厨师做菜),所以现在服务员的职责就很简单了给顾客点菜,知道顾客点菜完成后将菜单通过菜单对象传递给菜单队列。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 服务员对象
/// </summary>
internal class Waiter
{
//持有一个宏命令对象【菜单】
private MenuCommand menuCommand = new MenuCommand();
/// <summary>
/// 客户点菜
/// </summary>
/// <param name="command">菜品</param>
public void OrderDish(ICommand command)
{
//添加到菜单中
menuCommand.AddCommand(command);
}
/// <summary>
/// 客户点菜完毕,表示需要执行命令了,这里执行这个菜单的组合命令【即厨师真正开始做菜了】
/// </summary>
public void OrderOver()
{
this.menuCommand.Execute();
}
}//Class_end
}
2.5.5、厨师从命令队列中取菜单去做菜
现在有了命令队列,且有人负责向命令队列里面添加菜单;那么此时真正做菜的人就是厨师了,厨师从命令菜单里面取菜单,并开始做菜(且在做菜钱会把自己设置到命令对象中去当接收者,表示这个菜有我来做);为了更好的体现命令队列的用法,我们使用多线程来模拟多个厨师同事做菜(即他们可以同时从命令队列里面获取菜单,然后开始做菜,做好一道菜后又取下一道菜,如此循环);为了需要知道菜品是由哪个厨师制作的,所以需要再厨师对象初始化的时候就传入厨师姓名。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 做热菜的厨师对象
/// </summary>
internal class HotCooker : ICooker
{
//厨师姓名
private string cookerName;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="name">厨师姓名</param>
public HotCooker(string cookerName)
{
this.cookerName = cookerName;
}
public void Cook(string name,int tableNum)
{
//每次做菜的时间都是不一定的,用随机数来模拟
Random random = new Random(Guid.NewGuid().GetHashCode());
int cookTime = random.Next(5,20);
Console.WriteLine($"热菜厨师【{this.cookerName}】正在给【{tableNum}】桌做【{name}】");
try
{
//让线程休息一下,表示正在做菜
Thread.Sleep(cookTime);
}
catch (Exception ex)
{
throw ex;
}
finally
{
Console.WriteLine($"热菜厨师【{cookerName}】为【{tableNum}】桌做好了【{name}】,共花费【{cookTime}】分钟");
}
}
public void Run()
{
//new Thread(new ThreadStart(()=>
//{
// while (true)
// {
// Thread.Sleep(1000);
// //从命令队列里面获取命令对象
// ICommand command = CommondQueue.GetOneCommand();
// if (command!=null)
// {
// //说明去到命令对象了,这个命令对象还没有设置接收者(因为前面还不知道
// //到底哪一个厨师来真正执行这个命令;现在知道了,就是当前的厨师实例,设置命令到命令对象中)
// command.SetCooker(this);
// command.Execute();
// }
// }
//})).Start();
Task task = new Task(() =>
{
while (true)
{
Thread.Sleep(1000);
//从命令队列里面获取命令对象
ICommand command = CommondQueue.GetOneCommand();
if (command != null)
{
//说明去到命令对象了,这个命令对象还没有设置接收者(因为前面还不知道
//到底哪一个厨师来真正执行这个命令;现在知道了,就是当前的厨师实例,设置命令到命令对象中)
command.SetCooker(this);
command.Execute();
}
}
});
task.Start();
}
}//Class_end
}
2.5.6、实现后厨管理
现在由于后厨由很多厨师需要管理,我们专门定义一个后厨管理类(定义厨师是哪些人,且安排他们做菜)。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CommandPattern.Macros
{
/// <summary>
/// 后厨管理类
/// </summary>
internal class CookerManager
{
//用来控制是否需要创建厨师,如果已经创建就不要执行了
private static bool runFlag = false;
//运行后厨管理,创建厨师对象并启动他们相应的线程(无论运行多少次,创建厨师对象和启动线程的工作只做一次)
public static void RunCookerManager()
{
if (!runFlag)
{
runFlag = true;
//创建三位厨师
HotCooker hotCooker1 = new HotCooker("张三");
HotCooker hotCooker2 = new HotCooker("李四");
HotCooker hotCooker3 = new HotCooker("王五");
//启动各位厨师做菜
hotCooker1.Run();
hotCooker2.Run();
hotCooker3.Run();
}
}
}//Class_end
}
2.5.7、客户端测试
cs
namespace CommandPattern
{
internal class Program
{
static void Main(string[] args)
{
TestQueue();
Console.ReadLine();
}
/// <summary>
/// 测试队列
/// </summary>
public static void TestQueue()
{
Console.WriteLine("测试队列");
//1-先启动后台,让整个程序运行起来
Macros.CookerManager.RunCookerManager();
//2-为了简单,直接使用循环模拟桌号点菜过程
for (int i = 0; i <3; i++)
{
//创建服务员
Macros.Waiter waiter = new Macros.Waiter();
//创建命令对象(即需要点的菜品)
Macros.ICommand carrotRribs = new Macros.CarrotRribsCommand(i);
Macros.ICommand pekingDuck = new Macros.PekingDuckCommand(i);
//点菜(就是服务员把这些菜让服务员记录下来)
waiter.OrderDish(carrotRribs);
waiter.OrderDish(pekingDuck);
//点菜完毕
waiter.OrderOver();
}
}
}//Class_end
}
2.5.8、运行结果

由于我们使用了多线程在处理请求队列,可能每次运行的效果不一样;在多线程环境下,我们虽然保证了队列对象获取的先进先出,但究竟是哪个厨师做菜,做多长时间都不是固定的。