6 封装调用:命令模式

1.引入

通过封装方法调用,可以把运算快包装成形。调用此运算的对象不需要关心事情是如何进行的,只要知道如何使用包装成形的方法来完成它就可以。通过封装方法调用,可以做一些很聪明的事情,例如记录日志,或者重复使用这些封装来实现撤销(undo).

1.1 问题引入

一家公司需要设计一个创新控制器,这个遥控器具有七个可编程的插槽(每个都可以指定到一个不同的家电装置),每个插槽都有对应的开关按钮。这个遥控器还具备一个整体的撤销按钮。

现提供一组类,这些类由多家厂商开发出来的,用来控制家电自动化装置,例如电灯、风扇、热水器、音响设备和其他类似的可控装置。

需求:创建一组控制遥控器的API,让每个插槽都能够控制一个或一组装置。注意的是,能够控制目前的装置和未来任何可能出现的装置。

看看其他厂商的类

 这些类数量不少,并且接口也各有差异,并且这些类以后还会越来越多。

1.2 分析

许多类除了具备on()和off()方法之外,还有其他方法,而且每个类的方法不同,厂商不同。这些都是需要分离的关注点,这很重要:遥控器应该知道如何解读按钮被按下的动作,然后发出正确的请求,但是遥控器不需要知道家电自动化的细节。因此,不需要让遥控器知道太多厂商类的细节,即不想让遥控器包含一大堆if语句,比如“if slot1==Light,then light.on().....”。因为,如果有新的厂商类进来,就必须修改代码,还会造成潜在的错误,而且工作没完没了。

命令模式可以将“动作的请求者”从“动作的执行者”对象中解耦

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

使用这个模式,就能够创建一个 API,将这些命令对象加载到按钮插槽,让遥控器的代码尽量保持简单。而把家电自动化的工作和进行该工作的对象一起封装在命令对象中。

1.3 命令模式的简单介绍

用餐厅的运作来帮助了解命令模式。研究顾客、女招待、订单,以及快餐厨师之间的交互。通过这样的互动,就能体会到命令模式所设计的对象,也会知道他们之间如何被解耦。这样就能帮助解决遥控器的API了。

餐厅的工作方式:

  1. 顾客将订单交给女招待
  2. 女招待拿了订单,放在订单柜台,告知“订单来了”
  3. 快餐厨师根据订单准备餐点

更详细的研究这个交互的过程

  1. createOrder():顾客点餐,订单包含一个订单表格,顾客订购的餐点项目写在上面
  2. takeOrder():女招待拿走了订单,放在订单柜台,然后调用orderUp()方法,通知厨师开始准备餐点
  3. orderUp():订单上有所有准备餐点的指示,指导厨师用类似makeBurgger()等方法来进行烹饪。
  4. output:直接出餐。

餐厅的角色和职责:

一张订单封装了准备餐点的请求把订单想象成一个用来请求准备餐点的对象,和一般的对象一样,订单对象可以被传递:从女招待传递到订单柜台,或者从女招待传递到接替下一班的女招待。订单的接口只包含一个方法,也就是orderUp().这个方法封装了准备餐点所需的动作订单内有一个“需要进行准备工作的对象”(也就是厨师)的引用<见下面的图>。这一切都被封装起来,所以女招待不需要知道订单上有什么,也不需要知道是谁来准备餐点;他只需要将订单放到订饭窗口,然后喊一声“订单来了”就可以了。

<由客户\订单,发出请求:准备XXX餐点,此处的XXX餐点就类似于动作actionX,而制作XXX餐点的厨师,也就是负责接收动作actionX的接受者Receiver>

动作和接受者在命令对象中被绑定在一起。命令对象提供一个方法execute(),这个方法封装了这些动作,调用这个方法就会调用接受者的这些动作。

女招待的工作是接受订单,然后调用订单的orderUp()方法:女招待的工作很简单:接下顾客的订单,继续帮助下一个顾客,然后将一定数量的订单放到订单柜台,并调用orderUp()方法,让人来准备餐点。女招待其实不必担心订单的内容是什么,或者由谁来准备餐点。她只需要知道,订单有一个orderUp()方法可以调用,这就够了。现在,一天内,不同的顾客有不同的订单,这会使得女招待的takeOrder()方法被传入不同的参数。女招待知道所有的订单都支持orderUp()方法,任何时候她需要准备餐点时,调用这个方法就是了。

快餐厨师具备准备餐点的知识快餐厨师是一种对象,他真正知道如何准备餐点。一旦女招待调用orderUp()方法,快餐厨师就接手,实现需要创建餐点的所有方法。请注意,女招待和厨师之间是彻底的解耦:女招待的订单封装了餐点的细节,她只要调用每个订单的方法即可,而厨师看了订单就知道该做些什么餐点;厨师和女招待之间从来不需要直接沟通。

将餐厅想成是OO设计模式的一种模型,而这个模型允许将“发出请求的对象”和“接受与执行这些请求的对象”分隔开。比方说,对于遥控器API,我们需要分隔开“发出请求的按钮代码”和“执行请求的厂商特定对象”。

1.4 从餐厅到命令模式

重新绘制餐厅图以反映出命令模式。所有的角色依然不变,只有名字改变了。

 1.5 遥控器使用的简单测试

开始建立第一个命令对象。虽然还没搞清楚如何设计遥控器的API,但自下而上建造一些东西,可能会有帮助。

实现接口

首先,让所有的命令对象实现相同的包含一个方法的接口。在餐厅的例子中,称该方法为orderUp(),现在改为一般管用的名称execute().

public interface Command {
    public void execute();
}

  实现一个打开电灯的命令

假设想实现一个打开电灯的命令。根据厂商所提供的类,Light类有两个方法:on()和off()。

命令对象:构造函数里面要传入 命令接受者\命令执行者,execute()方法表示的就是命令接受者\命令执行者 所需要执行的命令动作

public class LightOnCommand implements Command {
    Light light;

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

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

这是一个命令,因此需要实现Command接口。

构造器被传入了某个电灯(比方说:客厅的点灯),以便让这个命令控制,然后记录在实例变量中。一旦调用execute(),就由这个电灯对象成为接受者,负责接受请求。 

这个execute()方法调用接收对象(我们正在控制的电灯)的on()方法。

使用命令对象

先简化需求:假设有一个遥控器,它只有一个按钮和对应的插槽,可以控制一个装置。

public class SimpleRemoteControl {

    //有一个插槽持有命令,而这个命令控制着一个装置
    Command slot;

    public SimpleRemoteControl() {}

    //这个方法用来设置:插槽控制的命令。
    //如果这段代码的客户想要改变遥控器按钮的行为,可以多次调用这个方法
    public void setCommand(Command command){
        slot=command;
    }

    //当按下按钮时,这个方法就会被调用,使得当前命令衔接插槽,并调用他的execute()方法。
    public void buttonWasPressed(){
        slot.execute();
    }
}

遥控器是调用者,会传入一个命令对象<由setCommand()负责接收命令对象>,发送请<由按钮按下的动作来执行-buttonWasPressed>。

电灯对象,就是 请求的接受者,并负责执行。

这个模型允许将“发出请求的对象”和“接受与执行这些请求的对象”分隔开

遥控器使用的简单测试

public class RemoteControlTest {

    public static void main(String[] args) {

        //遥控器就是调用者,会传入一个命令对象,可以用来发送请求
        SimpleRemoteControl remote=new SimpleRemoteControl();
        //创建一个电灯对象,就是请求的接受者
        Light light=new Light("bedroom");
        //创建一个命令,将接受者传给电灯对象 light,用来接收命令 相当于--将命令与命令的接受者/执行者绑定
        LightOnCommand lightOn=new LightOnCommand(light);
        //将调用者和命令相绑定,此时命令已经将动作和动作的接受者\执行者绑定了
        remote.setCommand(lightOn);
        //模拟按下按钮,则遥控器将按下信号->命令->命令绑定的执行者
        remote.buttonWasPressed();

        //在上面的步骤中,就将 "发出请求的对象"和"接受与执行这些请求的对象"分隔开了
        //                          遥控器           命令与命令执行者light
    }

}

执行测试代码的输出结果:

 这个模型允许将“发出请求的对象”和“接受与执行这些请求的对象”分隔开

                               请求调用者               请求接受者

 2.定义命令模式

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

一个命令对象通过在特定接受者上绑定一组动作来封装一个请求。要达到这一点,命令对象将动作和接受者包进对象中。这个对象只暴露出一个execute()方法,当此方法被调用时,接受者就会进行这些动作。(参考:LightCommand的构造函数里的light对象,execute()方法里实现将特定的light和Light这个类的on()动作进行绑定,确定特定接受者需要执行的动作)。从外面看,其他对象不知道究竟那个接受者进行了那些动作,只知道如果调用execute()方法,请求的目的就能达到。

我们也看到了利用命令来参数化对象的一些例子。再回到餐厅,一整天下来,女招待参数化许多订单。在简单遥控器中,我们先用一个“打开电灯”命令加载按钮插槽,稍后又将命令替换成为另一个“打开车库门”命令。就和女招待一样,遥控器插槽根本不在乎所拥有的是什么命令对象,只要该命令对象实现了Command接口就可以了。

Invoker:调用者持有一个命令对象,并在某个时间点调用命令对象的execute()方法,将请求付诸实行。 

3.将命令指定到插槽

现在开始设计遥控器的代码,打算将遥控器的每个插槽,对应到一个命令,这样遥控器就变成“调用者”。当按下按钮,相应命令对象的execute()方法就会被调用,其结果就是,接受者(例如:点灯、天花板电扇、音响)的动作被调用。

遥控器实现

public class RemoteControl {

    Command[]  onCommands;
    Command[]  offCommands;

    public RemoteControl(){
        onCommands=new Command[7];
        offCommands=new Command[7];
        //初始化两个开和关的数组,构造的时候不知道要绑定哪个命令,则就用noComaand。
        Command noCommand=new NoCommand();
        for (int i=0;i<7;i++){
            onCommands[i]=noCommand;
            offCommands[i]=noCommand;
        }
    }

    //将请求调用者(遥控器的槽) 与 请求接受者\执行者 进行匹配
    public void setCommand(int slot,Command onCommand,Command offCommand){
        //开和关的命令 要统一设置相同的槽位
        onCommands[slot]=onCommand;
        offCommands[slot]=offCommand;
    }

    //这样就实现了解耦,因为只需要 让其excute()执行即可,而无需关心具体的逻辑
    public void onButtonWasPressed(int slot){
        onCommands[slot].execute();
    }

    public void offButtonWasPressed(int slot){
        offCommands[slot].execute();
    }

    @override
    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();
	}


}

关于上面构造函数中,对开和关命令的初始化,使用NoCommand对象来进行初始化,这样是为了让没有被明确指定命令的插槽,有个默认的值,而不是Null,以免出现问题。

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

NoCommand对象是一个空对象(null object)的例子。当你不想返回一个有意义的对象时,空对象就很有用。客户也可以将处理null的责任转移给空对象。举例来说,遥控器不可能一出厂就设置了有意义的命令对象,所以提供了NoCommand对象作为代用品,当调用它的execute()方法时,这种对象什么事情都不做。
在许多设计模式中,都会看到空对象的使用。甚至有些时候,空对象本身也被视为是一种设计模式。
 

设备相关的开关命令对象

前面设置了LightOnCommand的命令对象,其关闭的命令对象LightOffCommadn也是类似的。下面再来编写音响(Sterero)的开与关的命令:

public class StereoOnWithCDCommand implements Command {

    //命令对象要先明确命令的执行者\接受者
    Stereo stereo;

    public  StereoOnWithCDCommand(Stereo stereo){
        this.stereo=stereo;
    }

    @Override
    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("Garage");
        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);

        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);
        
    }
}

遥控器的测试结果:

 使用命令模式,从逻辑上让遥控器的类和厂商的类解耦。下面的UML类图提供了设计的全貌:

4.撤销功能的实现

4.1 撤销功能的基本实现

现在需要在遥控器上面增加撤销的功能。撤销功能描述:客厅的电灯是关闭的,然后你按下遥控器上的开启按钮,自然电灯就被打开了。现在如果按下撤销按钮,那么上一个动作将被倒转,在这个例子里,电灯将被关闭。在进入更复杂的例子之前,先让撤销按钮能够处理电灯。

实现步骤

1.当命令支持撤销时,该命令必须提供和execute()方法相反的undo()方法。不管execute()刚才在做什么,undo()都会倒转过来。因此,在各个具体命令中加入undo()之前,必须先在Command接口中加入undo()方法:

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

 2.从LightOnCommand开始下手:如果LightOnCommand的execute()方法被调用,那么最后被调用的是on()方法。我们知道undo()方法需要调用off()方法进行相反的操作

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

处理方法很简单,然后再来处理LightOffCommand,此处的undo()方法需要调用点灯的on()方法

public class DimmerLightOffCommand implements Command {
	Light light;
	int prevLevel;

	public DimmerLightOffCommand(Light light) {
		this.light = light;
		prevLevel = 100;
	}

	public void execute() {
		prevLevel = light.getLevel();
		light.off();
	}

	public void undo() {
		light.on( );
	}
}

在对命令对象进行了相关设置之后,还需要更新调用者--遥控器,让遥控器能够追踪最后被按下的按钮是什么--新建对象来绑定撤销请求

3.要加上对撤销按钮的支持,则必须对遥控器类做一些修改。计划修改:加入一个新的实例变量,用来追踪最后被调用的命令,不管何时撤销按钮被按下,我们都可以取出这个命令并调用它的undo()方法。

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) {...}
 
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];//当按钮按下的时候,用undoCommand来记录下这个请求的命令
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = offCommands[slot];//当按钮按下的时候,用undoCommand来记录下这个请求的命令
	}
 
	public void undoButtonWasPushed() {
		undoCommand.undo();
	}
  
	public String toString() {
        //...
	}
}

对上面修改的代码进行测试:

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);

		remoteControl.undoButtonWasPushed();

    }
}

测试结果输出:

 4.2 使用状态实现撤销

通常,想要实现撤销功能,需要记录一些状态。比如厂商类中的天花板上的吊扇。吊扇允许有多种转动速度,也允许被关闭。

吊扇的源码为:

public class CeilingFan {
	public static final int HIGH = 3;
	public static final int MEDIUM = 2;
	public static final int LOW = 1;
	public static final int OFF = 0;
	String location;
	int speed;
 
	public CeilingFan(String location) {
		this.location = location;
		speed = OFF;
	}
  
	public void high() {
		speed = HIGH;
		System.out.println(location + " ceiling fan is on high");
	} 
 
	public void medium() {
		speed = MEDIUM;
		System.out.println(location + " ceiling fan is on medium");
	}
 
	public void low() {
		speed = LOW;
		System.out.println(location + " ceiling fan is on low");
	}
  
	public void off() {
		speed = OFF;
		System.out.println(location + " ceiling fan is off");
	}
  
	public int getSpeed() {
		return speed;
	}
}

加入撤销到吊扇的命令类

将撤销加入天花板吊扇的诸多命令中。这么做,需要追踪吊扇的最后设置速度,如果undo()方法被调用了,就要恢复成之前吊扇速度的设置值。下面是CeilingFanHighCommand的代码:

public class CeilingFanHighCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;//增加局部状态以便追踪吊扇之前的速度
  
	public CeilingFanHighCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	@Override
	public void execute() {
        //在改变吊扇速度之前,要将之前的速度状态记录下来
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.high();
	}
 
	@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();
		}
	}
}

吊扇一共有3个速度档位:low(低速)、medium(中速)、high(高速),还有个就是off(关闭)命令。这些代码为:

public class CeilingFanLowCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanLowCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	public void execute() {
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.low();
	}
 
	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();
		}
	}
}


public class CeilingFanMediumCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanMediumCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	public void execute() {
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.medium();
	}
 
	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();
		}
	}
}

public class CeilingFanOffCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanOffCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	public void execute() {
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.off();
	}
 
	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();
		}
	}
}

接下来,来对天花板吊扇进行测试:

public class RemoteLoader {
 
	public static void main(String[] args) {
		RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();


		CeilingFan ceilingFan = new CeilingFan("Living Room");
   
		CeilingFanMediumCommand ceilingFanMedium = 
				new CeilingFanMediumCommand(ceilingFan);
		CeilingFanHighCommand ceilingFanHigh = 
				new CeilingFanHighCommand(ceilingFan);
		CeilingFanOffCommand ceilingFanOff = 
				new CeilingFanOffCommand(ceilingFan);
  
        //0号槽对应开和关:风扇中档和关闭
		remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
        //1号槽对应开和关:风扇高档和关闭
		remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);
       
		remoteControl.onButtonWasPushed(0);//先开风扇中档
		remoteControl.offButtonWasPushed(0);//在关闭电扇
		System.out.print("undo:  ");
		remoteControl.undoButtonWasPushed();//撤销,返回关闭的上一个状态-中档
  
		remoteControl.onButtonWasPushed(1); //先开风扇高档
		System.out.print("undo:  ");
		remoteControl.undoButtonWasPushed();//撤销,返回高档的上一个状态
	}
}

测试代码输出

4.3 宏命令

需求:有个按钮按下之后,就能够同时弄暗灯光、打开音响和电视,设置好DVD,并让热水器开始加温。

创建宏命令:制造一种新的命令,用来执行其他一堆命令,代码为:

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();
		}
	}
 
    /**
     * 当需要进行撤销操作的时候,直接从最后向前依次调用命令对象的undo()方法即可
     */
	public void undo() {
		for (int i = commands.length -1; i >= 0; i--) {
			commands[i].undo();
		}
	}
}

使用宏命令

1.首先创建想要进入宏的命令集合;

        //1.创建所有的装置,电灯、电视、音响和热水器
    	Light light = new Light("Living Room");
		TV tv = new TV("Living Room");
		Stereo stereo = new Stereo("Living Room");
		Hottub hottub = new Hottub();
 		//2.创建所有的On和off指令来控制它们
		LightOnCommand lightOn = new LightOnCommand(light);
		StereoOnCommand stereoOn = new StereoOnCommand(stereo);
		TVOnCommand tvOn = new TVOnCommand(tv);
		HottubOnCommand hottubOn = new HottubOnCommand(hottub);
		LightOffCommand lightOff = new LightOffCommand(light);
		StereoOffCommand stereoOff = new StereoOffCommand(stereo);
		TVOffCommand tvOff = new TVOffCommand(tv);
		HottubOffCommand hottubOff = new HottubOffCommand(hottub);

2.创建两个数组,其中一个用于记录开启命令,另一个用于记录关闭命令,并在数组中放入相应的命令;

        //3.创建两个数组,一个用来创建开启命令,另一个用来记录关闭命令
		Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn};
		Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff};
  		//4.创建两个对应的宏持有他们。
		MacroCommand partyOnMacro = new MacroCommand(partyOn);
		MacroCommand partyOffMacro = new MacroCommand(partyOff);

3.将宏命令指定给所希望的按钮;

remoteControl.setCommand(0, partyOnMacro, partyOffMacro);

4.最后,按下按钮,测试是否正常工作。

        System.out.println(remoteControl);
		System.out.println("--- Pushing Macro On---");
		remoteControl.onButtonWasPushed(0);
		System.out.println("--- Pushing Macro Off---");
		remoteControl.offButtonWasPushed(0);

测试结果为

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

回答:一般来说,我们尽量设计“傻瓜”命令对象,它只懂得调用一个接受者的一个行为。然后,有许多“聪明”命令对象会实现许多逻辑,直接完成一个请求。当然可以设计聪明的命令对象,但是这样的话,调用者和接受者之间的解耦程度是比不上“傻瓜”命令对象的,而且也不能把接受者当做参数传给命令。

 5.命令模式的更多用途

5.1 队列请求

命令可以将运算块打包(一个接收者和一组动作),然后将它传来传去.就像是一般的对象一样。现在,即使在命令对象被创建许久之后,运算依然可以被调用。事实上,它甚至可以在不同的线程中被调用。我们可以利用这样的特性衍生一些应用,例如:日程安排( Scheduler) 、线程池、工作队列等。

想象有一个工作队列:你在某一端添加命令,然后另一端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute()方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令……

工作队列类和进行计算的对象之间完全是解耦的。此刻线程可能在进行财务运算,下一刻却在读取网络数据。工作队列对象不在乎到底做些什么,它们只知道取出命令对象,然后调用其execute()方法。类似地,它们只要是实现命令模式的对象,就可以放入队列里,当线程可用时,就调用此对象的execute()方法。

5.2 日志请求

某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用这些动作恢复到之前的状态。通过新增两个方法(store()、load()),命令模式就能够支持这一点。在Java中,我们可以利用对象的序列化 (Serialization)实现这些方法,但是一般认为序列化最好还是只用在对象的持久化上 (persistence)。

要怎么做呢?当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,我们就可以将命令对象重新加载,并成批地依次调用这些对象的execute()方法。

这种日志的方式对于遥控器来说没有意义,然而,有许多调用大型数据结构的动作的应用无法在每次改变发生时被快速地存储。通过使用记录日志,我们可以将上次检查点(checkpoint)之后的所有操作记录下来,如果系统出状况,从检查点开始应用这些操作。比方说,对于电子表格应用,我们可能想要实现的错误恢复方式是将电子表格的操作记录在日志中,而不是每次电子表格一有变化就记录整个电子表格。对更高级的应用而言,这些技巧可以被扩展应用到事务(transaction)处理中,也就是说,一整群操作必须全部进行完成,或者没有进行任何的操作。

6.总结

  1. 命令模式将发出请求的对象和执行请求的对象解耦。
  2. 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。
  3. 调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。
  4. 调用者可以接受命令当做参数,甚至在运行时动态地进行。
  5. 命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态。
  6. 宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持销。
  7. 实际操作时,很常见使用“聪明”命令对象,也就是直接实现了请求﹐而不是将工作委托给接收者。
  8. 命令也可以用来实现日志和事务系统。
     
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现电视遥控器命令模式,你可以使用Java中的命令模式设计模式。以下是一个简单的实现示例: 首先,我们需要定义一个命令接口(Command),其中包含一个执行方法execute(),用于执行命令。 ```java public interface Command { void execute(); } ``` 然后,我们可以创建一些具体的命令类,实现命令接口,并在其中定义具体的命令逻辑。 ```java public class TurnOnCommand implements Command { private Television television; public TurnOnCommand(Television television) { this.television = television; } @Override public void execute() { television.turnOn(); } } public class TurnOffCommand implements Command { private Television television; public TurnOffCommand(Television television) { this.television = television; } @Override public void execute() { television.turnOff(); } } // 其他命令类... ``` 接下来,我们需要创建一个电视类(Television),其中包含一些可以被命令调用的方法。 ```java public class Television { public void turnOn() { System.out.println("电视已打开"); // 执行打开电视的逻辑 } public void turnOff() { System.out.println("电视已关闭"); // 执行关闭电视的逻辑 } // 其他电视相关的方法... } ``` 现在,我们可以创建一个遥控器类(RemoteControl),用于执行命令。 ```java public class RemoteControl { private Command command; public void setCommand(Command command) { this.command = command; } public void pressButton() { command.execute(); } } ``` 最后,你可以在主程序中使用这些类来模拟遥控器使用。 ```java public class Main { public static void main(String[] args) { Television television = new Television(); Command turnOnCommand = new TurnOnCommand(television); Command turnOffCommand = new TurnOffCommand(television); RemoteControl remoteControl = new RemoteControl(); remoteControl.setCommand(turnOnCommand); remoteControl.pressButton(); // 打开电视 remoteControl.setCommand(turnOffCommand); remoteControl.pressButton(); // 关闭电视 } } ``` 这就是一个简单的使用Java实现电视遥控器命令模式的示例。你可以根据需要扩展和修改这些类来满足你的具体需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值