学习设计模式《十二》——命令模式

一、基础概念

**命令模式的本质是【封装请求】**命令模式的关键是把请求封装成为命令对象,然后就可以对这个命令对象进行一系列的处理(如:参数化配置、可撤销操作、宏命令、队列请求、日志请求等)。

**命令模式的定义:**将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

命令模式的说明:

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、运行结果

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

三、项目源码工程

kafeiweimei/Learning_DesignPattern: 这是一个关于C#语言编写的基础设计模式项目工程,方便学习理解常见的26种设计模式https://github.com/kafeiweimei/Learning_DesignPattern

相关推荐
傍晚冰川1 小时前
FreeRTOS任务调度过程vTaskStartScheduler()&任务设计和划分
开发语言·笔记·stm32·单片机·嵌入式硬件·学习
月初,1 小时前
MongoDB学习和应用(高效的非关系型数据库)
学习·mongodb·nosql
casual_clover2 小时前
Android 之 kotlin 语言学习笔记四(Android KTX)
android·学习·kotlin
Love__Tay2 小时前
【学习笔记】Python金融基础
开发语言·笔记·python·学习·金融
我的golang之路果然有问题3 小时前
云服务器部署Gin+gorm 项目 demo
运维·服务器·后端·学习·golang·gin
不伤欣4 小时前
游戏设计模式 - 子类沙箱
游戏·unity·设计模式
漫谈网络4 小时前
MVC与MVP设计模式对比详解
设计模式·mvc
蔡蓝4 小时前
设计模式-观察着模式
java·开发语言·设计模式
Lester_11014 小时前
嵌入式学习笔记 - freeRTOS xTaskResumeAll( )函数解析
笔记·stm32·单片机·学习·freertos
jackson凌4 小时前
【Java学习笔记】Math方法
java·笔记·学习