初识设计模式 chapter 06-命令模式

初识设计模式 chapter 06-命令模式


1 引言


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

2 正文


2.1 命令模式可能行


本章的需求背景是:有一个电器公司要求开发一款遥控器可以控制家庭电器的开关、音量调节等功能。并且提供了一个可编程的遥控器,一共15个按钮:7对on/off按钮,一个全局撤销(undo)按钮。还有很多家庭电器类,而且以后还会增加。

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

回到餐厅模式,我们都知道餐厅是怎么工作的:
1、你,也就是 顾客,把 订单交给 女招待
2、 女招待拿了 订单,放在订单柜台,然后喊了一声“订单来了”。
3、 快餐厨师根据 订单准备餐点。

把餐厅想成是OO设计模式的一种模型,而这个模型允许将“发出请求的对象”和“接受与执行这些请求的对象”分离开来。比方说,对于遥控器API,我们需要分隔开“发出请求的按钮代码”和“执行请求的厂商特定对象”。万一遥控器的每个插槽都持有一个像餐厅订单那样的对象,会怎么样?那么,当一个按钮被按下,而遥控器不需要知道事情是怎么发生的,也不需要知道涉及哪些对象。

2.2 从餐厅到命令模式


实现命令接口Command

public interface Command {
	public void execute();
}

实现一个打开电灯的命令

//这是一个命令,所以需要实现Command接口
public class LightOnCommand implements Command {
	Light light;
  
	/*
	 * 构造器被传入了某个电灯(比方说客厅的电灯),以便这个命令控制。
	 * 然后记录在实例变量中。一旦弟阿勇execute(),就由这个电灯对象成为接受者,负责接收请求。
	 */
	public LightOnCommand(Light light) {
		this.light = light;
	}
 
	//这个execute()方法调用接受对象(我们正在控制的电灯)的on()方法
	public void execute() {
		light.on();
	}
}

使用命令对象,假设我们有一个遥控器,它只有一个按钮和对应的插槽,可以控制一个装置:

//
// This is the invoker
//
public class SimpleRemoteControl {
	//有一个插槽持有命令,而这个命令控制着一个装置
	Command slot;
 
	public SimpleRemoteControl() {}
 
	//这个方法用来设置插槽控制的命令。如果这段代码的客户想要改变遥控器按钮的行为,可以多次调用这个方法。
	public void setCommand(Command command) {
		slot = command;
	}
 
	//当按下这个按钮时,这个方法就会被调用,使得当前命令衔接插槽,并调用它的execute()方法
	public void buttonWasPressed() {
		slot.execute();
	}
}

遥控器使用的简单测试

//这是命令模式的客户
public class RemoteControlTest {
	public static void main(String[] args) {
		//遥控器就是调用者,会传入一个命令对象,可以用来发出请求。
		SimpleRemoteControl remote = new SimpleRemoteControl();
		//创建一个电灯对象,此对象也就是请求的接受者。
		Light light = new Light();
		GarageDoor garageDoor = new GarageDoor();
		//在这里创建一个命令,然后将接受者传给它
		LightOnCommand lightOn = new LightOnCommand(light);
		GarageDoorOpenCommand garageOpen = 
		    new GarageDoorOpenCommand(garageDoor);
 
		//把命令传给调用者
		remote.setCommand(lightOn);
		//按下按钮
		remote.buttonWasPressed();
		remote.setCommand(garageOpen);
		remote.buttonWasPressed();
    }
}

2.3 定义命令模式


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

实现遥控器

//
// This is the invoker
//
public class RemoteControl {
	//这时候,遥控器要处理7个开与关的命令,使用相应数组记录这些命令
	Command[] onCommands;
	Command[] offCommands;
 
	public RemoteControl() {
		//在构造器中,只需要实例化并初始化两个开与关的数组
		onCommands = new Command[7];
		offCommands = new Command[7];
 		//NoCommand对象是一个空对象(null obeject)的例子。当你不想返回一个有意义的对象时,空对象就很有用,客户也可以将处理null的责任转义给空对象。举例来说,遥控器不可能一出厂就设置了有意义的命令对象,所以提供了NoCommand对象作为替代品,当调用它的execute()		//方法时,这种对象什么也不做。
		Command noCommand = new NoCommand();
		for (int i = 0; i < 7; i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
	}
  
	/*
	 * setCommand()方法须有三个参数,分别是插槽的位置、开的命令、关的命令。
	 * 这些命令将记录在开关数组中对应的插槽位置,以供稍后使用。
	 */
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
 
	//当按下开或关的按钮,硬件就会负责调用对应的方法,也就是onButtonWasPushed()或者offButtonWasPushed()。
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
	}
  
	//覆盖toString(),打印出每个插槽和它对应的命令。稍后在测试遥控器的时候,会用到这个方法。
	public String toString() {
		StringBuffer stringBuff = new StringBuffer();
		stringBuff.append("\n------ Remote Control -------\n");
		for (int i = 0; i < onCommands.length; i++) {
			stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()
				+ "    " + offCommands[i].getClass().getName() + "\n");
		}
		return stringBuff.toString();
	}
}

实现命令

public class StereoOnWithCDCommand implements Command {
	Stereo stereo;
 
	public StereoOnWithCDCommand(Stereo stereo) {
		this.stereo = stereo;
	}
 
	//要实现这个请求,需要调用音响的三个方法:首先打开它,然后把它设置成播放CD,最后把音量设置为11。
	public void execute() {
		stereo.on();
		stereo.setCD();
		stereo.setVolume(11);
	}
}

逐步测试遥控器

public class RemoteLoader {
 
	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
 
		//将所有的装置创建在合适的位置
		Light livingRoomLight = new Light("Living Room");
		Light kitchenLight = new Light("Kitchen");
		CeilingFan ceilingFan= new CeilingFan("Living Room");
		GarageDoor garageDoor = new GarageDoor("");
		Stereo stereo = new Stereo("Living Room");
  
		LightOnCommand livingRoomLightOn = 
				new LightOnCommand(livingRoomLight);
		LightOffCommand livingRoomLightOff = 
				new LightOffCommand(livingRoomLight);
		LightOnCommand kitchenLightOn = 
				new LightOnCommand(kitchenLight);
		LightOffCommand kitchenLightOff = 
				new LightOffCommand(kitchenLight);
  
		CeilingFanOnCommand ceilingFanOn = 
				new CeilingFanOnCommand(ceilingFan);
		CeilingFanOffCommand ceilingFanOff = 
				new CeilingFanOffCommand(ceilingFan);
 
		GarageDoorUpCommand garageDoorUp =
				new GarageDoorUpCommand(garageDoor);
		GarageDoorDownCommand garageDoorDown =
				new GarageDoorDownCommand(garageDoor);
 
		StereoOnWithCDCommand stereoOnWithCD =
				new StereoOnWithCDCommand(stereo);
		StereoOffCommand  stereoOff =
				new StereoOffCommand(stereo);
 
		//现在已经有了全部的命令,可以将他们加载到遥控器插槽中。
		remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
		remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
		remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
		remoteControl.setCommand(3, stereoOnWithCD, stereoOff);
  
		//在这里,使用toString()方法打印出每个遥控器的插槽和它被指定的命令
		System.out.println(remoteControl);
 
		//一切就绪,逐步按下每个插槽的开与关按钮。
		remoteControl.onButtonWasPushed(0);
		remoteControl.offButtonWasPushed(0);
		remoteControl.onButtonWasPushed(1);
		remoteControl.offButtonWasPushed(1);
		remoteControl.onButtonWasPushed(2);
		remoteControl.offButtonWasPushed(2);
		remoteControl.onButtonWasPushed(3);
		remoteControl.offButtonWasPushed(3);
	}
}


2.4 别忘了撤销


重新设计Command接口

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

重新设计命令对象

public class LightOnCommand implements Command {
	Light light;
	int level;
	public LightOnCommand(Light light) {
		this.light = light;
	}
 
	public void execute() {
        level = light.getLevel();
		light.on();
	}
 
	public void undo() {
		light.dim(level);
	}
}

重新设计遥控器

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;
		}
		//初始化的时候,并没有所谓的“前一个命令”,所以将它设置为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();
		/*
		 * 当按下按钮,我们取得这个命令,并优先执行它然后将它记录在nudoConmmand实例变量中。
		 * 不管是开或者关命令,我们的处理方法都是一样的。
		 */
		undoCommand = onCommands[slot];
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = offCommands[slot];
	}
 
	public void undoButtonWasPushed() {
		//当按下撤销按钮,我们调用undoConmmand实例变量的Undo()方法,就可以倒转前一个命令。
		undoCommand.undo();
	}
  
	public String toString() {
		StringBuffer stringBuff = new StringBuffer();
		stringBuff.append("\n------ Remote Control -------\n");
		for (int i = 0; i < onCommands.length; i++) {
			stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName()
				+ "    " + offCommands[i].getClass().getName() + "\n");
		}
		stringBuff.append("[undo] " + undoCommand.getClass().getName() + "\n");
		return stringBuff.toString();
	}
}

2.5 宏命令


public class MacroCommand implements Command {
	Command[] commands;
 
	public MacroCommand(Command[] commands) {
		this.commands = commands;
	}
 
	//当这个宏命令被遥控器执行时,就会一次性执行数组里的每个命令
	public void execute() {
		for (int i = 0; i < commands.length; i++) {
			commands[i].execute();
		}
	}
 
	public void undo() {
		for (int i = commands.length -1; i >= 0; i--) {
			commands[i].undo();
		}
	}
}

2.6 命令模式的高级用途


队列请求和日志请求,关于这两个高级用途实例请阅读其他材料,本书中只略微带过,不过这才是命令模式在实际应用中大展拳脚的地方,后面有机会再深入研究。

3 本章小结


玩过游戏的朋友肯定对宏这个东西不陌生,君不见wower必备的焦点宏,喊话宏,其实都是由一个个命令组成。命令模式在实际项目中的运用也是较多,各位coder有哪些实际经历呢?














  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值