命令(Command)模式

命令模式是一种对象行为型模式,主要用于将请求封装为一个对象,以实现请求的参数化、队列化,支持撤销/重做操作。在系统中,它通过将请求的发起者与执行者解耦,提高了灵活性。适用于需要动态指定请求执行者或需要支持撤销/重做功能的场景。此模式包括Command接口、ConcreteCommand实现类、Invoker请求调用者和Receiver请求接收者四个角色。通过Command模式,可以实现回调函数的面向对象替代,支持在不同时间指定、排列和执行请求,以及构建事务等高级操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

命令(Command)模式

隶属类别——对象行为型模式


1. 意图

将一个请求封装成一个对象,以便使用不同的请求、队列或者日志来参数化(使用统一的语言去调用,例如遥控器插插槽根本不在乎所拥有的是什么命令,只要该命令对象实现了Command接口就可以了)其他对象。命令模式也支持可撤销操作。

2. 别名

动作(Action), 事务(Transaction)

3. 动机

有时必须向某个对象提交请求,但并不知道关于被请求的操作或者请求的接受者的任何信息。

例如,用户界面工具箱包括按钮和菜单这样的对象,它们执行请求响应用户输入。但工具箱不能显式的在按钮或菜单中实现该请求,因为只有使用工具箱的应用才知道由哪个对象做哪些操作。而工具箱的设计者无法知道请求的接受者和执行的操作。

命令模式通过将请求本身变成一个对象来使工具箱的对象可向未指定的应用对象提出请求。这个对象可被存储并向其他的对象一样被传递。这一模式的关键是一个抽象的Command类或者接口,它定义了一个执行操作的接口。 其最简单的形式是一个抽象的Execute操作。 具体的Command实现将真正的接受者(Receiver)作为其一个实例变量(使用委托的方式)。并实现Execute操作,指定接受者采取的动作。而接受者有执行该请求所需的具体信息。
在这里插入图片描述

该应用为每一个菜单项去实现Command接口,该MenuItem使用一个具体的接受者作为构造参数进行构造,且在对应的MenuItem中的Execute()中,采用委托的方式,方法内部调用该菜单命令所应该完成的操作。Execute()中可以可以调用一个或者多个接受者的操作。

有时候一个MenuItem需要执行一系列命令,例如,你需要“Party”MenuItem,你执行这一个命令是,就可以弄暗灯光,打开音响和电视,设置好DVD,并让热水器开始加温。因为这种需要将多条命令串联起来的情况很常见,我们有时候可以定义MenuItem让其成为一个MacroCommand(宏命令),MacroCommand是一个Command的一个实现,它执行了一个命令序列,MacroCommand没有明确的接受者,而序列中的命令都各自定义了其接受者:

在这里插入图片描述

请注意这些例子中Command模式时怎么样解耦调用操作的对象和具有执行该操作所需信息的那个对象的。这使我们在设计用户界面时拥有很大的灵活性(在Menu里面可以命令数组,对其对应的插槽的命令进行设置),一个应用如果想让一个菜单与一个按钮代表同一项功能,只需让它们共享相应具体Command子类的同一个实例即可。我们还可以动态地替换Command对象(如代码示例中的RemoteControljava),这可以用于实现上下文有关的菜单。我们也可通过将几个名字组成更大的命令的形式来支持命令脚本(command scripting).所有这些之所以成为可能乃是因为提交一个请求的对象仅需知道如何提交它,而不需知道该请求将如何执行。

4. 适用性

当你有如下需求时,可使用Command模式:

  • 像上面讨论的MeuItem对象那样,抽象出待执行的动作以参数化某对象。你可用过程语言中的回调(callback)函数表达式这种参数化机制。所谓的回调函数是指某函数先在某处注册,而它将在稍后某个需要的时候被调用。Command模式时回调机制的一个面向对象的代替品。
  • 在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接受者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
  • 支持取消操作 Command的Execute操作可在实施操作前将状态存储起来,在取消操作时,这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这个列表并分别调用undo和execute来实现重数不限的“撤销”和“执行”。
  • 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍 在Command接口中添加中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用execute方法重新执行它们
  • 用来构建在原语操作上的高层操作构造一个系统 这样一种的结构在支持**事务(transaction)**的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command中有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。

5. 结构

在结构类的UML图时,HeadFirst和GOF对于,客户和具体命令类之间的关系结果不一,HeadFirst表明的是associatio(关联),GOF表明的是(依赖),为此,我查询了一些资料:

相对而比较多认同的答案是这个:

In general, you use an association to represent something like a field in a class. The link is always there, in that you can always ask an order for its customer. It need not actually be a field, if you are modeling from a more interface perspective, it can just indicate the presence of a method that will return the order’s customer.

To quote from the 3rd edition of UML Distilled (now just out) “a dependency exists between two elements if changes to the definition of one element (the supplier) may cause changes to the other (the client)”. This is a very vague and general relationship, which is why the UML has a host of stereotypes for different forms of dependency. In code terms, such things as naming a parameter type and creating an object in a temporary variable imply a dependency.

在这里插入图片描述

6. 参与者

  • Command

    ——声明执行操作的接口

  • ConcreteCommand(LightOnCommand,DVDOnCommand,HottobOnCommand)

    ——将一个接受者对象绑定于一个动作

    ——调用接受者相应的操作,以实现execute()

  • Client(Application)

    ——创建一个具体命令对象并设定它的接受者,创建许多命令对象加载到Invoker中

  • Invoker(RemoteControl)

    ——管理一组命令对象,相当于每个按钮都有一个对象。每当按下按钮,就调用对应的按钮相应的命令。

  • Reveiver(Light,DVD,Hottob)

    ——知道如何实施和执行一个请求相关的操作。任何类都可能作为一个接受者

7. 协作

  • Client创建一个ConcreteCommand对象并制定它的Receiver对象

  • 某Invoker对象存储该ConcreteCommand对象。

  • 该Invoker通过调用Command对象的execute操作来提交一个请求。若该命令是可撤销的。ConcreteCommand就在执行Execute操作之前存储当前状态以用于取消该命令。

  • ConcreteCommand对象对调用它的Receiver的一些操作以执行该请求

    下图是展示了这些对象之间的交互的序列图。它说明了Command是如何将调用者和接受者(以及它执行的请求)解耦的:

在这里插入图片描述

8. 效果

Command模式有以下的优点和缺点:

优点:

  • 1.Command模式将调用操作的对象与知道如何实现该操作的对象解耦。
  • 2.ConcreteCommand是头等的对象。它们可像其他的对象一样被操纵和扩展
  • 3.你可以将多个命令封装成一个复合的命令。例如前面的MacroCommand类。一般来说,复合命令是Composite模式的一个实例
  • 4.增加新的ConcreteCommand很容易,因为这个无需改变已有的类。

缺点(其实差不多都是同一点):

  • 类的数目大大增加 完成一个目标需要大量的类和对象协同合作。应用程序开发人员需要小心正确地开发这些类。
  • 提升了实现和维护的工作量 每个单独的命令都是一个ConcreteCommand类,它增加了实现和维护的类的数量。

9. 实现

实现Command模式的时候须考虑以下问题:

  • 1.一个命令对象应该达到何种的智能程度 命令对象的能力可大可小。一个极端是它仅确定一个接受者和执行该请求的动作。另一极端是它自己实现了所有功能。根本不需要额外的接受者对象。当需要定义与已有的类无关的命令,当没有合适的接受者,或当一个命令隐式地知道它的接受者是,可以使用后一极端方式。例如,创建另一个应用窗口的命令对象本省可能和任何其它对象的对象一样有能力创建该接口。在两个极端间的情况时命令对象有足够的信息可以动态的找到它们的接收者。

  • 2.支持取消(undo)和重做(redo) 如果Command提供方法逆转(reverse)它们操作的执行(例如LightOnCommand的undo()),就可以支持取消和重做(两次撤销就相当于重做啊! 仅仅只存储一次操作时)功能。为达到这个目的ConcreteCommand类可能需要存储额外的状态信息。这个状态包括:

    • 接受者对象,它真正执行出该请求的各操作
    • 接受者上执行操作的参数。
    • 如果处理请求的操作会改变接受者对象中某些值,那么这些值也必须先存储起来。接受者还必须提供一些操作,以使该命令可将接受者恢复到它先前的状态

    若应用只支持一次取消操作,那么只需存储最近一次呗执行的命令。而若要支持多级的取消和重做,就需要有一个已被执行命令的历史列表(history list)——感觉可以用队列实现,该列表的最大长度决定了取消和重做的级数。历史列表存储了一倍执行的命令序列。向后遍历该列表并逆向执行(reverse-executing)命令是取消它们的结果,向前遍历并执行命令是重执行它们。

    有时可能不得不将一个可撤销的命令在它可以被放入历史列表中之前先拷贝下来。这是因为执行原来的请求的命令对象将在稍后执行其他的请求。如果命令的状态在各次调用之间会发生变化,那就必须进行拷贝以区分相同的命令的不同的调用。

    例如,一个删除选定对象的删除命令(DeleteCommand)在它每次被执行时,必须存储不同的对象集合。因此该删除命令对象在执行后必须被拷贝,并且将该拷贝放入历史列表中。如果该命令的状态在执行时从不改变,则不需要拷贝,而仅需将一个对该命令的引用放入历史列表。在放入历史李彪中之前必须要备考的那些Command起着原型(Proxytype模式)的作用。

  • 3.避免取消操作过程中的错误累积 在实现一个可靠的,能保持原先语义的取消/重做机制时,可能会遇到滞后影响问题,由于命令重复的执行、取消执行、和重执行的过程可能会积累错误,以至于一个应用的状态最终偏离初始值。这就有必要在Command中存入更多的信息以保证这些对象可被精确地复原成它们初始的状态。这里可以使用Memento(备忘录)模式来让该Command访问这些信息而不暴露其它对象的内部信息。

  • 4.使用C++模板(在Java相当于使用什么呢?又是这个问题。。两次了还没解决,妈耶!) 对于

    • 1.不能被取消

    • 2.不需要参数的命令

      我们可以使用C++模板来实现,这样可以避免为每一个动作和接受者都创建一个Command子类。

  • 5.设置空命令 对于部分Invoker进行初始化时,可以采用空命令,从而确保每个插槽一定都是有引用存在的。空命令的实现如下

    public class NoCommand implements Command{
    	
    	@Override
    	public void execute() {}
    	
    	@Override
    	public void undo() {}
    }
    
    

    NoCommand是一个空对象的(null object)的例子。当你不向返回一个有意义的对象时,空对象也很有用,客户也可以将处理null的责任转移给空对象。举例来说,遥控器不可能一出厂就设置了有意义的命令对象,所以提供了NoCommand对象作为代用品,当调用它的execute()方法时,这种对象就什么事情都不做。

    在许多设计模式中,都会看到空对象的使用。甚至有些时候,空对象本省也被视为一种设计模式。

10. 代码示例

首先是Command——Command.java

public interface Command {
	void execute();
	void undo();
}

然后是ConcreteCommand——LightOnCommand & LightOffCommand & CeilingFanHighCommand & CeilingFanLowCommand & CeilingFanMediumCommand & CeilingFanOffCommand & NoCommand

LightOnCommand.java

public class LightOnCommand implements Command{
	Light light;
	
	public LightOnCommand(Light light) {
		this.light = light;
	}
	
	@Override
	public void execute() {
		light.on();
	}
	
	@Override
	public void undo() {
		light.off();
	}
}

LightOffCommand.java

public class LightOffCommand implements Command {
	Light light ;
	
	public LightOffCommand(Light light) {
		this.light = light;
	}
	
	@Override
	public void execute() {
		light.off();
	}
	
	@Override
	public void undo() {
		light.on();
	}
}

CeilingFanHighCommand.java

public class CeilingFanHighCommand implements Command {
	CeilingFan ceilingFan;
	int preSpeed;
	
	public CeilingFanHighCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
	
	@Override
	public void execute() {
		preSpeed = ceilingFan.getSpeed();
		ceilingFan.high();
	}
	
	@Override
	public void undo() {
		if (preSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (preSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (preSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (preSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

CeilingFanMediumCommand.java

public class CeilingFanMediumCommand implements Command {
	CeilingFan ceilingFan;
	int preSpeed;
	
	public CeilingFanMediumCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
	
	@Override
	public void execute() {
		preSpeed = ceilingFan.getSpeed();
		ceilingFan.medium();
	}
	
	@Override
	public void undo() {
		if (preSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (preSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (preSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (preSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

CeilingFanLowCommand.java

public class CeilingFanLowCommand implements Command {
	CeilingFan ceilingFan;
	int preSpeed;
	
	public CeilingFanLowCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
	
	@Override
	public void execute() {
		preSpeed = ceilingFan.getSpeed();
		ceilingFan.low();
	}
	
	@Override
	public void undo() {
		if (preSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (preSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (preSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (preSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

CeilingFanOffCommand.java

public class CeilingFanOffCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanOffCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	@Override
	public void execute() {
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.off();
	}
 
	@Override
	public void undo() {
		if (prevSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (prevSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (prevSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (prevSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

NoCommand.java

public class NoCommand implements Command{
	
	@Override
	public void execute() {}
	
	@Override
	public void undo() {}
}

接着是Client——RemoteLoader.java

public class RemoteLoader {
 
	public static void main(String[] args) {
		RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
 
		Light livingRoomLight = new Light("Living Room");
 
		LightOnCommand livingRoomLightOn = 
				new LightOnCommand(livingRoomLight);
		LightOffCommand livingRoomLightOff = 
				new LightOffCommand(livingRoomLight);
 
		remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
 
		remoteControl.onButtonWasPushed(0);
		remoteControl.offButtonWasPushed(0);
		System.out.println(remoteControl);
		remoteControl.undoButtonWasPushed();
		remoteControl.offButtonWasPushed(0);
		remoteControl.onButtonWasPushed(0);
		System.out.println(remoteControl);
		remoteControl.undoButtonWasPushed();

		CeilingFan ceilingFan = new CeilingFan("Living Room");
   
		CeilingFanMediumCommand ceilingFanMedium = 
				new CeilingFanMediumCommand(ceilingFan);
		CeilingFanHighCommand ceilingFanHigh = 
				new CeilingFanHighCommand(ceilingFan);
		CeilingFanOffCommand ceilingFanOff = 
				new CeilingFanOffCommand(ceilingFan);
  
		remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
		remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);
   
		remoteControl.onButtonWasPushed(0);
		remoteControl.offButtonWasPushed(0);
		System.out.println(remoteControl);
		remoteControl.undoButtonWasPushed();
  
		remoteControl.onButtonWasPushed(1);
		System.out.println(remoteControl);
		remoteControl.undoButtonWasPushed();
	}
}

接下来是Invoker——RemoteControlWithUndo.java

public class RemoteControlWithUndo {
	Command[] onCommands;
	Command[] offCommands;
	Command undoCommand;
	
	public RemoteControlWithUndo() {
		onCommands = new Command[7];
		offCommands = new Command[7];
		Command noCommand = new NoCommand();
		for (int i = 0; i < 7; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
		undoCommand = noCommand;
	}
	
	public void setCommand(int slot,Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
	
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
	
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = offCommands[slot];
	}
	
	public void undoButtonWasPushed() {
		undoCommand.undo();
	}
}

最后是receiver——Light & CeilingFan

Light.java

public class Light {
	private String name;
	
	public Light(String name) {
		this.name = name;
	}
	
	public void on() {
		System.out.println("Light is on");
	}
	
	public void off() {
		System.out.println("Light is off");
	}
	
	public String toString() {
		return name;
	}
}

CeilingFan.java

public class CeilingFan {
	String location = "";
	int level;
	public static final int HIGH = 3;
	public static final int MEDIUM = 2;
	public static final int LOW = 1;
	public static final int OFF = 0;
	
	public CeilingFan(String location) {
		this.location = location;
	}
  
	public void high() {
		// turns the ceiling fan on to high
		level = HIGH;
		System.out.println(location + " ceiling fan is on high");
 
	} 

	public void medium() {
		// turns the ceiling fan on to medium
		level = MEDIUM;
		System.out.println(location + " ceiling fan is on medium");
	}

	public void low() {
		// turns the ceiling fan on to low
		level = LOW;
		System.out.println(location + " ceiling fan is on low");
	}
 
	public void off() {
		// turns the ceiling fan off
		level = OFF;
		System.out.println(location + " ceiling fan is off");
	}
 
	public int getSpeed() {
		return level;
	}
}

最后放上对应类图的UML:这里解释一下,由于在Client(RemoteLoader中内部进行了测试,随意创建了大量Command),这里加上Client的依赖太乱了,所以就没有显示Client的依赖,另外说了Client与Receiver和ConcreteCommand之间我个人比较认同HeadFirst的观点即使association。
在这里插入图片描述

11. 已知应用

  • 队列请求 命令可以将运算块打包(一个接受者和一组ConcreteCommand)然后传来传去,就像一般的对象一样使用。现在即使在创建命令许久之后,运算依然可以被调用,事实上它甚至可以在不同的线程中被调用,我们可以利用这样的特性衍生出一些应用,例如:日程安排表,线程池,工作队列。
  • 日志请求 某个应用需要我将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态,通过新增的两个方法(store(),load()),命令模式就支持这一点。在Java中,我们可以利用对象的序列化(Serialization)实现这些方法,但一般认为序列化最好还在只有在对象的持久化(persistence).我们应该怎么做呢,当我们执行命令是,将历史记录存储在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批次地依次调用这些对象的execute()方法。这种日志方式对于遥控器来说没有意义,然而,有许多调用大型数据结构的动作的应用无法在每次改变时快速存储。通过使用记录日志,我们可以将上次检查点(checkpoint)之后的所有操作记录下载,如果系统出状况,从检查点开始执行这些操作。比方说,对于电子表格应用,我们可能想要实现的错误恢复方式是将电子表格的操作记录在日志中,而不是每次电子表格一有变化就记录整个电子表格。对更高级别的应用而言,这些技巧可以被扩展到应用到事务(transaction)处理中,也就是说,一整群操作必须全部进行完成,或者没有任何进行的操作

12. 相关应用

  • Composite(组合)模式: 可以使用组合模式来实现宏命令
  • Memento(备忘录)模式: 可用来保持某个状态,命令用这一状态来取消它的效果。
  • Proxytype(原型)模式: 在被放入历史列表前必须被拷贝的命令可以起到一种原型的作用

13. 设计原则口袋

  • 封装变化
  • 多用组合少用继承
  • 针对接口编程,不要针对实现编程
  • 为交互对象的松偶尔设计而努力
  • 类应该对扩展开放,对修改关闭
  • 依赖抽象,不要依赖具体类

14. 参考文献

《HeadFirst设计模式》

《设计模式:可复用面向对象软件的基础》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值