設計模式——命令模式(Command Pattern)
一、命令模式的定義
將“請求”封裝成物件,以便使用不同的請求,佇列或者日誌來引數化其他物件。命令模式也支援可撤銷。
命令介面–ICommand
public interface ICommand {
public void execute();
public void undo();
}
定義統一的介面,所有的命令類都需要實現該介面。
命令類–ConcreteCommand
public class ConcreteCommand implements ICommand {
private Reciever reciever;
public ConcreteCommand(Reciever receiver) {
this.reciever = receiver;
}
public void execute() {
reciever.action();
}
public void undo() {
reciever.undo();
}
}
將某一個具體的“請求”封裝成具體的命令類,實現ICommand介面。命令模式的核心目的是將發出請求的物件和執行請求的物件解耦,被解耦的兩者之間是通過命令物件進行溝通的,命令物件封裝了接受者及其一個或多個動作。比如:開燈命令,可以是LightOnCommand;
命令執行者—Reciever
public class Reciever {
private String recieverName;
pubic Reciever(String name) {
this.recieverName = name;
}
public void action() {
System.out.println("do action...");
}
public void undoAction() {
System.out.println("undo action ..." );
}
}
該類是真正執行命令的類,實質上它根本不知道ICommand和Command的存在,所以它只要純粹完成自己的邏輯。比如一個電燈類(Light),就只需要實現開關電燈的邏輯,並暴露出呼叫介面就行了。
呼叫者–Invoker
public class Invoker {
private ICommand command;
public Invoker() {}
public void setCommand(ICommand command) {
this.command = command;
}
// 呼叫建立時就設定好的命令物件
public invokeCommand() {
this.command.execute();
}
// 動態呼叫通過引數傳過來的命令物件
public invokeCommand(ICommand command) {
command.execute();
}
public invokeUndo() {
this.command.undo();
}
}
命令的呼叫者,它持有一個或者多個命令物件,在某個時間點呼叫命令物件的execute方法,將請求付諸執行。
客戶端–Client
public class Client {
Receiver reciever = new Reciever();
ICommand c = new ConcreteCommand(reciever);
Invoker invoker = new Invoker();
invoker.setCommand(c);
invoker.invokeCommand();
}
負責建立具體的命令物件,並設定命令的執行者。
巨集命令
public class MacroCommand implements ICommand {
ICommands[] commands;
public MacroCommand(ICommand[] commands) {
this.commands = commands;
}
public void execute() {
for (int i=0; i<commands.length; i++) {
commands[i].execute();
}
}
public void undo() {
for (int i=0; i<commands.length; i++) {
commands[i].undo();
}
}
}
巨集命令的目的是,製造一種新的命令來執行其他一堆命令。這樣這個巨集命令也是一個實現了ICommand的命令物件,因此可以普通的命令物件一樣被執行。
二、一個具體的例子
實現一個控制電燈開關的遙控器,有三個功能:開、關、撤銷操作。
/**
* 命令真正執行者,對應類圖中的Reciever
*/
public class Light {
public void onLight() {
System.out.println("light on...");
}
public void offLight() {
System.out.println("light off...");
}
public void undo() {
System.out.println("light undo...")
}
}
/**
* 開燈命令,對應類圖中的ConcreteCommand
*/
public class LightOnCommand implements ICommand{
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.onLight();
}
public void undo() {
light.undo();
}
}
/**
* 關燈命令,對應類圖中的ConcreteCommand
*/
public class LightOffCommand implements ICommand{
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.offLight();
}
public void undo() {
light.undo();
}
}
/**
* 定義一個空命令物件,它是一個空物件,不做任何事情。可能遙控器在出廠時並沒有
* 設定真正有效的命令物件,就可以用空物件代替,以便後續在設定真正的命令物件。
*/
public class NoCommand implements ICommand {
public void execute(){}
public void undo(){}
}
/**
* 命令呼叫者,對應類圖中的Invoker
*/
public class LightController {
// 持有多個命令物件
private ICommand[] commands;
// 記錄前一個命令物件,用於“撤銷”操作
private ICommand undoCommand;
public LightController() {
// 用於儲存開和關兩個命令物件
this.commands = new ICommand[2];
// 初始化
ICommand noCommand = new NoCommand();
for(int i=0; i<2; i++){
command[i] = noCommand;
}
undoCommand = noCommand;
}
public void setCommands(ICommand onCommand, ICommand offCommand) {
// 第一個元素被設為開命令物件
commands[0] = onCommand;
commands[1] = offCommand;
}
// 按下開啟電燈的按鈕(請求開啟電燈)
public void onButtonPushed() {
commands[0].execute();
undoCommand = commands[0];
}
// 按下打關電燈的按鈕(請求關掉電燈)
public void offButtonPushed() {
commands[1].execute();
undoCommand = commands[1];
}
// 按下撤銷按鈕,由對應的命令物件執行其撤銷操作
pubic void undoButtonPushed() {
undoCommand.undo();
}
// ...其他程式碼
}
/**
* 建立者,對應類圖中的Client類
*/
public class LightControllerTest {
public void static main(String[] args) {
// 建立命令物件及其執行者
Light light = new Light();
LightOnCommand onCommand = new LightOnCommand(light);
LightOffCommand offCommand = new LightOffCommand(light);
LightController lightController = new LightController();
lightController.setCommands(onCommand, offCommand);
// 測試
lightController.onButtonPushed();
lightController.offButtonPushed();
lightController.undoButtonPushed();
}
}
三、優缺點
優點
- 將發出請求的物件與執行請求的物件解耦。
- 呼叫者可以執行通過引數傳過來的命令物件,因而可以在執行時動態執行命令。
- 命令的傳送者和命令執行者有不同的生命週期。命令傳送了並不是立即執行。
- 可以使用巨集命令任意組合一些命令,比如實現“一鍵式家居控制”的需求,就可以將各種家電的命令物件組合成一組。
- 因為將“請求”封裝成了物件,使得命令可以被傳遞,被延時執行。比如,將命令物件傳送到遠端服務進行執行;也可以將命令物件扔到佇列中,由專門的執行命令執行緒進行執行,執行命令的執行緒不需要知道具體命令內容,只要是實現了ICommand介面的物件,它都可以執行。
- 因為將“請求”封裝成了物件,從而使得可以對請求的執行過程進行一些控制。比如“撤銷”操作可以讓命令執行失敗之後,進行撤銷,可用於事務執行。
- 可將命令記錄到日誌,系統故障後恢復等。
缺點
- 每一個命令都封裝為命令物件,導致類太多。
- 對於這一點,個人感覺不需要如此細的粒度,比如LightOnCommand,LightOffCommand,只要設計一個合適的資料結構,通過資料來表達,就可以省掉很多類,程式碼的靈活和可擴充套件性也不受影響。
四、應用場景
佇列
命令物件扔到佇列中,由專門的執行命令執行緒進行執行,執行命令的執行緒不需要知道具體命令內容,只要是實現了ICommand介面的物件,它都可執行。因為命令模式下請求者和執行者可以有不同的生命週期,所以命令物件在建立很久以後,仍然可以被執行。另外,可以使用執行緒池來執行這些命令,只要這些命令實現Runable介面。
日誌
因為命令物件可以對命令自身進行管理,比如可以輸出命令日誌,所以可以用於“容災”等需要記錄所有命令的應用。在這些應用中,命令物件中新增記錄日誌將命令儲存到磁碟中,一旦系統崩潰或宕機,重啟後可以重新載入命令,再批次執行命令物件的execute方法。比如資料庫事務處理,電子表格等應用。
狀態條
如果假如系統需要按順序執行一系列的命令操作,並且需要了解命令執行進度和狀態,則可以讓每個命令物件都提供一個獲取命令執行狀態的方法,系統呼叫該方法顯示狀態。
需要多級撤銷或重做操作
可以在Invoker中用一個棧來儲存執行過的命令物件,一旦需要撤銷或重做操作,可以通過pop出最近的一個命令物件,並執行其undo方法。
web服務請求處理
將每個web請求封裝成一個命令物件。比如struts框架中,就用到了命令模式: Struts框架中,在模型層都要繼承一個Action介面,並實現execute方法,其實這個Action就是命令類。為什麼Struts會應用命令模式,是因為Struts的核心控制器ActionServlet只有一個,相當於Invoker,而模型層的類會隨著不同的應用有不同的模型類,相當於具體的Command。這樣,就需要在ActionServlet和模型層之間解耦,而命令模式正好解決這個問題。