設計模式-命令模式(Command)-Java

設計模式-命令模式(Command)-Java


目錄




內容

1、前言

  裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的打開和關閉,例如電燈或者排氣扇。在購買開關時,我們並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、排氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制排氣扇或者其他電器設備。開關與電器之間通過電線建立連接,如果開關打開,則電線通電,電器工作;反之,開關關閉,電線斷電,電器停止工作。相同的開關可以通過不同的電線來控制不同的電器,如圖1-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-G6so35ud-1591957589436)(./images/開關與電燈、排氣扇示意圖.png)]
圖1 開關與電燈、排氣扇示意圖

  在圖1中,我們可以將開關理解成一個請求的發送者,用戶通過它來發送一個“開燈”請求,而電燈是“開燈”請求的最終接收者和處理者,在圖中,開關和電燈之間並不存在直接耦合關係,它們通過電線連接在一起,使用不同的電線可以連接不同的請求接收者,只需更換一根電線,相同的發送者(開關)即可對應不同的接收者(電器)。在軟件開發中也存在很多與開關和電器類似的請求發送者和接收者對象,例如一個按鈕,它可能是一個“關閉窗口”請求的發送者,而按鈕點擊事件處理類則是該請求的接收者。爲了降低系統的耦合度,將請求的發送者和接收者解耦,我們可以使用一種被稱之爲命令模式的設計模式來設計系統,在命令模式中,發送者與接收者之間引入了新的命令對象(類似圖1中的電線),將發送者的請求封裝在命令對象中,再通過命令對象來調用接收者的方法。本章我們將學習用於將請求發送者和接收者解耦的命令模式。

2、示例案例-自定義功能鍵

 &esmp;Sunny軟件公司開發人員爲公司內部OA系統開發了一個桌面版應用程序,該應用程序爲用戶提供了一系列自定義功能鍵,用戶可以通過這些功能鍵來實現一些快捷操作。Sunny軟件公司開發人員通過分析,發現不同的用戶可能會有不同的使用習慣,在設置功能鍵的時候每個人都有自己的喜好,例如有的人喜歡將第一個功能鍵設置爲“打開幫助文檔”,有的人則喜歡將該功能鍵設置爲“最小化至托盤”,爲了讓用戶能夠靈活地進行功能鍵的設置,開發人員提供了一個“功能鍵設置”窗口,該窗口界面如圖2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-o3gzPGMM-1591957589440)(./images/功能鍵設置.png)]
圖2-1 功能鍵設置界面效果圖

  通過如圖2-1所示界面,用戶可以將功能鍵和相應功能綁定在一起,還可以根據需要來修改功能鍵的設置,而且系統在未來可能還會增加一些新的功能或功能鍵。Sunny軟件公司某開發人員欲使用如下代碼來實現功能鍵與功能處理類之間的調用關係:

//FunctionButton:功能鍵類,請求發送者
class FunctionButton {
private HelpHandler help; //HelpHandler:幫助文檔處理類,請求接收者
//在FunctionButton的onClick()方法中調用HelpHandler的display()方法
public void onClick() {
help = new HelpHandler();
help.display(); //顯示幫助文檔
}
}

  在上述代碼中,功能鍵類FunctionButton充當請求的發送者,幫助文檔處理類HelpHandler充當請求的接收者,在發送者FunctionButton的onClick()方法中將調用接收者HelpHandler的display()方法。顯然,如果使用上述代碼,將給系統帶來如下幾個問題:

  • (1) 由於請求發送者和請求接收者之間存在方法的直接調用,耦合度很高,更換請求接收者必須修改發送者的源代碼,如果需要將請求接收者HelpHandler改爲WindowHanlder(窗口處理類),則需要修改FunctionButton的源代碼,違背了“開閉原則”。
  • (2) FunctionButton類在設計和實現時功能已被固定,如果增加一個新的請求接收者,如果不修改原有的FunctionButton類,則必須增加一個新的與FunctionButton功能類似的類,這將導致系統中類的個數急劇增加。由於請求接收者HelpHandler、WindowHanlder等類之間可能不存在任何關係,它們沒有共同的抽象層,因此也很難依據“依賴倒轉原則”來設計FunctionButton。
  • (3) 用戶無法按照自己的需要來設置某個功能鍵的功能,一個功能鍵類的功能一旦固定,在不修改源代碼的情況下無法更換其功能,系統缺乏靈活性。不難得知,所有這些問題的產生都是因爲請求發送者FunctionButton類和請求接收者HelpHandler、WindowHanlder等類之間存在直接耦合關係,如何降低請求發送者和接收者之間的耦合度,讓相同的發送者可以對應不同的接收者?這是Sunny軟件公司開發人員在設計“功能鍵設置”模塊時不得不考慮的問題。命令模式正爲解決這類問題而誕生,此時,如果我們使用命令模式,可以在一定程度上解決上述問題(注:命令模式無法解決類的個數增加的問題),下面就讓我們正式進入命令模式的學習,看看命令模式到底如何實現請求發送者和接收者解耦。

3、命令模式概述

  在軟件開發中,我們經常需要向某些對象發送請求(調用其中的某個或某些方法),但是並不知道請求的接收者是誰,也不知道被請求的操作是哪個,此時,我們特別希望能夠以一種松耦合的方式來設計軟件,使得請求發送者與請求接收者能夠消除彼此之間的耦合,讓對象之間的調用關係更加靈活,可以靈活地指定請求接收者以及被請求的操作。命令模式爲此類問題提供了一個較爲完美的解決方案。

  命令模式可以將請求發送者和接收者完全解耦,發送者與接收者之間沒有直接引用關係,發送請求的對象只需要知道如何發送請求,而不必知道如何完成請求。

3.1、命令模式定義

  • 命令模式(Command Pattern):將一個請求封裝爲一個對象,從而讓我們可用不用的請求對客戶進行參數化;對請求排隊或者記錄請求日誌,以及支持可撤銷的操作。命令模式是一種對象行爲型模式,其別名爲動作(Action)模式或事務(Transaction)模式。

  命令模式的定義比較複雜,提到了很多術語,例如“用不同的請求對客戶進行參數化”、“對請求排隊”,“記錄請求日誌”、“支持可撤銷操作”等,在後面我們將對這些術語進行一一講解。

3.2、命令模式結構

  命令模式的核心在於引入了命令類,通過命令類來降低發送者和接收者的耦合度,請求發送者只需指定一個命令對象,再通過命令對象來調用請求接收者的處理方法,其結構如圖3.2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qCoJpDsp-1591957589441)(./images/model_command.png)]
圖3.2-1 命令模式結構圖

3.3、命令模式結構圖中角色

  在命令模式結構圖中包含如下幾個角色:

  • Command(抽象命令類):抽象命令類一般是一個抽象類或接口,在其中聲明瞭用於執行請求的execute()等方法,通過這些方法可以調用請求接收者的相關操作。
  • ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中聲明的方法,它對應具體的接收者對象,將接收者對象的動作綁定其中。在實現execute()方法時,將調用接收者對象的相關操作(Action)。
  • Invoker(調用者):調用者即請求發送者,它通過命令對象來執行請求。一個調用者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關以及是怎麼被執行的。

3.4、典型實現

  命令模式的關鍵在於引入了抽象命令類,請求發送者針對抽象命令類編程,只有實現了抽象命令類的具體命令才與請求接收者相關聯。在最簡單的抽象命令類中只包含了一個抽象的execute()方法,每個具體命令類將一個Receiver類型的對象作爲一個實例變量進行存儲,從而具體指定一個請求的接收者,不同的具體命令類提供了execute()方法的不同實現,並調用不同接收者的請求處理方法。 典型的抽象命令類代碼如下所示:

abstract class Command {
public abstract void execute();
}

  對於請求發送者即調用者而言,將針對抽象命令類進行編程,可以通過構造注入或者設值注入的方式在運行時傳入具體命令類對象,並在業務方法中調用命令對象的execute()方法,其典型代碼如下所示:

class Invoker {
private Command command;
//構造注入
public Invoker(Command command) {
this.command = command;
}
//設值注入
public void setCommand(Command command) {
this.command = command;
}
//業務方法,用於調用命令類的execute()方法
public void call() {
command.execute();
}
}

  具體命令類繼承了抽象命令類,它與請求接收者相關聯,實現了在抽象命令類中聲明的execute()方法,並在實現時調用接收者的請求響應方法action(),其典型代碼如下所示:

class ConcreteCommand extends Command {
private Receiver receiver; //維持一個對請求接收者對象的引用
public void execute() {
receiver.action(); //調用請求接收者的業務處理方法action()
}
}

  請求接收者Receiver類具體實現對請求的業務處理,它提供了action()方法,用於執行與請求相關的操作,其典型代碼如下所示:

class Receiver {
public void action() {
//具體操作
}
}

4、命令模式完整解決方案-自定義功能鍵

  爲了降低功能鍵與功能處理類之間的耦合度,讓用戶可以自定義每一個功能鍵的功能,Sunny軟件公司開發人員使用命令模式來設計“自定義功能鍵”模塊,其核心結構如圖4-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bUfnCFK9-1591957589442)(./images/自定義功能鍵.png)]
圖4-1 自定義功能鍵核心結構圖

  在圖4-1中,FBSettingWindow是“功能鍵設置”界面類,FunctionButton充當請求調用者,Command充當抽象命令類,MinimizeCommand和HelpCommand充當具體命令類,WindowHanlder和HelpHandler充當請求接收者。完整代碼如下所示:

  • FBSettingWindow類代碼4-1:功能就設置窗口類

      package command;
    
      import java.util.ArrayList;
    
      //功能鍵設置窗口類
      public class FBSettingWindow {
      	private String title; //窗口標題
      	//定義一個ArrayList來存儲所有功能鍵
      	private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();
      	public FBSettingWindow(String title) {
      		this.title = title;
      	}
      	public void setTitle(String title) {
      		this.title = title;
      	}
      	public String getTitle() {
      		return this.title;
      	}
      	public void addFunctionButton(FunctionButton fb) {
      		functionButtons.add(fb);
      	}
      	public void removeFunctionButton(FunctionButton fb) {
      		functionButtons.remove(fb);
      	}
    
      	//顯示窗口及功能鍵
      	public void display() {
      		System.out.println("顯示窗口:" + this.title);
      		System.out.println("顯示功能鍵:");
      		for (Object obj : functionButtons) {
      			System.out.println(((FunctionButton)obj).getName());
      		}
      		System.out.println("------------------------------");
      	}
      }
    
  • FunctionButton類代碼4-2:功能鍵類-請求發送者

      package command;
    
      //功能鍵類:請求發送者
      public class FunctionButton {
      	private String name; //功能鍵名稱
      	private Command command; //維持一個抽象命令對象的引用
      	public FunctionButton(String name) {
      		this.name = name;
      	}
      	public String getName() {
      	return this.name;
      	}
      	//爲功能鍵注入命令
      	public void setCommand(Command command) {
      		this.command = command;
      	}
      	//發送請求的方法
      	public void onClick() {
      		System.out.print("點擊功能鍵:");
      		command.execute();
      	}
      }
    
  • Command類代碼4-3:抽象命令類

      package command;
    
      //抽象命令類
      public abstract class Command {
      	public abstract void execute();
      }
    
  • HelpCommand類代碼4-4:幫助命令類-具體命令類

      package command;
    
      //幫助命令類:具體命令類
      public class HelpCommand extends Command{
      	private HelpHandler hhObj; //維持對請求接收者的引用
      	public HelpCommand() {
      		hhObj = new HelpHandler();
      	}
      	//命令執行方法,將調用請求接收者的業務方法
      	@Override
      	public void execute() {
      		hhObj.display();
      	}
      }
    
  • MinimizeCommand類代碼4-5:最小化命令類-具體命令類

      package command;
    
      //最小化命令類:具體命令類
      public class MinimizeCommand extends Command {
      	private WindowHandler whObj; //維持對請求接收者的引用
      	public MinimizeCommand() {
      		whObj = new WindowHandler();
      	}
      	//命令執行方法,將調用請求接收者的業務方法
      	@Override
      	public void execute() {
      		whObj.minimize();
      	}
      }
    
  • HelpHandler類代碼4-6:幫助文檔處理類-請求接收者

      package command;
    
      //幫助文檔處理類:請求接收者
      public class HelpHandler {
      	public void display() {
      		System.out.println("顯示幫助文檔!");
      	}
      }
    
  • WindowHandler類代碼4-7:窗口處理類-請求接收者

      package command;
    
      //窗口處理類:請求接收者
      public class WindowHandler {
      	public void minimize() {
      		System.out.println("將窗口最小化至托盤!");
      	}
      }
    
  • Utils類代碼4-8:工具類-獲取具體命令對象

      package command;
    
      import java.util.Properties;
    
      public class Utils {
      	public static Command getCommand(String args) {
      		try {
      			Properties prop = new Properties();
      			prop.load(Utils.class.getClassLoader().getResourceAsStream("command.properties"));
      			String className = prop.getProperty(args);
      			return (Command) (Class.forName(className).newInstance());
      		}catch (Exception e) {
      			e.printStackTrace();
      			return null;
      		}
      	}
      }
    
  • command.properties配置文件內容:

      help=command.HelpCommand
      minimize=command.MinimizeCommand
    
  • Client類代碼4-9:客戶端類-測試

      package command;
    
      public class Client {
      	public static void main(String args[]) {
      		FBSettingWindow fbsw = new FBSettingWindow("功能鍵設置");
      		FunctionButton fb1,fb2;
      		fb1 = new FunctionButton("功能鍵1");
      		fb2 = new FunctionButton("功能鍵1");
      		Command command1,command2;
      		//通過讀取配置文件和反射生成具體命令對象
      		command1 = Utils.getCommand("help");
    
      		command2 = Utils.getCommand("minimize");
      		//將命令對象注入功能鍵
      		fb1.setCommand(command1);
      		fb2.setCommand(command2);
      		fbsw.addFunctionButton(fb1);
      		fbsw.addFunctionButton(fb2);
      		fbsw.display();
      		//調用功能鍵的業務方法
      		fb1.onClick();
      		fb2.onClick();
      	}
      }
    
  • 測試結果:

      顯示窗口:功能鍵設置
      顯示功能鍵:
      功能鍵1
      功能鍵1
      ------------------------------
      點擊功能鍵:顯示幫助文檔!
      點擊功能鍵:將窗口最小化至托盤!
    

  如果需要修改功能鍵的功能,例如某個功能鍵可以實現“自動截屏”,只需要對應增加一個新的具體命令類,在該命令類與屏幕處理者(ScreenHandler)之間創建一個關聯關係,然後將該具體命令類的對象通過配置文件注入到某個功能鍵即可,原有代碼無須修改,符合“開閉原則”。在此過程中,每一個具體命令類對應一個請求的處理者(接收者),通過向請求發送者注入不同的具體命令對象可以使得相同的發送者對應不同的接收者,從而實現“將一個請求封裝爲一個對象,用不同的請求對客戶進行參數化”,客戶端只需要將具體命令對象作爲參數注入請求發送者,無須直接操作請求的接收者。

5、命令隊列的實現

  有時候我們需要將多個請求排隊,當一個請求發送者發送一個請求時,將不止一個請求接收者產生響應,這些請求接收者將逐個執行業務方法,完成對請求的處理。此時,我們可以通過命令隊列來實現。

  命令隊列的實現方法有多種形式,其中最常用、靈活性最好的一種方式是增加一個CommandQueue類,由該類來負責存儲多個命令對象,而不同的命令對象可以對應不同的請求接收者,CommandQueue類的典型代碼如下所示:

import java.util.*;
class CommandQueue {
//定義一個ArrayList來存儲命令隊列
private ArrayList<Command> commands = new ArrayList<Command>();
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
//循環調用每一個命令對象的execute()方法
public void execute() {
for (Object command : commands) {
((Command)command).execute();
}
}
}

  在增加了命令隊列類CommandQueue以後,請求發送者類Invoker將針對CommandQueue編程,代碼修改如下:

class Invoker {
private CommandQueue commandQueue; //維持一個CommandQueue對象的引用
//構造注入
public Invoker(CommandQueue commandQueue) {
this. commandQueue = commandQueue;
}
//設值注入
public void setCommandQueue(CommandQueue commandQueue) {
this.commandQueue = commandQueue;

}
//調用CommandQueue類的execute()方法
public void call() {
commandQueue.execute();
}
}

  命令隊列與我們常說的“批處理”有點類似。批處理,顧名思義,可以對一組對象(命令)進行批量處理,當一個發送者發送請求後,將有一系列接收者對請求作出響應,命令隊列可以用於設計批處理應用程序,如果請求接收者的接收次序沒有嚴格的先後次序,我們還可以使用多線程技術來併發調用命令對象的execute()方法,從而提高程序的執行效率。

6、撤銷操作的實現

 &esmp;可通過在命令類中增加一個逆向操作來實現。
擴展
  除了通過一個逆向操作來實現撤銷(Undo)外,還可以通過保存對象的歷史狀態來實現撤銷,後者可使用備忘錄模式(Memento Pattern)來實現。

  下面通過一個簡單的實例來學習如何使用命令模式實現撤銷操作:
  Sunny軟件公司欲開發一個簡易計算器,該計算器可以實現簡單的數學運算,還可以對運算實施撤銷操作。

  Sunny軟件公司開發人員使用命令模式設計瞭如圖6-1所示結構圖,其中計算器界面類CalculatorForm充當請求發送者,實現了數據求和功能的加法類Adder充當請求接收者,界面類可間接調用加法類中的add()方法實現加法運算,並且提供了可撤銷加法運算的undo()方法。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7z7GxsWh-1591957589444)(./images/簡易計算器.png)]
圖5 簡易計算器結構圖

  本實例完整代碼如下所示:

//加法類:請求接收者
class Adder {
private int num=0; //定義初始值爲0
//加法操作,每次將傳入的值與num作加法運算,再將結果返回
public int add(int value) {
num += value;
return num;
}
}
//抽象命令類
abstract class AbstractCommand {
public abstract int execute(int value); //聲明命令執行方法execute()
public abstract int undo(); //聲明撤銷方法undo()
}
//具體命令類
class ConcreteCommand extends AbstractCommand {
private Adder adder = new Adder();
private int value;
//實現抽象命令類中聲明的execute()方法,調用加法類的加法操作
public int execute(int value) {
this.value=value;
return adder.add(value);
}
//實現抽象命令類中聲明的undo()方法,通過加一個相反數來實現加法的逆向操作
public int undo() {
return adder.add(-value);
}
}
//計算器界面類:請求發送者
class CalculatorForm {
private AbstractCommand command;
public void setCommand(AbstractCommand command) {
this.command = command;
}
//調用命令對象的execute()方法執行運算
public void compute(int value) {
int i = command.execute(value);
System.out.println("執行運算,運算結果爲:" + i);
}
//調用命令對象的undo()方法執行撤銷
public void undo() {
int i = command.undo();
System.out.println("執行撤銷,運算結果爲:" + i);
}
}

編寫如下客戶端測試代碼:

class Client {
public static void main(String args[]) {
CalculatorForm form = new CalculatorForm();
AbstractCommand command;
command = new ConcreteCommand();
form.setCommand(command); //向發送者注入命令對象
form.compute(10);
form.compute(5);
form.compute(10);
form.undo();
}
}

編譯並運行程序,輸出結果如下:

執行運算,運算結果爲:10
執行運算,運算結果爲:15
執行運算,運算結果爲:25
執行撤銷,運算結果爲:15

7、請求日誌

&esmp; 請求日誌就是將請求的歷史記錄保存下來,通常以日誌文件(Log File)的形式永久存儲在計算
機中。很多系統都提供了日誌文件,例如Windows日誌文件、Oracle日誌文件等,日誌文件可
以記錄用戶對系統的一些操作(例如對數據的更改)。請求日誌文件可以實現很多功能,常
用功能如下:
(1) “天有不測風雲”,一旦系統發生故障,日誌文件可以爲系統提供一種恢復機制,在請求日
志文件中可以記錄用戶對系統的每一步操作,從而讓系統能夠順利恢復到某一個特定的狀
態;
(2) 請求日誌也可以用於實現批處理,在一個請求日誌文件中可以存儲一系列命令對象,例如
一個命令隊列;
(3) 可以將命令隊列中的所有命令對象都存儲在一個日誌文件中,每執行一個命令則從日誌文
件中刪除一個對應的命令對象,防止因爲斷電或者系統重啓等原因造成請求丟失,而且可以
避免重新發送全部請求時造成某些命令的重複執行,只需讀取請求日誌文件,再繼續執行文
件中剩餘的命令即可。
在實現請求日誌時,我們可以將命令對象通過序列化寫到日誌文件中,此時命令類必須實現
Java.io.Serializable接口。下面我們通過一個簡單實例來說明日誌文件的用途以及如何實現請求
日誌:
  Sunny軟件公司開發了一個網站配置文件管理工具,可以通過一個可視化界面對網站配置文件
進行增刪改等操作,該工具使用命令模式進行設計,結構如圖7-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3ORPkiFq-1591957589445)(./images/網站配置文件管理工具.png)]
圖7-1 網站配置文件管理工具結構圖

  現在Sunny軟件公司開發人員希望將對配置文件的操作請求記錄在日誌文件中,如果網站重新部署,只需要執行保存在日誌文件中的命令對象即可修改配置文件。本實例完整代碼如下所示:

import java.io.*;
import java.util.*;
//抽象命令類,由於需要將命令對象寫入文件,因此它實現了Serializable接口
abstract class Command implements Serializable {
protected String name; //命令名稱
protected String args; //命令參數
protected ConfigOperator configOperator; //維持對接收者對象的引用
public Command(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public void setConfigOperator(ConfigOperator configOperator) {
this.configOperator = configOperator;
}
//聲明兩個抽象的執行方法execute()
public abstract void execute(String args);
public abstract void execute();
}
//增加命令類:具體命令
class InsertCommand extends Command {
public InsertCommand(String name) {
super(name);
}
public void execute(String args) {
this.args = args;
configOperator.insert(args);
}
public void execute() {
configOperator.insert(this.args);
}
}
//修改命令類:具體命令
class ModifyCommand extends Command {
public ModifyCommand(String name) {
super(name);
}
public void execute(String args) {
this.args = args;
configOperator.modify(args);
}
public void execute() {
configOperator.modify(this.args);
}
}
//省略了刪除命令類DeleteCommand
//配置文件操作類:請求接收者。由於ConfigOperator類的對象是Command的成員對象,它也將隨Command對象一起寫入文件,因此ConfigOperator也需要實現Serializable接口
class ConfigOperator implements Serializable {
public void insert(String args) {
System.out.println("增加新節點:" + args);
}
public void modify(String args) {
System.out.println("修改節點:" + args);
}
public void delete(String args) {
System.out.println("刪除節點:" + args);
}
}
//配置文件設置窗口類:請求發送者
class ConfigSettingWindow {
//定義一個集合來存儲每一次操作時的命令對象
private ArrayList<Command> commands = new ArrayList<Command>();
private Command command;
//注入具體命令對象
public void setCommand(Command command) {
this.command = command;
}
//執行配置文件修改命令,同時將命令對象添加到命令集合中
public void call(String args) {
command.execute(args);
commands.add(command);
}
//記錄請求日誌,生成日誌文件,將命令集合寫入日誌文件
public void save() {
FileUtil.writeCommands(commands);
}
//從日誌文件中提取命令集合,並循環調用每一個命令對象的execute()方法來實現配置文件的重新設置
public void recover() {
ArrayList list;
list = FileUtil.readCommands();
for (Object obj : list) {
((Command)obj).execute();
}
}
}
//工具類:文件操作類
class FileUtil {
//將命令集合寫入日誌文件
public static void writeCommands(ArrayList commands) {
try {
FileOutputStream file = new FileOutputStream("config.log");
//創建對象輸出流用於將對象寫入到文件中
ObjectOutputStream objout = new ObjectOutputStream(new BufferedOutputStream(file));
//將對象寫入文件
objout.writeObject(commands);
objout.close();
}
catch(Exception e) {
System.out.println("命令保存失敗!");
e.printStackTrace();
}
}
//從日誌文件中提取命令集合
public static ArrayList readCommands() {
try {
FileInputStream file = new FileInputStream("config.log");
//創建對象輸入流用於從文件中讀取對象
ObjectInputStream objin = new ObjectInputStream(new BufferedInputStream(file));
//將文件中的對象讀出並轉換爲ArrayList類型
ArrayList commands = (ArrayList)objin.readObject();
objin.close();
return commands;
}
catch(Exception e) {
System.out.println("命令讀取失敗!");
e.printStackTrace();
return null;
}
}
}

編寫如下客戶端測試代碼:

class Client {
public static void main(String args[]) {
ConfigSettingWindow csw = new ConfigSettingWindow(); //定義請求發送者
Command command; //定義命令對象
ConfigOperator co = new ConfigOperator(); //定義請求接收者
//四次對配置文件的更改
command = new InsertCommand("增加");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("網站首頁");
command = new InsertCommand("增加");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("端口號");
command = new ModifyCommand("修改");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("網站首頁");
command = new ModifyCommand("修改");
command.setConfigOperator(co);
csw.setCommand(command);
csw.call("端口號");
System.out.println("----------------------------");
System.out.println("保存配置");
csw.save();
System.out.println("----------------------------");
System.out.println("恢復配置");
System.out.println("----------------------------");
csw.recover();
}
}

編譯並運行程序,輸出結果如下:

增加新節點:網站首頁
增加新節點:端口號
修改節點:網站首頁
修改節點:端口號
----------------------------
保存配置
----------------------------
恢復配置
----------------------------
增加新節點:網站首頁
增加新節點:端口號
修改節點:網站首頁
修改節點:端口號

8、宏命令

  宏命令(Macro Command)又稱爲組合命令,它是組合模式和命令模式聯用的產物。宏命令是一個具體命令類,它擁有一個集合屬性,在該集合中包含了對其他命令對象的引用。通常宏命令不直接與請求接收者交互,而是通過它的成員來調用接收者的方法。當調用宏命令的execute()方法時,將遞歸調用它所包含的每個成員命令的execute()方法,一個宏命令的成員可以是簡單命令,還可以繼續是宏命令。執行一個宏命令將觸發多個具體命令的執行,從而實現對命令的批處理,其結構如圖8-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ccg4uUGQ-1591957589446)(./images/宏命令.png)]
圖8-1 宏命令結構圖

9、總結

&esmp; 命令模式是一種使用頻率非常高的設計模式,它可以將請求發送者與接收者解耦,請求發送者通過命令對象來間接引用請求接收者,使得系統具有更好的靈活性和可擴展性。在基於GUI的軟件開發,無論是在電腦桌面應用還是在移動應用中,命令模式都得到了廣泛的應用。

9.1、優缺點

命令模式的主要優點如下:

  • (1) 降低系統的耦合度。由於請求者與接收者之間不存在直接引用,因此請求者與接收者之間實現完全解耦,相同的請求者可以對應不同的接收者,同樣,相同的接收者也可以供不同的請求者使用,兩者之間具有良好的獨立性+ + (2) 新的命令可以很容易地加入到系統中。由於增加新的具體命令類不會影響到其他類,因此增加新的具體命令類很容易,無須修改原有系統源代碼,甚至客戶類代碼,滿足“開閉原則”的要求。
  • (3) 可以比較容易地設計一個命令隊列或宏命令(組合命令)。
  • (4) 爲請求的撤銷(Undo)和恢復(Redo)操作提供了一種設計和實現方案。

命令模式的主要缺點如下:

  • 使用命令模式可能會導致某些系統有過多的具體命令類。因爲針對每一個對請求接收者的調用操作都需要設計一個具體命令類,因此在某些系統中可能需要提供大量的具體命令類,這將影響命令模式的使用。

9.2、適用場景

在以下情況下可以考慮使用命令模式:

  • (1) 系統需要將請求調用者和請求接收者解耦,使得調用者和接收者不直接交互。請求調用者無須知道接收者的存在,也無須知道接收者是誰,接收者也無須關心何時被調用。
  • (2) 系統需要在不同的時間指定請求、將請求排隊和執行請求。一個命令對象和請求的初始調用者可以有不同的生命期,換言之,最初的請求發出者可能已經不在了,而命令對象本身仍然是活動的,可以通過該命令對象去調用請求接收者,而無須關心請求調用者的存在性,可以通過請求日誌文件等機制來具體實現。
  • (3) 系統需要支持命令的撤銷(Undo)操作和恢復(Redo)操作。
  • (4) 系統需要將一組操作組合在一起形成宏命令。

後記

  參考文獻:Java設計模式(劉偉).pdf。持續更新,歡迎交流,本人QQ:806797785

前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端JAVA源代碼地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章