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

命令模式的目的

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

举个例子

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

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

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

分析这个例子

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

    当然,类不只这几个,但是相差不大,许多的类都具有on()和off()方法。但除此之外,还有一些其他方法。而且听起来似乎将来还会有更多的厂商类,而且每个类还会有各种各样的新方法。

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

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

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

    如果这样,只要有新的厂商类进来,就必须修改代码,这会造成潜在的错误,而且工作没完没了。

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

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

第一个命令对象

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

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()方法。

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

转载于:https://my.oschina.net/u/2450666/blog/708048

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值