举例说,在命令模式(Command Pattern)

2022-07-05 19:43:53 浏览数 (2)

大家好,又见面了,我是全栈君

在前面加上

谈到命令,大部分的人脑海中会想到以下这幅画面

这在现实生活中是一副讽刺漫画,做决定的人不清楚运行决定的人有何特点,瞎指挥、外行领导内行说的就是这样的。只是在软件设计领域,我们显然要为这样的现象正名了,让狮王能记住全部属下的特点并直接打电话通知任务。显然是为难他了,领导们非常忙。这是秘书处的工作。狮王仅仅要做出指示“近期鼠患猖獗。该抓抓了”,那秘书们就要起草红头文件(命令)并发给相关运行部门(猫),各司其职提高效率,公布请求的(秘书处)和运行请求的(猫)分离开来,将行为(抓鼠)封装成对象(红头文件),这就是命令模式。

官方定义

将一个请求封装成对象,从而可用不同的请求对客户进行參数化,对请求排队或记录请求日志,以及运行可撤销的操作——GOF23。

将请求封装成对象,这个对象就是命令对象。在结构化程序中。请求一般是以函数的形式表现的。对于该请求中可能涉及到的运行对象,假设我们以函參的形式传递,这会造成下面几个问题

1)紧耦合。客户程序须要依赖运行对象,在上例中,上层在公布命令时须要依赖详细的属下,这会违反依赖倒置原则;把请求封装成命令对象,这些命令对象遵循共同的命令接口,这就攻克了高层依赖问题。

2)函数没用强调撤销(undo)操作,函数中对对象状态的保存须要额外的业务逻辑。

3)函数的复用性及扩展型较差,这也是为什么结构化逐渐被对象语言代替的原因。

不同的请求能够对客户进行參数化,这个类似于策略模式的动态设置算法。在上例中。不同的请求被封装成了不同的命令。用户是能够自由选择当前要运行的命令是哪个。

“面向接口而不是实现”的编程原则,能够在运行时依据上下文情况来选择详细命令。比方,狮王并不总和老鼠过不去,这段时间机关单位工作作风较差,迟到现象频发。狮王就会指示“多打鸣,抓四风”,这时秘书处就会起草打鸣文件并保证其能够下发运行。秘书处的职责,事实上就像是触发器invoker(遥控器),他们起草何种文件并运行,就相当于触发器绑定了何种命令对象。这样的绑定关系是由Client(狮王)决定的。

请求排队、记录日志、和可撤销,这三点是命令模式的典型应用,在后文会提及,这个能够从类似文本编辑软件word中做比較,这些软件的用户界面设计广泛借鉴了命令模式的特点。封装请求成命令对象后,能够把一系列的命令排队、记录、撤销等。

角色

在该模式中包括下面几个角色:

Command —— 命令接口,上例中,相应于秘书处的红头文件的模板。当中定义了详细命令所需实现的方法。如execute、undo等。

ConcreateCommand —— 详细命令,上例中,相应于抓鼠文件、打鸣文件。

这些命令中一般会包括接受者的引用,比方抓鼠红头文件会相应于一个猫的引用实例,execute方法会调用猫的捕鼠方法,打鸣同理。

Client —— 创建详细命令并设置接受者,这是命令的实际制定者。相应于狮王。每一个详细命令对象在创建时。其接受者就已经被定义好了,在创建详细命令时须要关心谁来运行。注意这里的Client并非通常所说的使用用户,Client的作用类似于装载器Loader,创建不同的命令对象并将其动态绑定在invoker中。

Invoker —— 要求命令运行的对象,相应于上例的秘书处,他们的任务是确保上通下达。对于每一个详细的命令都要保证被运行。

Receiver —— 接受者。命令的详细运行者,相应于上例的阿鸡阿猫,这些运行者一般会被组合在详细命令中。他们有自己的方法,这些方法一般会在详细命令的execute方法中被调用。

代码实现

实现的UML图例如以下所看到的

以上图为例,Command是命令接口。秘书类Secretary组合该接口对象,捕鼠和打鸣实现了该命令接口。因为逻辑比較简单,直接贴代码,首先是个简单到不好意思的Command接口。

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public interface Command {
	public abstract void execute();
}
</span></span></span>

详细命令包含两个,CatchMouseCommand和CrowCommand

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class CatchMouseCommand implements Command{

	Cat cat;
	
	public CatchMouseCommand(Cat cat) {
		// TODO Auto-generated constructor stub
		this.cat = cat;
	}
	
	@Override
	public void execute() {
		// TODO Auto-generated method stub
		cat.CatchMouse();
	}

}</span></span></span>

And

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class CrowCommand implements Command{
	
	Cock cock;
	
	public CrowCommand(Cock cock) {
		// TODO Auto-generated constructor stub
		this.cock = cock;
	}
	
	@Override
	public void execute() {
		// TODO Auto-generated method stub
		cock.Crow();
	}
	
}</span></span></span>

能够看到这两个详细命令类都组合了接受者类。Cat或Cock,简单起见,这两个接受这类仅仅含有一个相应方法,例如以下

Cat

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">//猫类-receiver
public class Cat {
	//捕鼠方法
	public void CatchMouse() {
		System.out.println("Cat is catching mouse");
	}
}</span></span></span>

Cock

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">//公鸡类-receiver
public class Cock {
	//打鸣方法
	public void Crow() {
		System.out.println("Cock is crowing");
	}
}</span></span></span>

秘书类里组合了命令对象和两个方法,setCommand是因为设置详细命令,而publicCommand则类似于结构化语言中的回调,当须要运行该命令时这种方法就会调用详细命令的execute函数,秘书类作为请求发起者,并不关心详细命令由谁运行,怎样运行,这就实现了请求者和实现者的解耦。

invoker不关心receiver,反之亦然。

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class Secretary {
	Command command;
	
	public void setCommand(Command command) {
		this.command = command;
	}
	
	public void publicCommand() {
		this.command.execute();
	}
}</span></span></span>

Lion在本例中是命令的其实的制定者。他创建了详细命令并在合适时机交由秘书完毕命令公布和运行(receiver完毕),软件开发实践中这个类更像是一个装载者,他建立一种映射关系,特定的invoker绑定特定的ConcreteCommand,后文会以word编辑器软件开发进行分析。

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">/**
 * Lion不是实际的使用客户,其作用相似于装载器Loader,其作用是
 * 为触发器Invoker(秘书对象secretary)绑定不同的命令对象(catchMouseCommand)。 * 这就像在相似word软件的菜单中,不同的菜单项MenuItem就是不同Invoker,每一个菜单项 * 都会绑定一个命令对象,甚至多个菜单项可能绑定同样的命令对象。 * 用户点击时菜单项会触发其绑定的命令对象方法。 */public class Lion {		Secretary secretary;		public Lion(Secretary secretary) {		// TODO Auto-generated constructor stub		this.secretary = secretary;	}		public void createCatchMouseOrder() {		Cat cat = new Cat();		CatchMouseCommand catchMouseCommand = new CatchMouseCommand(cat);		secretary.setCommand(catchMouseCommand);	}		public void createCrowCommand() {		Cock cock = new Cock();		CrowCommand crowCommand = new CrowCommand(cock);		secretary.setCommand(crowCommand);	}}</span></span></span>

完毕了上述的基本类后。我们须要加入用户程序进行測试,例如以下

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public class UserApplication {
	/** 这是通常意义上的用户程序,也就是使用命令模式的上下文环境。
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Secretary secretary = new Secretary();
		//lion的作用相似Loader。为secretary(invoker)绑定命令对象。
		Lion lion = new Lion(secretary);
		
		lion.createCatchMouseOrder();
		secretary.publicCommand();
		
		lion.createCrowCommand();
		secretary.publicCommand();
	}
}</span></span></span>

从測试例中能够看到,lion为secretary动态绑定了不同的command,进而完毕请求參数化。在本例中当secretary接受到详细的command就会publiccommand(公布运行),实际在非常多场合,这个公布是由用户事件驱动的,比方某个MenuItem在初始化时绑定了相应功能。仅仅有在用户点击该MenuItem时,才会通过该菜单项的回调方法调用publiccommand,进而完毕实际的运行。 该測试例运行结果例如以下:

Cat is catching mouse Cock is crowing

扩展场景

上述代码演示样例是命令模式的一个使用演示样例,只解答了官方定义中的前半部分。即“请求參数化”,软件实践中命令模式应用较为广泛的场景是文本编辑器或者是IDE用户界面的开发,以此为例解释下官方定义中所说的请求排队、记录日志以及可撤销操作。

“请求排队”

假设你在一个配置一般的机子上频繁操作eclipse,通常你会看到以下这种界面

后面一大推Waiting的任务,就是正在排队的请求,这个现象的原因是以下这样的图:

我们要求IDE运行的请求被封装成命令对象放置在工作队列中,每个空暇线程会得到并运行该命令对象,但资源有限。调度器须要限制可以使用的线程数量。当有新的线程空暇时。排队等待的命令对象才会被顺序运行,这就是命令模式在请求排队中的应用方式。

“记录请求日志”

这个主要是应用于大型数据库的管理操作中。对于本文所举的样例实际意义不大。在大型数据库的维护中,全部的操作修改都是以命令对象的方式进行的,有些修改必须是以事务的方式进行。由于这些修改彼此都是紧密联系的,对于经年累月的频繁修改,无法做到每次修改的数据库内容都做一次备份,那样须要太多资源,于是,把每次的修改命令对象以序列化方式保存(store)在磁盘上,每两个checkpoint点之间的修改,都保存起来。

这样在系统发生崩溃时,通过反序列化的方式从磁盘上装载(load)这些命令对象,进而完毕这些命令的undo操作。这样就完毕了数据库系统恢复。

完毕上述任务不仅须要命令对象支持序列化操作。并且对于Command接口也有了新的要求。例如以下

“可撤销undo”Ctrl Z

还是举前文的样例。撤销操作要求对象回到命令运行前的状态,这就须要在Command接口中加入undo方法。

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">public interface Command {
	public abstract void execute();
	public abstract void undo(); //新增undo接口
}</span></span>

公鸡类Cock内部须要一个表示打鸣频率的实例。如果公鸡打鸣频率有高、中、低三种。通常情况下打鸣频率为低,可能每周打鸣2次。可是命令下达后,打鸣频率明显升高。达到每周7次,这就是新的Cock类

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;"><span style="font-family:KaiTi_GB2312;font-size:18px;">//公鸡类-receiver
public class Cock {
	
	public static final int LOW = 0;
	public static final int MEDIUM = 1;
	public static final int HIGH = 2;
	
	private int CrowFrequence; // 新添一个记录打鸣频率的状态实例,默觉得LOW
	
	//打鸣方法
	public void Crow() {
		System.out.println("Cock is crowing");
		setCrowFrequence(HIGH); // 调用打鸣方法后,频率为HIGH
	}

	//Getter和 Setter函数,获取和设置当前打鸣频率
	public int getCrowFrequence() {
		return CrowFrequence;
	}

	public void setCrowFrequence(int crowFrequence) {
		CrowFrequence = crowFrequence;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "CrowFrequence is "   CrowFrequence;
	}
}</span></span>

在详细命令中。须要加入一个状态来记录命令运行前的打鸣频率,在风头过后运行undo操作时,这个被记录的频率值就被恢复了。

代码语言:javascript复制
<span style="font-family:KaiTi_GB2312;font-size:18px;">public class CrowCommand implements Command{
	
	Cock cock;
	int PrevStage;    //记录在命令运行前的打鸣频率状态
	
	public CrowCommand(Cock cock) {
		// TODO Auto-generated constructor stub
		this.cock = cock;
	}
	
	@Override
	public void execute() {
		// TODO Auto-generated method stub
		PrevStage = cock.getCrowFrequence(); //运行命令前记录。		System.out.println("before execute : "   cock);		cock.Crow();		System.out.println("after execute : "   cock);	}	@Override	public void undo() {		// TODO Auto-generated method stub		cock.setCrowFrequence(PrevStage); //运行undo操作,设置保持的prevStage		System.out.println("after undo : "   cock);	}	}</span>

改写測试例加入运行undo操作,得到的结果例如以下

这样的undo情况比較简单,不过保存了一个实例域。并且只能够undo上一步,非常多时候须要建立操作的历史记录,这样就须要保存一个运行command的链表,每个链表中都含有能够撤销的命令及其详细实现,这个在类似word和PS之类的软件中应用的非常广泛,不做赘述。

“宏命令”

这是一种特殊的命令类。在该类中定义一连串的命令list,运行和撤销时。将该list中全部的命令都运行一遍。能够自己定义加入或者删除list中的详细命令,熟练使用word的同学都用过宏命令,比方毕业论文模板对于字体、段落、页眉页脚等全部格式的要求能够被简单定义成一个宏模板,仅仅要应用这个宏模板就能够高速自己主动的将当前文档设置成论文要求格式。

结构

该结构和上文uml图类似,可自行对照。

官方定义

效果及注意问题点

1)命令模式将请求调用者和实际运行这解耦,符合依赖倒置原则。

2)详细的command对象能够像其他对象一样进行扩展。

这是一个把函数抽象成类的特殊对象,较普通函数优势前文已述。

3)该结构符合开发封闭原则,加入新的类不须要修改原代码结构。

4)详细命令对象的智能程度怎样。这个命令对象是否一定依赖于receiver完毕操作,依赖程度是否会变化。

本文的详细对象是全然依赖于接受者的方法。

5)命令是否有必要进行undo操作。undo操作须要保持怎么的状态值。假设状态是较为复杂的对象,须要引入很多其他实例进行表示。在撤销过程中是否会引起接受者内部的其他变化。

与其它模式的关系

大型文本编辑软件的菜单树是分成复杂的,除了前文提到的宏命令以外,与组合模式相结合能够实现具有多层分支结构的菜单树命令。

当用来记录接受者的复杂状态时,能够使用备忘录模式。利用该模式能够完毕撤销操作后的状态恢复。

收尾

命令模式是在软件实践中应用较为广泛的一种模式,这个模式的应用场景较为特别,尤其是对于菜单树和数据库相关的功能模块中,这样的模式的长处明显,它分离了任务的请求者和运行者,把行为封装成对象,从而能够完毕类似请求參数化、状态保存/恢复、撤销、重做、宏命令等功能。该模式最典型的应用多在软件用户菜单树开发、事件驱动、数据库日志维护及恢复等将行为作为命令对象的场合。甚至当你为程序加入一个ActionBar.TabListener监听器对象时,框架层也用到了命令模式。

欢迎分享交流,共同进步~

注:欢迎分享,转载请声明~~

版权声明:本文博客原创文章,博客,未经同意,不得转载。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/117718.html原文链接:https://javaforall.cn

0 人点赞