HeadFirst设计模式(六) - 命令模式

命令模式的目的

    咱们将把封装带到一个全新的世界,即把方法调用(method invocation)封装起来。没错,经过封装方法调用,咱们能够把运算块包装成形。因此调用此运算的对象不须要关心事情是如何进行的,只要知道如何使用包装成形的方法来完成它就能够。经过封装方法调用,也能够作一些很聪明的事情,例如记录日志,或者重复使用这些封装来实现撤销(undo)。java

举个例子

    要实现一个多功能的遥控器,这个遥控器具备七个可编程的插槽(每一个均可以指定到一个不一样的加点装置),每一个插槽都有对应的开关按钮。这个遥控器还具有一个总体的撤销按钮。编程

    目前已经有了许多厂商开发出来的Java类,用来控制家电自动化装置,例如电灯、风扇、热水器、音响设备和其余相似的可控制装置。函数

    建立一组控制遥控器的API,让每一个插槽都可以控制一个或一组装置。请注意,可以控制目前的装置和将来可能出现的装置,这一点是很重要的。测试

分析这个例子

    目前已知的类是这些厂家提供的Java类,让咱们来看看他们的设计:this

    固然,类不仅这几个,可是相差不大,许多的类都具备on()和off()方法。但除此以外,还有一些其余方法。并且听起来彷佛未来还会有更多的厂商类,并且每一个类还会有各类各样的新方法。spa

    对于遥控器而言,须要关注的是如何解读按钮被按下的动做,而后发出正确的请求,可是遥控器不须要知道这些家电自动化的细节。线程

    不要让遥控器包含一大堆if语句,例如:设计

if slot1 == Light then light.on()
else if slo1 == .....

    若是这样,只要有新的厂商类进来,就必须修改代码,这会形成潜在的错误,并且工做没完没了。日志

    对于这种状况,咱们就可使用命令模式,命令模式可将“动做的请求者”从“动做的执行者”对象中解耦。在上面的例子中,请求者能够是遥控器,而执行者对象就是厂商类其中之一的实例。利用命令模式,把请求(例如打开电灯)封装成一个特定的对象(例如客厅电灯对象)。因此若是对每一个按钮都存储一个命令对象,那么当按钮被按下的时候,就能够请命令对象作相关的工做。遥控器并不须要知道工做内容是什么,只要有个命令对象能和正确的对象沟通,把事情作好就能够了。code

    说了这么多好像有些混乱,让咱们用代码来实现。

第一个命令对象

    首先,实现命令接口,让全部的命令对象实现相同的包含一个方法的接口:

public interface Command {
	public void execute();
}

    接下来,实现一个打开电灯的命令。根据厂商提供的类,Light类有两个方法,即on()和off()方法。下面是如何将他们实现成一个命令的代码:

/**
 * 打开电灯的命令
 */
public class LightOnCommand implements Command {

	// 电灯对象
	private Light light;

	// 构造函数,传入一个电灯对象
	public LightOnCommand(Light light) {
		this.light = light;
	}
	
	// 命令方法的执行函数,这里将打开电灯
	public void execute() {
		light.on();
	}

}

    让咱们看看这个类都作了那些事情:

  1. 该类实现了Command接口,如今它是一个命令对象;
  2. 构造函数要求该类在实例时要传入某个电灯(比方说客厅的电灯),以便让这个命令控制,而后记录在实例变量light中。一旦调用execute(),就有这个电灯对象成为接收者负责接受请求;
  3. execute(0方法调用接收对象的on()方法;

    如今有了LightOnCommand类,让咱们看看如何使用它。

使用命令对象

    建立一个遥控器类,它目前只有一个按钮和对应的插槽,能够控制一个装置:

public class SimpleRemoteControl {
	// 只有一个插槽
	Command slot;
	
	// 构造函数
	public SimpleRemoteControl() {}
	
	// 设置插槽要执行的命令
	public void setCommand(Command command) {
		this.slot = command;
	}
	
	// 按下遥控器的按钮,这个方法就会被调用
	public void buttonWasPressed() {
		slot.execute();
	}
}

    该类有一个成员变量slot,它是类型是一个Command接口,在实例化这个类时须要被传入。而后在调用buttonWasPressed()方法时,会调用这个命令接口的execute()方法。如今,对这个例子进行测试:

public class Client {

	public static void main(String[] args) {
		// 建立一个遥控器
		SimpleRemoteControl remote = new SimpleRemoteControl();

		// 建立一个电灯对象
		Light light = new Light();
		// 建立一个开灯命令对象,将电灯传入给它
		LightOnCommand lightOn = new LightOnCommand(light);

		// 将该命令输入到遥控器的按钮中
		// 遥控器只有一个按钮,按下这个按钮就会执行lightOn命令对象的execute()方法
		remote.setCommand(lightOn);

		// 按下遥控器按钮
		remote.buttonWasPressed();

		/**
		 * output:打开电灯.
		 */
	}

}

命令模式的定义

命令模式将“请求”封装成对象,以便使用不一样的请求、队列或者日志来参数化其余对象。命令模式也支持可撤销的操做。

    如今,仔细看这个定义。咱们知道一个命令对象经过在特定接收者上绑定一组动做来封装一个请求。要打到这一点,命令对象将动做和接收者包装进对象中。这个对象只暴露出一个execute()方法,当此方法被调用的时候,接收者就会进行这些动做。从外面看来,其余对象不知道究竟哪一个接收者进行了哪些动做,只知道若是调用execute()方法,请求的目的就能达到。

命令模式的类图

    若是将上面例子中的类套用到该类图中的话,具体以下:

  • Command接口就是例子中的Command接口;
  • ConcreteCommand(具体命令)类就是例子中的LightOnCommand类;
  • Receiver(接收者)类就是例子中的Light类;
  • Invoker(调用者)就是例子中的SimpleRemoteControl类;
  • Client就是例子中用于测试的Client类;

完成这个遥控器例子

首先编写Receiver类:

public class Light {
	public void on() {
		System.out.println("打开电灯.");
	}
	
	public void off() {
		System.out.println("关闭电灯.");
	}
}

public class Stereo {
	public void on() {
		System.out.println("打开音响.");
	}
	
	public void off() {
		System.out.println("关闭音响.");
	}
}

public class TV {
	public void on() {
		System.out.println("打开电视.");
	}
	
	public void off() {
		System.out.println("关闭电视.");
	}
}

接着编写Command接口:

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

接着编写ConcreteCommand类:

public class NoCommand implements Command {

	public void execute() {
		System.out.println("按钮无效.");
	}

}

public class LightOnCommand implements Command {

	// 电灯对象
	private Light light;

	// 构造函数,传入一个电灯对象
	public LightOnCommand(Light light) {
		this.light = light;
	}
	
	// 命令方法的执行函数,这里将打开电灯
	public void execute() {
		light.on();
	}
	
	public void undo(){
		light.off();
	}
}

public class LightOffCommand implements Command {
	// 电灯对象
	private Light light;

	// 构造函数,传入一个电灯对象
	public LightOffCommand(Light light) {
			this.light = light;
		}

	// 命令方法的执行函数,这里将关闭电灯
	public void execute() {
		light.off();
	}
	
	public void undo(){
		light.on();
	}
}

public class StereoOnCommand implements Command {
	// 音响对象
	private Stereo stereo;

	// 构造函数,传入一个音响对象
	public StereoOnCommand(Stereo stereo) {
		this.stereo = stereo;
	}

	// 命令方法的执行函数,这里将打开音响
	public void execute() {
		stereo.on();
	}
	
	public void undo(){
		stereo.off();
	}
}

public class StereoOffCommand implements Command {
	// 音响对象
	private Stereo stereo;

	// 构造函数,传入一个音响对象
	public StereoOffCommand(Stereo stereo) {
		this.stereo = stereo;
	}

	// 命令方法的执行函数,这里将关闭音响
	public void execute() {
		stereo.off();
	}
	
	public void undo(){
		stereo.on();
	}
}

public class TVOnCommand implements Command {
	// 电视对象
	private TV tv;

	// 构造函数,传入一个电视对象
	public TVOnCommand(TV tv) {
		this.tv = tv;
	}

	// 命令方法的执行函数,这里将打开电视
	public void execute() {
		tv.on();
	}
	
	public void undo(){
		tv.off();
	}
}

public class TVOffCommand implements Command {
	// 电视对象
	private TV tv;

	// 构造函数,传入一个电视对象
	public TVOffCommand(TV tv) {
			this.tv = tv;
		}

	// 命令方法的执行函数,这里将关闭电视
	public void execute() {
		tv.off();
	}
	
	public void undo(){
		tv.on();
	}
}

接着编写Invoker类:

public class RemoteControl {
	// on的命令组
	private Command[] onCommands;
	// off的命令组
	private Command[] offCommands;
	// 撤销命令
	private Command undoCommand;

	public RemoteControl() {
		// 初始化时将命令都设置成noCommand
		onCommands = new Command[3];
		offCommands = new Command[3];

		Command noCommand = new NoCommand();
		for (int i = 0; i < 3; 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实例变量中。
		// 不论是开仍是关,咱们处理方法都是同样的。
		undoCommand = onCommands[slot];
	}

	public void offbuttonWasPushed(int slot) {
		offCommands[slot].execute();
		// 当按下按钮时,取得这个命令,记录在undoCommand实例变量中。
		// 不论是开仍是关,咱们处理方法都是同样的。
		undoCommand = offCommands[slot];
	}

	public void undoButtonWasPushed() {
		undoCommand.undo();
	}
}

最后编写测试代码:

public class Client {

	public static void main(String[] args) {
		// 建立遥控器,也就是Invoker
		RemoteControl remote = new RemoteControl();
		
		// 建立具体的设备,也就是Receiver
		Light light = new Light();
		TV tv = new TV();
		Stereo stereo = new Stereo();
		
		// 建立具体的命令,也就是ConcreteCommand
		// 开灯关灯的命令
		LightOnCommand lightOn = new LightOnCommand(light);
		LightOffCommand lightOff = new LightOffCommand(light);
		// 开电视关电视的命令
		TVOnCommand tvOn = new TVOnCommand(tv);
		TVOffCommand tvOff = new TVOffCommand(tv);
		// 开音响关音响的命令
		StereoOnCommand stereoOn = new StereoOnCommand(stereo);
		StereoOffCommand stereoOff = new StereoOffCommand(stereo);
		
		// 将命令装载到遥控器中
		remote.setCommand(0, lightOn, lightOff);
		remote.setCommand(1, tvOn, tvOff);
		remote.setCommand(2, stereoOn, stereoOff);
		
		// 测试开的按钮
		remote.onButtonWasPushed(0);
		remote.onButtonWasPushed(1);
		remote.onButtonWasPushed(2);
		
		// 测试关的按钮
		remote.offbuttonWasPushed(0);
		remote.offbuttonWasPushed(1);
		remote.offbuttonWasPushed(2);
		
		// 测试撤销的按钮
		// 最后按的是关闭音响,那么执行该方法后,音响会开启
		remote.undoButtonWasPushed();
	}

}

输出结果:

打开电灯. // 测试on按钮
打开电视.
打开音响.
关闭电灯. // 测试off按钮
关闭电视.
关闭音响.
打开音响. // 测试undo按钮

到此位置,测试结束!

关于一些细节

问:接收者必定有必要存在吗?为什么命令对象不直接实现execute()方法的细节?

答:通常来讲,尽可能设计傻瓜命令对象,它只懂得调用一个接收者的一个行为(单一职责)。然而,有许多“聪明”的命令对象会实现许多逻辑,直接完成一个请求,但耦合程度高。

问:如何可以实现多层次的撤销操做?但愿按下撤销按钮许屡次,回到很早之前状态。

答:不要只是记录最后一个命令,而使用一个堆栈(后进先出)记录操做过的每个命令。而后,无论何时按下了撤销按钮,你均可以从堆栈中取出最上层的命令,而后执行undo()方法来撤销它。

最后说一些其余的,命令模式还能够有更多的用途,好比使用在队列请求和日志请求。

想象一下,有一个工做队列(先进先出),你在某一端添加命令,而后另外一端则是线程,线程从队列中取出一个命令,而后调用它的execute()方法,等待这个调用完成,而后丢弃该命令,执行下一个……

在想象一下,某些应用须要咱们将全部的动做都记录在日志中,并能在系统死机后,从新调用这些动做恢复到以前的状态。咱们能够在执行命令的时候,将历史记录存储在磁盘中。一旦系统死机重启后,咱们就能够将命令对象读取出来从新执行execute()方法。

命令模式能用到地方还有不少,目前就记录到这里。

相关文章
相关标签/搜索