Java设计模式-命令模式

命令(Command)模式,有时也被称为动作(Action)模式或者事务(Transaction)模式,在23种设计模式中属于对象行为型模式。

命令模式将请求封装成对象,以便使用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

模式介绍

在开发中,请求响应的场景非常常见,一般会把请求的响应操作封装到一个方法中,比如在Servlet中可以把这种操作放在doGet()或者doPost()方法中,但是这种实现请求响应的操作并不是命令模式。

命令模式是要解决这样一类问题的,某些对象需要发送请求(调用其中的某个或者某些方法),但是并不知道请求的执行者是谁,也不知道被请求的操作的是哪个,如何可以将请求的发送与请求的执行者以一种低耦合的方式设计,让对象的调用关系更加灵活,命令模式提供了一种比较完美的解决方式。

一般命令模式的请求响应会涉及三个操作类,这些类都是封装对象行为的类,而非简单的属性类(比方说JavaBean、Domain或者Model)。请求类会将自己的行为通知到命令类中,然后命令类将具体的命令转发到请求的执行类中,最后由执行类完成本次的调用过程。

适用性

  • 系统需要将发出请求的对象和执行请求的对象进行解耦时,调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要将一组操作组合在一起,即支持宏命令。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
  • 可以作为面向对象回调机制Callback的一个替代。

示例介绍一

这里以一个点灯开关的示例做介绍,实现一个打开点灯的命令。先抽象一个包含共有方法的命令接口。

public interface Command {
	/**
	 * 执行方法
	 */
	void execute();
}

然后实现一个LightOnCommand的命令。

public class LightOnCommand implements Command {
	private Light light;

	public LightOnCommand(Light light) {
		this.light = light;
	}

	@Override
	public void execute() {
		light.lightOn();
	}
}

假设有这样一个开关,它可以控制电灯的开启操作。

public class SimpleRemoteControl {
	private Command command;

	public SimpleRemoteControl(Command command) {
		this.command = command;
	}

	public void buttonWasPressed() {
		command.execute();
	}
}
public static void main(String[] args) {
	// 创建接收者
	Light light = new Light();
	// 创建命令对象,设定其接收者
	Command command = new LightOnCommand(light);
	// 创建请求者,把命令对象设置进去
	SimpleRemoteControl controll = new SimpleRemoteControl(command);
	// 执行方法
	controll.buttonWasPressed();
}
//Light is on

示例介绍二

将上面的示例抽象为命令模式的一般模式。

// 电灯对象
public class Receiver {
	/**
	 * 真正执行命令请求的操作
	 */
	public void action() {
		System.out.println("执行操作");
	}
}
// LightOnCommand命令对象
public class ConcreteCommand implements Command {
	/**
	 * 持有相应的接收者对象
	 */
	private Receiver receiver;

	/**
	 * 构造方法
	 * 
	 * @param receiver
	 */
	public ConcreteCommand(Receiver receiver) {
		this.receiver = receiver;
	}

	@Override
	public void execute() {
		// 通常会转调接收者的形影方法,让接收者来真正执行功能
		receiver.action();
	}
}
// 电源开关SimpleRemoteControl
public class Invoker {
	/**
	 * 持有命令对象
	 */
	private Command command;

	/**
	 * 构造方法
	 * 
	 * @param command
	 */
	public Invoker(Command command) {
		this.command = command;
	}

	/**
	 * 行动方法
	 */
	public void action() {
		command.execute();
	}
}
public static void main(String[] args) {
	// 创建接收者
	Receiver receiver = new Receiver();
	// 创建命令对象,设定其接收者
	Command command = new ConcreteCommand(receiver);
	// 创建请求者,把命令对象设置进去
	Invoker invoker = new Invoker(command);
	// 执行方法
	invoker.action();
}

其它示例

在多名命令或者说宏命令的场景中,在请求操作中持有的命令对象有可能会为null,那么每一个操作之前都需要对命令对象进行判空,代码如下:

public void onButtonWasPushed(int slot) {
	if (onCommands[slot] != null) {
		onCommands[slot].execute();
	}
}

其实上面这种代码实现方式在平常开发中很常见,为了避免每一个请求者都对所持有的命令对象进行判空操作,一般开发中会提供一个什么都不执行的默认空命令对象。

public class NoCommand implements Command {
	@Override
	public void execute() {
		System.out.println("no action");
	}
}
public RemoteControl() {
	onCommands = new Command[2];
	offCommands = new Command[2];
	Command noCommand = new NoCommand();
	for (int i = 0; i < 2; i++) {
		onCommands[i] = noCommand;
		offCommands[i] = noCommand;
	}
}

NoCommand就是一个空对象,当不想返回任何操作,或者想提供默认操作时,这种空对象的实现方式就很有用。在许多设计模式中,都可以看到空对象的使用,甚至有些时候,空对象本身就被看做是一种设计模式。

模式分析

命令模式的一般类图如下:

  • Command:抽象命令,一般设计成接口的形式,顶层执行操作的接口。
  • ConcreteCommand:实现execute()方法,负责调用接收者的相应操作。
  • Invoker:负责调用命令对象执行请求操作。
  • Receiver:负责具体的执行请求的类。
  • Client:创建一个具体的命令对象ConcreteCommand,并确定其的接收者。

命令模式的执行请求流程一般为:Invoker->Command->Receiver。

命令模式中,一般请求对象(Invoker)持有一个Command对象引用,Command持有一个Receiver对象的引用,执行对象Receiver并不会持有Command或者Invoker。

命令模式通过增加了一个中间层(Command),将请求的对象(Invoker)和执行对象(Receiver)进行了解耦。被解耦的两者之间是通过命令对象进行通信的。命令对象封装了接收者的一个或者一组动作。

// 一个动作 开灯 public void execute() { light.lightOn(); } // 一组动作 音响播放CD public void execute() { stereo.on(); stereo.setDVD(); stereo.setVolume(11); }

命令模式中Command封装的是操作,即动作方法,而不是属性字段。一般来说一个命令对象只对应一个接收者。

命令模式中接收者(Receiver)不是必需的,即直接使用命令完成请求执行的的操作。只是这样实现的解耦程度不如使用接收者的结构。

命令的调用者(Invoker)通过调用命令对象的execute()方法发出请求,最终会使接收者的动作被调用。

命令的调用者可以通过接收命令当做参数,甚至可以在运行时动态进行。

命令可以支持撤销操作,做法是实现一个undo()方法来回到execute()被执行之前的状态,这里的undo操作是在命令对象中操作的,并不是在接受者中实现的。

命令模式支持宏命令,宏命令时命令的一种延伸,运行调用多个方法。宏方法也支持撤销。

模式优缺点

模式优点

命令模式将发出请求的对象与执行请求的对象进行了解耦。

可以新增Command类,而无需更改已有的类。

可以将多个命令组合成一个复合命令,如命令队列或者宏命令。

可以很方便地对请求执行取消操作。

模式缺点

命令模式可能导致系统会有许多的命令类。由于每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。

与其它模式对比

命令模式通过命令对象将请求者与执行者绑定在了一起,如下图所示:

从Command模式的时序图中也可以很明显看出,Command是作为一个中间者的身份,请求和响应并不是直接相互调用,而是通过Command建立联系。

在《重构改善既有代码的设计》一书中有这样一句描述。

计算机科学是这样一门科学:它相信所有的问题都可以通过一个间接层来解决。

在很多代码重构中都会引入一个间接层,其实间接层的引入就是为了降低请求与相应之间的耦合。

在Command设计模式中,Command提供的是一个提交请求的接口

在观察者模式中时序图如下:

观察者和被观察者其实是直接相互关联调用的,并且一个观察者可以同时关联多个观察者。

在观察者模式中,接口是为了处理被观察者的变化而设计的,耦合性相比命令模式也更高一些。

小结

在命令模式中也充分体现了面向对象的设计模式的通病,类的膨胀,大量衍生类的创建,这是一个不可避免的问题。但是,它带来的好处也是显而易见的,更低的耦合性、更灵活的控制性以及更好的扩展性。

在学习设计模式时,其实见到的例子都是很简单的,基本就是为了介绍某种设计模式而想出来的,而实际工作的的场景都是相当复杂的,有时候可能还是需要兼顾老代码的实现。所以平常在开发时,某些设计模式很难与书上的例子匹配开来,这也是为什么找不出应该使用哪种设计模式的原因。

最后了,一句话,不能为了学习设计模式而学习设计模式。学习设计模式不仅是为了更好的理解别人代码的实现意图,更多的也是让自己的代码更容易被别人理解。

示例源代码下载 提取码:hfng

评论

您确定要删除吗?删除之后不可恢复