訊息人士:SK 海力士 90 億美元收購英特爾快閃記憶體業務計劃有望獲中國批准
命令模式將“請求”封裝成物件,以便使用不同的請求、佇列或者日誌來引數化其他物件。命令模式也支援可撤銷的操作。
設計自動化遙控器的API。這個遙控器有7個可程式設計的插槽,每個都可以指定到一個不同的家電裝置。每個插槽都有對應的“開啟”和“關閉”按鈕。這個遙控器還具備一個整體的撤銷按鈕。
你要做的是建立一組控制遙控器的API,讓每個插槽都能夠控制一個或一組裝置。請注意,要能夠控制目前的裝置和任何未來可能出現的裝置,這一點很重要。
產商的類
public class TV { public void on(){} public void off(){} public void setInputChannel(){} public void setVolume(){} } public class CeilingFan { public void high(){} public void medium(){} public void low(){} public void off(){} public void getSpeed(){} } public class OutDoorLight { public void on(){} public void off(){} }
如何設計遙控器的API?
** 要怎麼將遙控器和產商類的例項解耦?可以考慮使用命令模式。**命令模式可將“動作的請求者”從“動作的執行者”物件中解耦。在這個例子裡,請求者是遙控器,而執行者物件就是產商類其中之一的例項。
我們可以把請求(例如開啟電燈)封裝成一個特定物件(例如客廳電燈物件)。所以,如果對每個按鈕都儲存一個命令物件,那麼當按鈕被按下的時候,就可以請命令物件做相關的工作。遙控器並不需要知道工作內容是什麼,只要有個命令物件能和正確的物件溝通,把事情做好就可以了。所以,遙控器和電燈物件解耦了。
進一步理解命令模式
我們都知道餐廳是怎麼工作的:
- 顧客把訂單交給服務員;
- 服務員拿到訂單後,放在訂單櫃檯,然後告訴廚師:“訂單來了!”;
- 廚師根據訂單準備餐點。
讓我們更詳細地研究下這個互動過程:
一張訂單封裝了準備餐點的請求
把訂單想象成一個用來請求準備餐點的物件,和一般的物件一樣,訂單物件可以被傳遞:從服務員傳遞到訂單櫃檯,或者從一個服務員傳遞給下一班的服務員。訂單的介面只包含一個方法,這個方法封裝了準備餐點所需的動作。訂單內有一個到“需要進行準備工作的物件”(也就是廚師)的引用。這一切都被封裝起來,所以服務員不需要知道訂單上有什麼,也不需要知道是誰來準備餐點;她只需要將訂單放到訂單視窗,然後喊一聲“訂單來了!”就可以了。
服務員的工作是接受訂單,然後呼叫訂單物件的方法
服務員的工作很簡單:接下顧客的訂單,繼續幫助下一個顧客,然後將一定數量的訂單放到訂單櫃檯,並呼叫訂單物件的方法,讓廚師來準備餐點。服務員不需要擔心訂單的內容是什麼,或者由誰來準備餐點。她只需要知道,訂單有個準備餐點的方法可以呼叫,這就夠了。
廚師具備準備餐點的知識
廚師是一種物件,他真正知道如何準備餐點。一旦服務員呼叫準備餐點的方法,廚師就接手,實現建立餐點的所有方法。請注意,服務員和廚師之間是徹底的解耦:服務員的訂單封裝了餐點的細節,她只要呼叫每個訂單的方法即可,而廚師看了訂單就知道該做些什麼餐點;廚師和服務員之間從來不需要直接溝通。
把餐廳想成是OO設計模式的一種模型,而這個模型允許將“發出請求的物件”和“接受與執行這些請求的物件”分割開來。比方說,對於遙控器API,我們需要分割開“發出請求的按鈕程式碼”和“執行請求的產商特定物件”。假如遙控器的每個插槽都持有一個像餐廳訂單那樣的物件,會怎麼樣?那麼,當一個按鈕被按下,只要呼叫該物件的方法,電燈就開了,而遙控器不需要知道事情是怎麼發生的,也不需要知道涉及哪些物件。
實現一個命令物件
現在讓我們開始建立第一個命令物件。
實現命令介面
首先,讓所有的命令物件實現相同的包含一個execute()方法的介面:
public interface Command {
public void execute();
}
實現一個開啟電燈的命令
現在,假設想實現一個開啟電燈的命令。根據產商提供的類,Light類有兩個方法:on()和off()。下面是如何將它實現成一個命令:
// 這是一個命令,所以需要實現Command介面
public class LightOnCommand implements Command {
Light light;
// 構造器被傳入了某個電燈(比方說:客廳的電燈),然後記錄在例項變數中。
// 一旦呼叫execute(),就由這個電燈物件成為接收者,負責接收請求。
public LightOnCommand(Light light){
this.light = light;
}
// 這個execute()方法呼叫接收物件(我們正在控制的電燈)的on()方法
@Override
public void execute() {
light.on();
}
}
使用命令物件
讓我們把這一切簡化,假設我們的遙控器只有一個按鈕和對應的插槽,可以控制一個裝置:
public class SimpleRemoteControl {
// 有一個插槽持有命令,而這個命令控制著一個裝置
Command slot;
public SimpleRemoteControl(){}
// 這個方法用來設定插槽控制的命令。如果這段程式碼的客戶想要改變遙控器按鈕的行為,
// 可以多次呼叫這個方法。
public void setCommand(Command command){
slot = command;
}
// 當按下按鈕時,這個方法就會被呼叫,使得當前命令銜接插槽,並呼叫它的execute()方法
public void buttonWasPressed(){
slot.execute();
}
}
遙控器使用的簡單測試
下面這段程式碼用來測試上面的簡單遙控器:
// 控制器就是呼叫者,會傳入一個命令物件,可以用來發出請求
SimpleRemoteControl remote = new SimpleRemoteControl();
// 現在建立了一個電燈物件,此物件也就是請求的接收者
Light light = new Light();
// 在這裡建立一個命令,然後將接收者傳給它
LightOnCommand lightOn = new LightOnCommand(light);
// 把命令傳給呼叫者
remote.setCommand(lightOn);
// 模擬按下按鈕
remote.buttonWasPressed();
設計類圖
定義命令模式
命令模式將“請求”封裝成物件,以便使用不同的請求、佇列或者日誌來引數化其他物件。命令模式也支援可撤銷的操作。
我們知道一個命令物件通過在特定接收者上繫結一組動作來封裝一個請求,要達到這一點,命令物件將動作和接收者包進物件中。這個物件只暴露出一個execute()方法,當此方法被呼叫的時候,接收者就會進行這些動作。從外面來看,其他物件不知道究竟哪個接收者進行了哪些動作,只知道如果呼叫execute()方法,請求的目的就能達到。
我們也看到了利用命令來引數化物件的一些例子。再回到餐廳,一整天下來,服務員引數化了許多訂單。在簡單遙控器中,我們用一個“開啟電燈”命令載入按鈕插槽。就和服務員一樣,遙控器插槽根本不在乎所擁有的是什麼命令物件,只要該物件實現了Command介面就可以了。
我們還未說到使用命令模式來實現“佇列、日誌和支援撤銷操作”。這是基本命令模式相當直接的擴充套件,稍後就會看到這些內容。
實現遙控器
現在讓我們去實現有7個插槽的遙控器。
public class RemoteControl {
// 這個時候,遙控器要處理7個開與關的命令,使用相應陣列記錄這些命令
Command[] onCommands;
Command[] offCommands;
// 在構造器中,只需例項化並初始化這兩個開與關的陣列
public RemoteControl() {
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;
}
}
// setCommand()方法有3個引數,分別是插槽的位置、開的命令和關的命令。
// 這些命令將記錄在開關陣列中對應的插槽位置,以供稍後使用
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
// 當按下開或關的按鈕,硬體就會負責呼叫對應的方法
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
}
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
}
}
加上撤銷功能
好了,我們現在需要在遙控器上加上撤銷的功能。這個功能使用起來就像是這樣的:比如說客廳的電燈是關閉的,然後你按下遙控器上的開啟按鈕,自然電燈就被打開了。現在如果按下撤銷按鈕,那麼上一個動作將被倒轉,在這個例子裡,電燈將被關閉。
當命令支援撤銷時,該命令就必須提供和execute()方法相反的undo()方法。不管execute()剛才做什麼,undo()都會倒轉過來。我們需要在Command介面中加入undo()方法。
public interface Command {
public void execute();
public void undo();
}
現在讓我們深入電燈的命令,並實現undo()方法。
public class LightOnCommand implements Command {
Light light;
public LightOnCommand(Light light){
this.light = light;
}
@Override
public void execute() {
light.on();
}
// execute開啟電燈,所以undo()做的事情就是關閉電燈。
@Override
public void undo() {
light.off();
}
}
要加上撤銷按鈕,必須對遙控器做一些小修改。
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
// 前一個命令將被記錄在這裡
Command undoCommand;
public RemoteControl() {
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;
}
// 當按下按鈕,我們取得這個命令,並優先執行它,然後將它記錄在undoCommand中。
public void onButtonWasPushed(int slot){
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void offButtonWasPushed(int slot){
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
// 當按下撤銷按鈕,我們呼叫undoCommand例項變數的undo()方法,就可以倒轉前一個命令
public void undoButtonWasPushed(){
undoCommand.undo();
}
}
public class RemoteLoader {
public static void main(String[] args) {
RemoteControlWithUndo remoteControl = new RemoteControlWithUndo();
// 建立一個電燈物件
Light livingRoomLight = new Light("Living Room");
// 建立支援undo功能的命令
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();
}
}
使用狀態實現撤銷
吊扇
public class CeilingFan {
String location = "";
int speed;
public static final int HIGH = 3;
public static final int MEDIUM = 2;
public static final int LOW = 1;
public static final int OFF = 0;
public CeilingFan(String location) {
this.location = location;
speed = OFF;
}
public void high() {
// turns the ceiling fan on to high
speed = HIGH;
System.out.println(location + " ceiling fan is on high");
}
public void medium() {
// turns the ceiling fan on to medium
speed = MEDIUM;
System.out.println(location + " ceiling fan is on medium");
}
public void low() {
// turns the ceiling fan on to low
speed = LOW;
System.out.println(location + " ceiling fan is on low");
}
public void off() {
// turns the ceiling fan off
speed = OFF;
System.out.println(location + " ceiling fan is off");
}
public int getSpeed() {
return speed;
}
}
加入撤銷到吊扇的命令類
public class CeilingFanHighCommand implements Command {
CeilingFan ceilingFan;
final LinkedList<Integer> speedStack;
public CeilingFanHighCommand(CeilingFan ceilingFan, LinkedList<Integer> speedStack) {
this.ceilingFan = ceilingFan;
this.speedStack = speedStack;
}
@Override
public void execute() {
speedStack.addLast(ceilingFan.getSpeed());
ceilingFan.high();
}
@Override
public void undo() {
if (speedStack.isEmpty()) {
ceilingFan.off();
return;
}
Integer prevSpeed = speedStack.removeLast();
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");
LinkedList<Integer> stack = new LinkedList<Integer>();
CeilingFanLowCommand ceilingFanLow =
new CeilingFanLowCommand(ceilingFan, stack);
CeilingFanMediumCommand ceilingFanMedium =
new CeilingFanMediumCommand(ceilingFan, stack);
CeilingFanHighCommand ceilingFanHigh =
new CeilingFanHighCommand(ceilingFan, stack);
CeilingFanOffCommand ceilingFanOff =
new CeilingFanOffCommand(ceilingFan, stack);
remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff);
remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff);
remoteControl.setCommand(2, ceilingFanLow, ceilingFanOff);
System.out.println(remoteControl);
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
remoteControl.onButtonWasPushed(1);
remoteControl.onButtonWasPushed(2);
remoteControl.undoButtonWasPushed();
remoteControl.undoButtonWasPushed();
remoteControl.undoButtonWasPushed();
}
}
巨集命令
一個按鈕執行多個命令
Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn};
Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff};
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
remoteControl.setCommand(0, partyOnMacro, partyOffMacro);
public class MacroCommand implements Command{
Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for (int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
@Override
public void undo() {
for (int i = 0; i < commands.length; i++) {
commands[i].undo();
}
}
}