1. 程式人生 > >Java設計模式05--命令模式

Java設計模式05--命令模式

裝修新房的最後幾道工序之一是安裝插座和開關,通過開關可以控制一些電器的開啟和關閉,例如電燈或者排氣扇。在購買開關時,我們並不知道它將來到底用於控制什麼電器,也就是說,開關與電燈、排氣扇並無直接關係,一個開關在安裝之後可能用來控制電燈,也可能用來控制排氣扇或者其他電器裝置。開關與電器之間通過電線建立連線,如果開關開啟,則電線通電,電器工作;反之,開關關閉,電線斷電,電器停止工作。相同的開關可以通過不同的電線來控制不同的電器,如圖1所示:

開關與電燈、排氣扇示意圖

       在圖1中,我們可以將開關理解成一個請求的傳送者,使用者通過它來發送一個“開燈”請求,而電燈是“開燈”請求的最終接收者和處理者,在圖中,開關和電燈之間並不存在直接耦合關係,它們通過電線連線在一起,使用不同的電線可以連線不同的請求接收者,只需更換一根電線,相同的傳送者(開關)即可對應不同的接收者(電器)。

       在軟體開發中也存在很多與開關和電器類似的請求傳送者和接收者物件,例如一個按鈕,它可能是一個“關閉視窗”請求的傳送者,而按鈕點選事件處理類則是該請求的接收者。為了降低系統的耦合度,將請求的傳送者和接收者解耦,我們可以使用一種被稱之為命令模式的設計模式來設計系統,在命令模式中,傳送者與接收者之間引入了新的命令物件(類似圖1中的電線),將傳送者的請求封裝在命令物件中,再通過命令物件來呼叫接收者的方法。本章我們將學習用於將請求傳送者和接收者解耦的命令模式。

1 自定義功能鍵

       Sunny軟體公司開發人員為公司內部OA系統開發了一個桌面版應用程式,該應用程式為使用者提供了一系列自定義功能鍵,使用者可以通過這些功能鍵來實現一些快捷操作。Sunny

軟體公司開發人員通過分析,發現不同的使用者可能會有不同的使用習慣,在設定功能鍵的時候每個人都有自己的喜好,例如有的人喜歡將第一個功能鍵設定為“開啟幫助文件”,有的人則喜歡將該功能鍵設定為“最小化至托盤”,為了讓使用者能夠靈活地進行功能鍵的設定,開發人員提供了一個“功能鍵設定”視窗,該視窗介面如圖2所示:

2  “功能鍵設定”介面效果圖

       通過如圖2所示介面,使用者可以將功能鍵和相應功能繫結在一起,還可以根據需要來修改功能鍵的設定,而且系統在未來可能還會增加一些新的功能或功能鍵。

       Sunny軟體公司某開發人員欲使用如下程式碼來實現功能鍵與功能處理類之間的呼叫關係:

  1. //FunctionButton:功能鍵類,請求傳送者
  2. class FunctionButton {
  3. private HelpHandler help; //HelpHandler:幫助文件處理類,請求接收者
  4. //在FunctionButton的onClick()方法中呼叫HelpHandler的display()方法
  5. public void onClick() {
  6. help = new HelpHandler();
  7. help.display(); //顯示幫助文件
  8. }
  9. }

在上述程式碼中,功能鍵類FunctionButton充當請求的傳送者,幫助文件處理類HelpHandler充當請求的接收者,在傳送者FunctionButtononClick()方法中將呼叫接收者HelpHandlerdisplay()方法。顯然,如果使用上述程式碼,將給系統帶來如下幾個問題:

       (1) 由於請求傳送者和請求接收者之間存在方法的直接呼叫,耦合度很高,更換請求接收者必須修改傳送者的原始碼,如果需要將請求接收者HelpHandler改為WindowHanlder(視窗處理類),則需要修改FunctionButton的原始碼,違背了“開閉原則”。

       (2) FunctionButton類在設計和實現時功能已被固定,如果增加一個新的請求接收者,如果不修改原有的FunctionButton類,則必須增加一個新的與FunctionButton功能類似的類,這將導致系統中類的個數急劇增加。由於請求接收者HelpHandlerWindowHanlder等類之間可能不存在任何關係,它們沒有共同的抽象層,因此也很難依據“依賴倒轉原則”來設計FunctionButton

       (3) 使用者無法按照自己的需要來設定某個功能鍵的功能,一個功能鍵類的功能一旦固定,在不修改原始碼的情況下無法更換其功能,系統缺乏靈活性。

       不難得知,所有這些問題的產生都是因為請求傳送者FunctionButton類和請求接收者HelpHandlerWindowHanlder等類之間存在直接耦合關係,如何降低請求傳送者和接收者之間的耦合度,讓相同的傳送者可以對應不同的接收者?這是Sunny軟體公司開發人員在設計“功能鍵設定”模組時不得不考慮的問題。命令模式正為解決這類問題而誕生,此時,如果我們使用命令模式,可以在一定程度上解決上述問題(注:命令模式無法解決類的個數增加的問題),下面就讓我們正式進入命令模式的學習,看看命令模式到底如何實現請求傳送者和接收者解耦。

2 命令模式概述

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

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

       命令模式定義如下:

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

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

       命令模式的核心在於引入了命令類,通過命令類來降低傳送者和接收者的耦合度,請求傳送者只需指定一個命令物件,再通過命令物件來呼叫請求接收者的處理方法,其結構如圖3所示:

圖3命令模式結構圖

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

       ● Command(抽象命令類):抽象命令類一般是一個抽象類或介面,在其中聲明瞭用於執行請求的execute()等方法,通過這些方法可以呼叫請求接收者的相關操作。

       ● ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中宣告的方法,它對應具體的接收者物件,將接收者物件的動作繫結其中。在實現execute()方法時,將呼叫接收者物件的相關操作(Action)

       ● Invoker(呼叫者):呼叫者即請求傳送者,它通過命令物件來執行請求。一個呼叫者並不需要在設計時確定其接收者,因此它只與抽象命令類之間存在關聯關係。在程式執行時可以將一個具體命令物件注入其中,再呼叫具體命令物件的execute()方法,從而實現間接呼叫請求接收者的相關操作。

       ● Receiver(接收者):接收者執行與請求相關的操作,它具體實現對請求的業務處理。

       命令模式的本質是對請求進行封裝,一個請求對應於一個命令,將發出命令的責任和執行命令的責任分割開每一個命令都是一個操作:請求的一方發出請求要求執行一個操作;接收的一方收到請求,並執行相應的操作。命令模式允許請求的一方和接收的一方獨立開來,使得請求的一方不必知道接收請求的一方的介面,更不必知道請求如何被接收、操作是否被執行、何時被執行,以及是怎麼被執行的

       命令模式的關鍵在於引入了抽象命令類,請求傳送者針對抽象命令類程式設計,只有實現了抽象命令類的具體命令才與請求接收者相關聯。在最簡單的抽象命令類中只包含了一個抽象的execute()方法,每個具體命令類將一個Receiver型別的物件作為一個例項變數進行儲存,從而具體指定一個請求的接收者,不同的具體命令類提供了execute()方法的不同實現,並呼叫不同接收者的請求處理方法。

        典型的抽象命令類程式碼如下所示:

  1. abstract class Command {
  2. public abstract void execute();
  3. }

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

  1. class Invoker {
  2. private Command command;
  3. //構造注入
  4. public Invoker(Command command) {
  5. this.command = command;
  6. }
  7. //設值注入
  8. public void setCommand(Command command) {
  9. this.command = command;
  10. }
  11. //業務方法,用於呼叫命令類的execute()方法
  12. public void call() {
  13. command.execute();
  14. }
  15. }

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

  1. class ConcreteCommand extends Command {
  2. private Receiver receiver; //維持一個對請求接收者物件的引用
  3. public void execute() {
  4. receiver.action(); //呼叫請求接收者的業務處理方法action()
  5. }
  6. }

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

  1. class Receiver {
  2. public void action() {
  3. //具體操作
  4. }
  5. }

疑問

思考

一個請求傳送者能否對應多個請求接收者?如何實現?


3 完整解決方案

       為了降低功能鍵與功能處理類之間的耦合度,讓使用者可以自定義每一個功能鍵的功能,Sunny軟體公司開發人員使用命令模式來設計“自定義功能鍵”模組,其核心結構如圖4所示:

自定義功能鍵核心結構圖

       在圖4中,FBSettingWindow是“功能鍵設定”介面類,FunctionButton充當請求呼叫者,Command充當抽象命令類,MinimizeCommandHelpCommand充當具體命令類,WindowHanlderHelpHandler充當請求接收者。完整程式碼如下所示:

  1. import java.util.*;
  2. //功能鍵設定視窗類
  3. class FBSettingWindow {
  4. private String title; //視窗標題
  5. //定義一個ArrayList來儲存所有功能鍵
  6. private ArrayList<FunctionButton> functionButtons = new ArrayList<FunctionButton>();
  7. public FBSettingWindow(String title) {
  8. this.title = title;
  9. }
  10. public void setTitle(String title) {
  11. this.title = title;
  12. }
  13. public String getTitle() {
  14. return this.title;
  15. }
  16. public void addFunctionButton(FunctionButton fb) {
  17. functionButtons.add(fb);
  18. }
  19. public void removeFunctionButton(FunctionButton fb) {
  20. functionButtons.remove(fb);
  21. }
  22. //顯示視窗及功能鍵
  23. public void display() {
  24. System.out.println("顯示視窗:" + this.title);
  25. System.out.println("顯示功能鍵:");
  26. for (Object obj : functionButtons) {
  27. System.out.println(((FunctionButton)obj).getName());
  28. }
  29. System.out.println("------------------------------");
  30. }
  31. }
  32. //功能鍵類:請求傳送者
  33. class FunctionButton {
  34. private String name; //功能鍵名稱
  35. private Command command; //維持一個抽象命令物件的引用
  36. public FunctionButton(String name) {
  37. this.name = name;
  38. }
  39. public String getName() {
  40. return this.name;
  41. }
  42. //為功能鍵注入命令
  43. public void setCommand(Command command) {
  44. this.command = command;
  45. }
  46. //傳送請求的方法
  47. public void onClick() {
  48. System.out.print("點選功能鍵:");
  49. command.execute();
  50. }
  51. }
  52. //抽象命令類
  53. abstract class Command {
  54. public abstract void execute();
  55. }
  56. //幫助命令類:具體命令類
  57. class HelpCommand extends Command {
  58. private HelpHandler hhObj; //維持對請求接收者的引用
  59. public HelpCommand() {
  60. hhObj = new HelpHandler();
  61. }
  62. //命令執行方法,將呼叫請求接收者的業務方法
  63. public void execute() {
  64. hhObj.display();
  65. }
  66. }
  67. //最小化命令類:具體命令類
  68. class MinimizeCommand extends Command {
  69. private WindowHanlder whObj; //維持對請求接收者的引用
  70. public MinimizeCommand() {
  71. whObj = new WindowHanlder();
  72. }
  73. //命令執行方法,將呼叫請求接收者的業務方法
  74. public void execute() {
  75. whObj.minimize();
  76. }
  77. }
  78. //視窗處理類:請求接收者
  79. class WindowHanlder {
  80. public void minimize() {
  81. System.out.println("將視窗最小化至托盤!");
  82. }
  83. }
  84. //幫助文件處理類:請求接收者
  85. class HelpHandler {
  86. public void display() {
  87. System.out.println("顯示幫助文件!");
  88. }
  89. }

       為了提高系統的靈活性和可擴充套件性,我們將具體命令類的類名儲存在配置檔案中,並通過工具類XMLUtil來讀取配置檔案並反射生成物件,XMLUtil類的程式碼如下所示:

  1. public class XMLUtil {
  2. //該方法用於從XML配置檔案中提取具體類類名,並返回一個例項物件
  3. public static Object getBean(String args) throws Exception {
  4. SAXReader reader = new SAXReader();
  5. String path = XMLUtil.class.getClassLoader().
  6. getResource("com/somnus/designPatterns/command/config.xml").getPath();
  7. Document document = reader.read(new File(path));
  8. String cName = null;
  9. if(args.equals("firstCommand")) {
  10. cName = document.selectSingleNode("/config/firstCommand").getText();
  11. }else if(args.equals("secondCommand")) {
  12. //獲取第二個包含類名的節點,即具體實現類
  13. cName = document.selectSingleNode("/config/secondCommand").getText();
  14. }
  15. //通過類名生成例項物件並將其返回
  16. Class<?> c = Class.forName(cName);
  17. Object obj = c.newInstance();
  18. return obj;
  19. }
  20. }

       配置檔案config.xml中儲存了具體建造者類的類名,程式碼如下所示:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <config>
  3. <firstCommand>com.somnus.designPatterns.command.HelpCommand</firstCommand>
  4. <secondCommand>com.somnus.designPatterns.command.MinimizeCommand</secondCommand>
  5. </config>

       編寫如下客戶端測試程式碼:

  1. public class Client {
  2. public static void main(String[] args) throws Exception {
  3. FBSettingWindow fbsw = new FBSettingWindow("功能鍵設定");
  4. FunctionButton fb1,fb2;
  5. fb1 = new FunctionButton("功能鍵1");
  6. fb2 = new FunctionButton("功能鍵1");
  7. Command command1,command2;
  8. //通過讀取配置檔案和反射生成具體命令物件
  9. command1 = (Command)XMLUtil.getBean("firstCommand");
  10. command2 = (Command)XMLUtil.getBean("secondCommand");
  11. //將命令物件注入功能鍵
  12. fb1.setCommand(command1);
  13. fb2.setCommand(command2);
  14. fbsw.addFunctionButton(fb1);
  15. fbsw.addFunctionButton(fb2);
  16. fbsw.display();
  17. //呼叫功能鍵的業務方法
  18. fb1.onClick();
  19. fb2.onClick();
  20. }
  21. }

       編譯並執行程式,輸出結果如下:

顯示視窗:功能鍵設定

顯示功能鍵:

功能鍵1

功能鍵1

------------------------------

點選功能鍵:顯示幫助文件!

點選功能鍵:將視窗最小化至托盤!

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

4 命令佇列的實現

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

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

  1. import java.util.*;
  2. public class CommandQueue {
  3. //定義一個ArrayList來儲存命令佇列
  4. private ArrayList<Command> commands = new ArrayList<Command>();
  5. public void addCommand(Command command) {
  6. commands.add(command);
  7. }
  8. public void removeCommand(Command command) {
  9. commands.remove(command);
  10. }
  11. //迴圈呼叫每一個命令物件的execute()方法
  12. public void execute() {
  13. for (Object command : commands) {
  14. ((Command)command).execute();
  15. }
  16. }
  17. }

       在增加了命令佇列類CommandQueue以後,請求傳送者類Invoker將針對CommandQueue程式設計,程式碼修改如下:

  1. public class Invoker {
  2. private CommandQueue commandQueue; //維持一個CommandQueue物件的引用
  3. //構造注入
  4. public Invoker(CommandQueue commandQueue) {
  5. this. commandQueue = commandQueue;
  6. }
  7. //設值注入
  8. public void setCommandQueue(CommandQueue commandQueue) {
  9. this.commandQueue = commandQueue;
  10. }
  11. //呼叫CommandQueue類的execute()方法
  12. public void call() {
  13. commandQueue.execute();
  14. }
  15. }

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

5 撤銷操作的實現

在命令模式中,我們可以通過呼叫一個命令物件的execute()方法來實現對請求的處理,如果需要撤銷(Undo)請求,可通過在命令類中增加一個逆向操作來實現。

微笑

擴充套件

除了通過一個逆向操作來實現撤銷(Undo)外,還可以通過儲存物件的歷史狀態來實現撤銷,後者可使用備忘錄模式(Memento Pattern)來實現

       下面通過一個簡單的例項來學習如何使用命令模式實現撤銷操作:

       Sunny軟體公司欲開發一個簡易計算器,該計算器可以實現簡單的數學運算,還可以對運算實施撤銷操作。

       Sunny軟體公司開發人員使用命令模式設計瞭如圖5所示結構圖,其中計算器介面類CalculatorForm充當請求傳送者,實現了資料求和功能的加法類Adder充當請求接收者,介面類可間接呼叫加法類中的add()方法實現加法運算,並且提供了可撤銷加法運算的undo()方法。

5  簡易計算器結構圖

本例項完整程式碼如下所示:

  1. //加法類:請求接收者
  2. public class Adder {
  3. private int num=0; //定義初始值為0
  4. //加法操作,每次將傳入的值與num作加法運算,再將結果返回
  5. public int add(int value) {
  6. num += value;
  7. return num;
  8. }
  9. }
  10. //抽象命令類
  11. public abstract class AbstractCommand {
  12. public abstract int execute(int value); //宣告命令執行方法execute()
  13. public abstract int undo(); //宣告撤銷方法undo()
  14. }
  15. //具體命令類
  16. class ConcreteCommand extends AbstractCommand {
  17. private Adder adder = new Adder();
  18. private int value;
  19. //實現抽象命令類中宣告的execute()方法,呼叫加法類的加法操作
  20. public int execute(int value) {
  21. this.value=value;
  22. return adder.add(value);
  23. }
  24. //實現抽象命令類中宣告的undo()方法,通過加一個相反數來實現加法的逆向操作
  25. public int undo() {
  26. return adder.add(-value);
  27. }
  28. }
  29. //計算器介面類:請求傳送者
  30. public class CalculatorForm {
  31. private AbstractCommand command;
  32. public void setCommand(AbstractCommand command) {
  33. this.command = command;
  34. }
  35. //呼叫命令物件的execute()方法執行運算
  36. public void compute(int value) {
  37. int i = command.execute(value);
  38. System.out.println("執行運算,運算結果為:" + i);
  39. }
  40. //呼叫命令物件的undo()方法執行撤銷
  41. public void undo() {
  42. int i = command.undo();
  43. System.out.println("執行撤銷,運算結果為:" + i);
  44. }
  45. }

       編寫如下客戶端測試程式碼:

  1. public class Client {
  2. public static void main(String args[]) {
  3. CalculatorForm form = new CalculatorForm();
  4. AbstractCommand command;
  5. command = new ConcreteCommand();
  6. form.setCommand(command); //向傳送者注入命令物件
  7. form.compute(10);
  8. form.compute(5);
  9. form.compute(10);
  10. form.undo();
  11. }
  12. }

編譯並執行程式,輸出結果如下:

執行運算,運算結果為:10

執行運算,運算結果為:15

執行運算,運算結果為:25

執行撤銷,運算結果為:15

疑問

思考

如果連續呼叫“form.undo()”兩次,預測客戶端程式碼的輸出結果。

       需要注意的是在本例項中只能實現一步撤銷操作,因為沒有儲存命令物件的歷史狀態,可以通過引入一個命令集合或其他方式來儲存每一次操作時命令的狀態,從而實現多次撤銷操作。除了Undo操作外,還可以採用類似的方式實現恢復(Redo)操作,即恢復所撤銷的操作(或稱為二次撤銷)。

微笑

練習

修改簡易計算器原始碼,使之能夠實現多次撤銷(Undo)和恢復(Redo)

6 請求日誌

       請求日誌就是將請求的歷史記錄儲存下來,通常以日誌檔案(Log File)的形式永久儲存在計算機中。很多系統都提供了日誌檔案,例如Windows日誌檔案、Oracle日誌檔案等,日誌檔案可以記錄使用者對系統的一些操作(例如對資料的更改)。請求日誌檔案可以實現很多功能,常用功能如下:

       (1) “天有不測風雲”,一旦系統發生故障,日誌檔案可以為系統提供一種恢復機制,在請求日誌檔案中可以記錄使用者對系統的每一步操作,從而讓系統能夠順利恢復到某一個