学习设计模式《十二》——命令模式
一、基础概念
命令模式的本质是【封装请求】命令模式的关键是把请求封装成为命令对象,然后就可以对这个命令对象进行一系列的处理(如:参数化配置、可撤销操作、宏命令、队列请求、日志请求等)。
命令模式的定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
命令模式的说明:
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、定义主板接口
因为主板是真正接收和实现用户命令请求的因此先来定义主板接口,约束命令对象:
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、创建具体的主板对象继承主板接口实现具体的功能
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
}
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、定义命令接口
因为对于用户来说,电脑开机我就只用按下开机按钮就可以了【这个动作抽象出来就是用户发出开机的命令,其他的就不用管了】为了扩展多种命令,我们的接口就只定义一个方法就是执行。
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、创建具体的命令继承命令接口并实现
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、创建机箱对象用于传递用户下达的命令给主板操作
由于用户根本不知道主板是什么,且不想直接直接通过操作主板才实现电脑的开机,只希望简单的按下按钮就可以启动,所以我们在用户与主板之间创建一个中间对象【机箱】来同时对接用户和主板【由于用户给机箱下达命令是通过按按钮的方式,则需要给机箱定义对应的按钮功能】
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、客户使用按钮启动系统
现在我们都具备了实现功能的主板、命令、及其作为用户下达命令与主板关联的机箱;但目前还都是零散的各个配件,无法使用;我们需要将这些配件组成一个整体提供给用户使用【现实生活中国,这个组装的工作是由装机工程师完成的,我们这里为了简单,就直接在客户端那里封装一个方法使用】。
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、定义主板接口
新增一个重启按钮的方法:
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、创建具体的主板对象继承主板接口实现具体的功能
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
}
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、定义命令接口
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、创建具体的命令继承命令接口并实现
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
}
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、创建机箱对象用于传递用户下达的命令给主板操作
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、客户使用按钮启动系统
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、定义加减法运算的接口
该接口定义了可以执行加法、减法操作、设置计算初始值、获取计算结果方法:
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、定义对象继承接口实现具体的加法减法
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、定义命令接口
正常来说命令接口只需要定义执行方法就可以,但这里还需要撤销功能,则还需要定义撤销方法:
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、具体的加法、减法命令实现
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
}
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,持有多个命令对象,计算器是可以实现可撤销操作的地方(要想实现操作的可撤销操作,就需要把操作过的命令都记录下来,形成命令的历史列表,撤销的时候从最后一个开始执行撤销)。
什么时候向命令历史列表添加值呢?(在每次操作加法、减法按钮按下的时候添加)。
什么时候移除命令列表里的值呢?(在撤销的时候移除命令历史列表的值,同时给恢复历史命令表里增加值);
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、组装命令和接收者并测试
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、定义厨师接口
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、定义具体类型的厨师继承厨师接口实现具体的烹饪方法
厨师分为两类:一类是做热菜的师傅,另一类是做凉菜的师傅:
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
}
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、定义命令接口
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方法的方法来设置接收者对象,这样可以动态的切换接收者对象,而不用重构对象。
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
}
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
}
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、定义菜单对象
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【要求命令对象执行请求、通常会持有命令对象,可以持有多个命令对象;这是客户端真正触发命令并要求命令执行对应操作的地方,相当于使用命令对象的入口】。
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、客户点菜测试
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、定义命令接口和实现具体的菜品命令
命令接口规范了命令的行为:除了执行命令外,还需要为命令对象设置接收者方法,还增加一个返回发出命令的桌号(我们这里为了简单就只做热菜)
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
}
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
}
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、厨师接口
厨师接口的烹饪菜品方法需要添加发出命令的桌号,这样在多线程出书信息的时候,才知道到底是在给哪个桌子做菜:
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来模拟队列实现:
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);}}
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、菜单向命令队列传递菜品命令,服务员点菜形成菜单
我们有了命令队列,此时就是需要服务员记录菜品为菜单,等待顾客点完菜品;现在执行菜单就相当于把菜品之间传递给后厨(也就是要把菜单里面的所有命令对象加入到命令队列里面)。
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
}
由于后面考虑了后厨管理,此时服务员不知道菜单的真正接收者是谁了(到底是哪个厨师做菜),所以现在服务员的职责就很简单了给顾客点菜,知道顾客点菜完成后将菜单通过菜单对象传递给菜单队列。
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、厨师从命令队列中取菜单去做菜
现在有了命令队列,且有人负责向命令队列里面添加菜单;那么此时真正做菜的人就是厨师了,厨师从命令菜单里面取菜单,并开始做菜(且在做菜钱会把自己设置到命令对象中去当接收者,表示这个菜有我来做);为了更好的体现命令队列的用法,我们使用多线程来模拟多个厨师同事做菜(即他们可以同时从命令队列里面获取菜单,然后开始做菜,做好一道菜后又取下一道菜,如此循环);为了需要知道菜品是由哪个厨师制作的,所以需要再厨师对象初始化的时候就传入厨师姓名。
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、实现后厨管理
现在由于后厨由很多厨师需要管理,我们专门定义一个后厨管理类(定义厨师是哪些人,且安排他们做菜)。
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、客户端测试
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
相关文章:

学习设计模式《十二》——命令模式
一、基础概念 命令模式的本质是【封装请求】命令模式的关键是把请求封装成为命令对象,然后就可以对这个命令对象进行一系列的处理(如:参数化配置、可撤销操作、宏命令、队列请求、日志请求等)。 命令模式的定义:将一个…...

十三、【核心功能篇】测试计划管理:组织和编排测试用例
【核心功能篇】测试计划管理:组织和编排测试用例 前言准备工作第一部分:后端实现 (Django)1. 定义 TestPlan 模型2. 生成并应用数据库迁移3. 创建 TestPlanSerializer4. 创建 TestPlanViewSet5. 注册路由6. 注册到 Django Admin 第二部分:前端…...

手撕 K-Means
1. K-means 的原理 K-means 是一种经典的无监督学习算法,用于将数据集划分为 kk 个簇(cluster)。其核心思想是通过迭代优化,将数据点分配到最近的簇中心,并更新簇中心,直到簇中心不再变化或达到最大迭代次…...

SmolVLA: 让机器人更懂 “看听说做” 的轻量化解决方案
🧭 TL;DR 今天,我们希望向大家介绍一个新的模型: SmolVLA,这是一个轻量级 (450M 参数) 的开源视觉 - 语言 - 动作 (VLA) 模型,专为机器人领域设计,并且可以在消费级硬件上运行。 SmolVLAhttps://hf.co/lerobot/smolvla…...

day45python打卡
知识点回顾: tensorboard的发展历史和原理tensorboard的常见操作tensorboard在cifar上的实战:MLP和CNN模型 效果展示如下,很适合拿去组会汇报撑页数: 作业:对resnet18在cifar10上采用微调策略下,用tensorbo…...

AIGC赋能前端开发
一、引言:AIGC对前端开发的影响 1. AIGC与前端开发的关系 从“写代码”到“生成代码”传统开发痛点:重复性编码工作、UI 设计稿还原、问题定位与调试...核心场景的AI化:需求转代码(P2C)、设计稿转代码(D2…...

Web 3D协作平台开发案例:构建制造业远程设计与可视化协作
HOOPS Communicator为开发者提供了丰富的定制化能力,助力他们在实现强大 Web 3D 可视化功能的同时,灵活构建符合特定业务需求的工程应用。对于希望构建在线协同设计工具的企业而言,如何在保障性能与用户体验的前提下实现高效开发,…...

AI Agent开发第78课-大模型结合Flink构建政务类长公文、长文件、OA应用Agent
开篇 AI Agent2025确定是进入了爆发期,到处都在冒出各种各样的实用AI Agent。很多人、组织都投身于开发AI Agent。 但是从3月份开始业界开始出现了一种这样的声音: AI开发入门并不难,一旦开发完后没法用! 经历过至少一个AI Agent从开发到上线的小伙伴们其实都听到过这种…...
极空间z4pro配置gitea mysql,内网穿透
极空间z4pro配置gitea mysql等记录,内网穿透 1、mysql、gitea镜像下载,极空间不成功,先用自己电脑科学后下载镜像,拉取代码: docker pull --platform linux/amd64 gitea/gitea:1.23 docker pull --platform linux/amd64 mysql:5.…...

第三方测试机构进行科技成果鉴定测试有什么价值
在当今科技创新的浪潮中,科技成果的鉴定测试至关重要,而第三方测试机构凭借其独特优势,在这一领域发挥着不可替代的作用。那么,第三方测试机构进行科技成果鉴定测试究竟有什么价值呢? 一、第三方测试机构能提供独立、公…...

华为云Flexus+DeepSeek征文|基于华为云Flexus X和DeepSeek-R1打造个人知识库问答系统
目录 前言 1 快速部署:一键搭建Dify平台 1.1 部署流程详解 1.2 初始配置与登录 2 构建专属知识库 2.1 进入知识库模块并创建新库 2.2 选择数据源导入内容 2.3 上传并识别多种文档格式 2.4 文本处理与索引构建 2.5 保存并完成知识库创建 3接入ModelArts S…...

【数据结构】_排序
【本节目标】 排序的概念及其运用常见排序算法的实现排序算法复杂度及稳定性分析 1.排序的概念及其运用 1.1排序的概念 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 1.2特性…...
《前端面试题:JS数据类型》
JavaScript 数据类型指南:从基础到高级全解析 一、JavaScript 数据类型概述 JavaScript 作为一门动态类型语言,其数据类型系统是理解这门语言的核心基础。在 ECMAScript 标准中,数据类型分为两大类: 1. 原始类型(Pr…...

PPT转图片拼贴工具 v4.3
软件介绍 这个软件就是将PPT文件转换为图片并且拼接起来。 效果展示 支持导入文件和支持导入文件夹,也支持手动输入文件/文件夹路径 软件界面 这一次提供了源码和开箱即用版本,exe就是直接用就可以了。 软件源码 import os import re import sys …...

Chrome安装代理插件ZeroOmega(保姆级别)
目录 本文直接讲解一下怎么本地安装ZeroOmega一、下载文件在GitHub直接下ZeroOmega 的文件(下最新版即可) 二、安装插件打开 Chrome 浏览器,访问 chrome://extensions/ 页面(扩展程序管理页面),并打开开发者…...

Transformer-BiGRU多变量时序预测(Matlab完整源码和数据)
Transformer-BiGRU多变量时序预测(Matlab完整源码和数据) 目录 Transformer-BiGRU多变量时序预测(Matlab完整源码和数据)效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现Transformer-BiGRU多变量时间序列预测&…...

新华三H3CNE网络工程师认证—Easy IP
Easy IP 就是“用路由器自己的公网IP,给全家所有设备当共享门牌号”的技术!(省掉额外公网IP,省钱又省配置!) 生活场景对比,想象你住在一个小区:普通动态NAT:物业申请了 …...
《视觉SLAM十四讲》自用笔记 第二讲:SLAM系统概述
在rm队伍里作为算法组梯队队员度过了一个赛季,为了促进和负责其他工作的算法组成员的交流,我决定在接下来的半个学期里(可能更快)读完这本书,并将其中的部分理论应用于我自制的雷达导航小车上。 以下为第二讲的部分笔记…...
vscode 插件 eslint, 检查 js 语法
1. 起因, 目的: 我的需求 vscode 写js代码, 有什么插件能进行语法检查。 比如某个函数没有定义,getName(), 但是却调用了。 那么这个插件会给出警告,在 getName() 给出红色波浪线。类似这种效果的插件, 有吗…...

Excel 模拟分析之单变量求解简单应用
正向求解 利用公式根据贷款总额、还款期限、贷款利率,求每月还款金额 反向求解 根据每月还款能力,求最大能承受贷款金额 参数: 目标单元格:求的值所在的单元格 目标值:想要达到的预期值 可变单元格:变…...

装备制造项目管理具备什么特征?如何选择适配的项目管理软件系统进行项目管控?
国内某大型半导体装备制造企业与奥博思软件达成战略合作,全面引入奥博思 PowerProject 打造企业专属项目管理平台,进一步提升智能制造领域的项目管理效率与协同能力。 该项目管理平台聚焦半导体装备研发与制造的业务特性,实现了从项目立项、…...

FPGA 动态重构配置流程
触发FPGA 进行配置的方式有两种,一种是断电后上电,另一种是在FPGA运行过程中,将PROGRAM 管脚拉低。将PROGRAM 管脚拉低500ns 以上就可以触发FPGA 进行重构。 FPGA 的配置过程大致可以分为:配置的触发和建立阶段、加载配置文件和建…...
Elasticsearch的审计日志(Audit Logging)介绍
Elasticsearch 的审计日志(Audit Logging)是一种记录与安全相关事件的功能,用于监控和追踪对集群的访问行为。通过审计日志,管理员可以了解谁在何时对哪些资源执行了什么操作,从而满足合规性要求、进行安全分析和排查异常行为。 一、审计日志的核心功能 记录安全事件捕获…...
软件测试:质量保障的基石与未来趋势
软件测试作为软件开发生命周期中的关键环节,不仅是发现和修复缺陷的手段,更是确保产品质量、提升用户体验和降低开发成本的重要保障。在当今快速迭代的互联网时代,测试已从单纯的验证活动演变为贯穿整个开发过程的质量管理体系。本文将系统阐…...

网络安全逆向分析之rust逆向技巧
rust逆向技巧 rust逆向三板斧: 快速定位关键函数 (真正的main函数):观察输出、输入,字符串搜索,断点等方法。定位关键 加密区 :根据输入的flag,打硬件断点,快速捕获程序中对flag访问的位置&am…...
Docker容器化技术概述与实践
哈喽,大家好,我是左手python! Docker 容器化的基本概念 Docker 容器化是一种轻量级的虚拟化技术,通过将应用程序及其依赖项打包到一个可移植的容器中,使其在任何兼容 Docker 的环境中都能运行。与传统的虚拟机技术不同…...
win中将pdf转为图片
0 资料 博客 1 正文 直接使用这个软件即可https://sourceforge.net/projects/pkpdfconverter/...

Leetcode 2494. 合并在同一个大厅重叠的活动
1.题目基本信息 1.1.题目描述 表: HallEvents ----------------- | Column Name | Type | ----------------- | hall_id | int | | start_day | date | | end_day | date | ----------------- 该表可能包含重复字段。 该表的每一行表示活动的开始日期和结束日期&…...

vue+elementui 网站首页顶部菜单上下布局
菜单集合后台接口动态获取,保存到store vuex状态管理器 <template><div id"app"><el-menu:default-active"activeIndex2"class"el-menu-demo"mode"horizontal"select"handleSelect"background-…...

网络安全-等级保护(等保) 3-3-1 GB/T 36627-2018 附录A (资料性附录) 测评后活动、附 录 B (资料性附录)渗透测试的有关概念说明
################################################################################ GB/T 36627-2018 《信息安全技术 网络安全等级保护测试评估技术指南》对网络安全等级保护测评中的相关测评技术进行明确的分类和定义,系统地归纳并阐述测评的技术方法,概述技术性安全测试和…...