1. 程式人生 > >命令模式( Command Pattern )

命令模式( Command Pattern )

  1. 參考書籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》

設計模式用前須知

  • 設計模式種一句出現頻率非常高的話是,“ 在不改動。。。。的情況下, 實現。。。。的擴充套件“ 。
  • 對於設計模式的學習者來說,充分思考這句話其實非常重要, 因為這句往往只對框架/ 工具包的設計才有真正的意義。因為框架和工具包存在的意義,就是為了讓其他的程式設計師予以利用, 進行功能的擴充套件,而這種功能的擴充套件必須以不需要改動框架和工具包中程式碼為前提
  • 對於應用程式的編寫者, 從理論上來說, 所有的應用層級程式碼至少都是處於可編輯範圍內的, 如果不細加考量, 就盲目使用較為複雜的設計模式, 反而會得不償失, 畢竟靈活性的獲得, 也是有代價的。

命令模式(Command Pattern)

  • 設計意圖
    • 把一個命令封裝成一個物件,作為引數傳遞給其他物件,使得該物件可以改變其 可以傳送不同的請求, 將請求排隊, 將請求撤銷。
    • 命令模式可以被看作是面向過程程式語言中的回撥函式在面嚮物件語言中的實現方式。
  • GoF 舉例

    • 命令模式最常見的例子就是我們平時編寫使用者介面所使用的工具包和類庫, 以 Java 為例, 支援以如下的方式繫結 事件(Event) 與 行為/命令(Command)

          //讓按鈕具備關閉視窗的功能
          button.addActionListener(new
      ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("按鈕執行關閉視窗的功能"); System.exit(0); } });
    • 上面的程式碼呈現的是應用程式設計師使用圖形介面類庫的實現介面時的場景, 而從圖形介面類庫編寫者角度來思考, 該如何編寫程式碼才能支援如上的寫法呢?

    • 在上述的例子中, 一個點選事件( ClickedEvent) 本質上是一個請求: 要求對使用者點選行為進行相應的請求。 對於類庫的編寫者來說, 如何響應使用者點選的請求, 是需要留給應用程式設計師去實現的, 所以對於類庫的設計者來說,在這種場景下, 他們並不知道一個請求的接受者( 點選事件被繫結在哪些元件上), 也不知道響應某個請求要進行哪些操作。
  • 解決方案:

    • 命令模式使得類庫中的物件可以把請求本身封裝成一個物件, 從而向未具體指定的應用程式物件傳送請求。 這個請求可以像普通的物件一樣被儲存或者傳遞。
      • Tips: 這裡的描述可能會使大部分程式設計師困惑,因為平時程式設計時, 我們接觸的請求似乎都是以物件的形式存在的, 例如 HttpRequest 中封裝了各種 Http 請求的引數。 但這裡的所說的請求其實來源於面向物件程式語言中一些學術化的術語, 在面嚮物件語言中, 物件之間的互動其實是通過方法呼叫完成的。 換句話說, 物件 A 如果想與物件 B 發生互動, 要求 B 對 A 的一個請求作出響應, 其具體方式是物件 A 首先得持有物件 B 的引用, 然後物件 A 呼叫 B.method(), 我們管這個過程可以看做 A 向 B 傳送了一個特定的請求,得到了mthod () 執行內容的響應 ,可能有返回值, 也可能沒有返回值。
    • 命令模式的核心是一個抽象類 Command , 它聲明瞭一個用於執行具體指令的介面。 在最簡單的情況下, 這個介面包含了一個抽象的 execute() 方法。 Command 的實現類會實現 execute 的具體內容。 此時, 當Command 實現類 ConcreteCommand 被例項化時, ConcreteCommand 就成為了某個請求的接受者, 具體如何響應請求, 是 execute() 方法內容決定的。
    • 下面通過圖形介面類庫的選單模組實現過程來更加詳細地展現這一過程。
      CommandExample1
    • 選單中的每一選項就是一個 MenuItem 例項。 Application 類建立了這些選單以及選單項以及介面的其他部分。 Application 類還需要持有使用者開啟的文件類 Document 物件的引用 .
    • Application 為每個 MenuItem配置一個具體的 Command 實現類。 當用戶選中一個選單項之後, MenuItem 會呼叫他的 command 物件的 execute 方法。 execute 方法會執行相應的操作。
      CommandExample2
    • MenuItem 並不知道他們使用的是 Command 哪一個子類 。 Command 子類可以儲存請求的 receiver , 然後通過 receiver 呼叫一個或更多的操作。

      • Tips: 這裡的 receiver 容易讓人困惑, 很多人會覺得 MenuItem 才是一個請求的 receiver。 這裡需要回想上文提到過的, 面向物件程式語言中的術語問題。 以文章一開始所舉的關閉視窗按鈕的 Java 程式碼為例。 程式碼中只是輸出了一行文字表示對應的功能得到了執行, 但如果實際編寫起來, 必然需要獲得一個代表視窗物件的例項, 如 window 物件, 在其中呼叫 window.close() 方法。 在這整個過程中, 視窗關閉按鈕點選的請求最終的響應物件其實是 window 這個物件。 把例子中的 window 物件和 receiver 對應起來 , 就不難理解 receiver 的職責了。
        //讓按鈕具備關閉視窗的功能
           button.addActionListener(new ActionListener()
           {
               public void actionPerformed(ActionEvent e)
               {
                    window.close(); // window 是這個 button 點選請求最終的 receiver 
                    System.exit(0);
               }
           });
    • 回到上圖所示的例子中, PaseteCommand 所接收到的點選請求的最終 receiver 是 Document 物件, Document 會通過 paste() 方法響應這個請求。
      CommandExample3

  • OpenCommand 的 execute 操作和之前的 PasteCommand 不同, 裡面包含了一些列的操作
    CommandExample4
  • 當一個 Command 中的 execute 可能需要執行一系列的操作時, 我們可以定義上圖的巨集命令 MacroCommand 結構來予以靈活的支援。 MacroCommand 物件可以包含多個 Command 物件的引用, 支援任意數量指令的呼叫 。 注意到, MacroCommand 物件並沒有明確的 receiver, 因為它所持有 commands 引用可以自行指定其各自的 receiver。

應用場景

命令模式(Command Pattern) 應當在你希望做到如下目標時使用:

  • 希望給物件引數化地傳遞一些行為。 對於熟悉回撥函式的人來說, 就是希望在面向物件程式語言中想要使用回撥函式時。
  • 在不同的時刻指定, 執行,排隊請求(注意這裡的請求是面向物件領域中的那個術語) 。 一個 Command 物件可以擁有和其對應請求完全獨立的生命週期。 如果一個請求最終的 receiver 可以用不依賴本程序記憶體空間的方式表示, 你可以把一個命令轉移到另外一個程序中去執行 ( RPC 的思想)
  • 支援撤銷 。 Command 類的 execute 操作可以將執行的狀態資訊儲存在command 物件中, 從而提供撤銷操作所需要進行的逆操作資訊。 為此, Command 抽象類需要新增一個 unexecute 方法來消除呼叫 execute 方法所產生的效果。 執行過的操作可以儲存在一個歷史列表 history list 中, 通過遍歷這個list, 依次執行 execute 操作或者 unexecute 操作, 可以實現不受限制層級的撤銷和重做。
  • 支援命令執行後導致的狀態變化進行日誌記錄 。 這些日誌可以被用於系統崩潰後的恢復。 通過為 Command 介面定義 load, store 操作, 可以持久化地記錄系統變化。 系統崩潰恢復時, 可以把尚未執行的命令從持久化的日誌中把所有的 commands 讀取出來 ,重新予以執行
  • 把一個圍繞高層級命令定義的系統以基礎的命令結構化的組織起來。 這種結構在支援“事務”的資訊系統中非常常見。 “事務”會把對於資料的多個改動封裝起來, 集體通過或集體撤銷。 命令模式 (Command Pattern) 提供了一個在應用層面, 模擬資料庫“事務”的方式。 命令模式也使得系統很容易擴充套件實現新的事務。

結構圖

這裡寫圖片描述

  • 這裡面最需要理解的就是 receiver 的角色, receiver 是一個請求最終的響應者。 客戶端發起一個請求以後, 中間其實會經過多個物件的依次呼叫 Invoker-> Command( ConcreteCommand) . execute() -> receiver.action()
  • 為了更加清晰, 下面把命令模式裡各個角色與之前的選單例子中類的對應關係羅列出來
    • Command 類無須對應
    • ConreteCommand –> PasteCommand / OpenCommand
    • Client –> Application
    • Invoker –> MenuItem
    • Receiver–> Document, Application

互動方式

這裡寫圖片描述

總結

命令模式看起來簡單,但是理解起來並不容易, 對於其中請求的傳送和接收, 都需要從面向物件領域裡的物件互動層面來理解, 如果簡單的對應到日常的程式設計經驗中, 則很容易發生誤解。

  • 命令模式解耦了將 操作的呼叫者 與 操作最終的執行者 解耦。
  • 命令模式中的 Command 可以被組裝成一個複合 Command 。 一個例子就是上文提到過的 MacroCommand 。 更寬泛的說, 複合命令其實就是組合模式(Composite Pattern)的一個應用。