【架構—設計模式】命令模式

裝修新房的最後幾道工序之一是安裝插座和開關,經過開關能夠控制一些電器的打開和關閉,例如電燈或者排氣扇。在購買開關時,咱們並不知道它未來到底用於控制什麼電器,也就是說,開關與電燈、排氣扇並沒有直接關係,一個開關在安裝以後可能用來控制電燈,也可能用來控制排氣扇或者其餘電器設備。開關與電器之間經過電線創建鏈接,若是開關打開,則電線通電,電器工做;反之,開關關閉,電線斷電,電器中止工做。相同的開關能夠經過不一樣的電線來控制不一樣的電器。php

在軟件開發中也存在不少與開關和電器相似的請求發送者和接收者對象,例如一個按鈕,它多是一個「關閉窗口」請求的發送者,而按鈕點擊事件處理類則是該請求的接收者。爲了下降系統的耦合度,將請求的發送者和接收者解耦,咱們可使用一種被稱之爲命令模式的設計模式來設計系統,在命令模式中,發送者與接收者之間引入了新的命令對象,將發送者的請求封裝在命令對象中,再經過命令對象來調用接收者的方法。編程

概述

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

命令模式能夠將請求發送者和接收者徹底解耦,發送者與接收者之間沒有直接引用關係,發送請求的對象只須要知道如何發送請求,而沒必要知道如何完成請求。數組

命令模式定義以下:多線程

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

命令模式的定義比較複雜,提到了不少術語,例如「用不一樣的請求對客戶進行參數化」、「對請求排隊」,「記錄請求日誌」、「支持可撤銷操做」等,在後面將對這些術語進行一一講解。併發

命令模式的核心在於引入了命令類,經過命令類來下降發送者和接收者的耦合度,請求發送者只需指定一個命令對象,再經過命令對象來調用請求接收者的處理方法,其結構如圖所示:工具

1366033467_9048.jpg

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

  • Command(抽象命令類):抽象命令類通常是一個抽象類或接口,在其中聲明瞭用於執行請求的execute()等方法,經過這些方法能夠調用請求接收者的相關操做。
  • ConcreteCommand(具體命令類):具體命令類是抽象命令類的子類,實現了在抽象命令類中聲明的方法,它對應具體的接收者對象,將接收者對象的動做綁定其中。在實現execute()方法時,將調用接收者對象的相關操做(Action)。
  • Invoker(調用者):調用者即請求發送者,它經過命令對象來執行請求。一個調用者並不須要在設計時肯定其接收者,所以它只與抽象命令類之間存在關聯關係。在程序運行時能夠將一個具體命令對象注入其中,再調用具體命令對象的execute()方法,從而實現間接調用請求接收者的相關操做。
  • Receiver(接收者):接收者執行與請求相關的操做,它具體實現對請求的業務處理。

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

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

典型的抽象命令類代碼以下所示:

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

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

class Invoker
{
    private $command;

    //構造注入
    public function __construct(Command $command)
    {
        $this->command = $command;
    }

    //設值注入
    public function setCommand(Command $command)
    {
        $this->command = $command;
    }

    //業務方法,用於調用命令類的execute()方法
    public function call()
    {
        $this->command->execute();
    }
}

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

class ConcreteCommand extends Command
{
    //維持一個對請求接收者對象的引用
    private $receiver;

    public function execute()
    {
        //調用請求接收者的業務處理方法action()
        $this->receiver->action();
    }
}

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

class Receiver
{
    public function action()
    {
        //具體操做
    }
}

案例

Sunny軟件公司開發人員爲公司內部OA系統開發了一個桌面版應用程序,該應用程序爲用戶提供了一系列自定義功能鍵,用戶能夠經過這些功能鍵來實現一些快捷操做。Sunny軟件公司開發人員經過分析,發現不一樣的用戶可能會有不一樣的使用習慣,在設置功能鍵的時候每一個人都有本身的喜愛,例若有的人喜歡將第一個功能鍵設置爲「打開幫助文檔」,有的人則喜歡將該功能鍵設置爲「最小化至托盤」,爲了讓用戶可以靈活地進行功能鍵的設置,開發人員提供了一個「功能鍵設置」窗口,該窗口界面如圖所示:

1366033417_2468.jpg

經過如圖所示界面,用戶能夠將功能鍵和相應功能綁定在一塊兒,還能夠根據須要來修改功能鍵的設置,並且系統在將來可能還會增長一些新的功能或功能鍵。

爲了下降功能鍵與功能處理類之間的耦合度,讓用戶能夠自定義每個功能鍵的功能,Sunny軟件公司開發人員使用命令模式來設計「自定義功能鍵」模塊,其核心結構如圖所示:

1366034181_4378.jpg

FBSettingWindow是「功能鍵設置」界面類,FunctionButton充當請求調用者,Command充當抽象命令類,MinimizeCommand和HelpCommand充當具體命令類,WindowHanlder和HelpHandler充當請求接收者。完整代碼以下所示:

<?php

//功能鍵設置窗口類
class FBSettingWindow
{
    private $title; //窗口標題

    //定義一個數組來存儲全部功能鍵
    private $functionButtons = [];

    public function __construct(string $title)
    {
        $this->title = $title;
    }

    public function setTitle(string $title)
    {
        $this->title = $title;
    }

    public function getTitle(): string
    {
        return $this->title;
    }

    public function addFunctionButton(FunctionButton $fb)
    {
        $this->functionButtons[] = $fb;
    }

    public function removeFunctionButton(FunctionButton $fb)
    {
        unset($this->functionButtons[array_search($fb, $this->functionButtons)]);
    }

    //顯示窗口及功能鍵
    public function display()
    {
        foreach ($this->functionButtons as $button) {
            $button->getName();
        }
    }
}

//功能鍵類:請求發送者
class FunctionButton
{
    private $name; //功能鍵名稱
    private $command; //維持一個抽象命令對象的引用

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function getName(): string
    {
        return $this->name;
    }

    //爲功能鍵注入命令
    public function setCommand(Command $command)
    {
        $this->command = $command;
    }

    //發送請求的方法
    public function onClick()
    {
        $this->command->execute();
    }
}

//抽象命令類
abstract class Command
{
    abstract public function execute();
}

//幫助命令類:具體命令類
class HelpCommand extends Command
{
    private $hhObj; //維持對請求接收者的引用

    public function __construct()
    {
        $this->hhObj = new HelpHandler();
    }

    //命令執行方法,將調用請求接收者的業務方法
    public function execute()
    {
        $this->hhObj->display();
    }
}

//最小化命令類:具體命令類
class MinimizeCommand extends Command
{
    private $whObj; //維持對請求接收者的引用

    public function __construct()
    {
        $this->whObj = new WindowHanlder();
    }

    //命令執行方法,將調用請求接收者的業務方法
    public function execute()
    {
        $this->whObj->minimize();
    }
}

//窗口處理類:請求接收者
class WindowHanlder
{
    public function minimize()
    {
        echo '窗口最小化';
    }
}

//幫助文檔處理類:請求接收者
class HelpHandler
{
    public function display()
    {
        echo '顯示幫助文檔';
    }
}

爲了提升系統的靈活性和可擴展性,咱們將具體命令類的類名存儲在配置文件中,並經過工具類ConfigUtil來讀取配置文件並反射生成對象。

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

命令隊列的實現

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

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

class CommandQueue
{
    //定義一個數組來存儲命令隊列
    private $commands = [];

    public function addCommand(Command $command)
    {
        $this->commands[] = $command;
    }

    public function removeCommand(Command $command)
    {
        unset($this->commands[array_search($command, $this->commands)]);
    }

    //循環調用每個命令對象的execute()方法
    public function execute()
    {
        foreach ($this->commands as $command) {
            $command->execute();
        }
    }
}

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

class Invoker
{
    private $commandQueue; //維持一個CommandQueue對象的引用

    //構造注入
    public function __construct(CommandQueue $commandQueue)
    {
        $this->commandQueue = $commandQueue;
    }

    //設值注入
    public function setCommandQueue(CommandQueue $commandQueue)
    {
        $this->commandQueue = $commandQueue;
    }

    //調用CommandQueue類的execute()方法
    public function call()
    {
        $this->commandQueue->execute();
    }
}

命令隊列與咱們常說的「批處理」有點相似。批處理,顧名思義,能夠對一組對象(命令)進行批量處理,當一個發送者發送請求後,將有一系列接收者對請求做出響應,命令隊列能夠用於設計批處理應用程序,若是請求接收者的接收次序沒有嚴格的前後次序,咱們還可使用多線程技術來併發調用命令對象的execute()方法,從而提升程序的執行效率。

撤銷操做的實現

在命令模式中,咱們能夠經過調用一個命令對象的execute()方法來實現對請求的處理,若是須要撤銷(Undo)請求,可經過在命令類中增長一個逆向操做來實現。

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

Sunny軟件公司欲開發一個簡易計算器,該計算器能夠實現簡單的數學運算,還能夠對運算實施撤銷操做。

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

1366039384_7864.jpg

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

<?php

//加法類:請求接收者
class Adder
{
    private $num = 0; //定義初始值爲0

    //加法操做,每次將傳入的值與num做加法運算,再將結果返回
    public function add(int $value): int
    {
        return $this->num += $value;
    }
}

//抽象命令類
abstract class AbstractCommand
{
    abstract public function execute(int $value): int; //聲明命令執行方法execute()

    abstract public function undo(): int; //聲明撤銷方法undo()
}

//具體命令類
class ConcreteCommand extends AbstractCommand
{
    private $adder;
    private $value;

    public function __construct()
    {
        $this->adder = new Adder();
    }

    //實現抽象命令類中聲明的execute()方法,調用加法類的加法操做
    public function execute(int $value): int
    {
        $this->value = $value;

        return $this->adder->add($value);
    }

    //實現抽象命令類中聲明的undo()方法,經過加一個相反數來實現加法的逆向操做
    public function undo(): int
    {
        return $this->adder->add(-$this->value);
    }
}

//計算器界面類:請求發送者
class CalculatorForm
{
    private $command;

    public function setCommand(AbstractCommand $command)
    {
        $this->command = $command;
    }

    //調用命令對象的execute()方法執行運算
    public function compute(int $value)
    {
        return $this->command->execute($value);
    }

    //調用命令對象的undo()方法執行撤銷
    public function undo()
    {
        return $this->command->undo();
    }
}

須要注意的是在本實例中只能實現一步撤銷操做,由於沒有保存命令對象的歷史狀態,能夠經過引入一個命令集合或其餘方式來存儲每一次操做時命令的狀態,從而實現屢次撤銷操做。除了Undo操做外,還能夠採用相似的方式實現恢復(Redo)操做,即恢復所撤銷的操做(或稱爲二次撤銷)。

請求日誌

請求日誌就是將請求的歷史記錄保存下來,一般以日誌文件(Log File)的形式永久存儲在計算機中。不少系統都提供了日誌文件,例如Windows日誌文件、Oracle日誌文件等,日誌文件能夠記錄用戶對系統的一些操做(例如對數據的更改)。請求日誌文件能夠實現不少功能,經常使用功能以下:

  1. 「天有不測風雲」,一旦系統發生故障,日誌文件能夠爲系統提供一種恢復機制,在請求日誌文件中能夠記錄用戶對系統的每一步操做,從而讓系統可以順利恢復到某一個特定的狀態;
  2. 請求日誌也能夠用於實現批處理,在一個請求日誌文件中能夠存儲一系列命令對象,例如一個命令隊列;
  3. 能夠將命令隊列中的全部命令對象都存儲在一個日誌文件中,每執行一個命令則從日誌文件中刪除一個對應的命令對象,防止由於斷電或者系統重啓等緣由形成請求丟失,並且能夠避免從新發送所有請求時形成某些命令的重複執行,只需讀取請求日誌文件,再繼續執行文件中剩餘的命令便可。

在實現請求日誌時,咱們能夠將命令對象經過序列化寫到日誌文件中。

宏命令

宏命令(Macro Command)又稱爲組合命令,它是組合模式和命令模式聯用的產物。宏命令是一個具體命令類,它擁有一個集合屬性,在該集合中包含了對其餘命令對象的引用。一般宏命令不直接與請求接收者交互,而是經過它的成員來調用接收者的方法。當調用宏命令的execute()方法時,將遞歸調用它所包含的每一個成員命令的execute()方法,一個宏命令的成員能夠是簡單命令,還能夠繼續是宏命令。執行一個宏命令將觸發多個具體命令的執行,從而實現對命令的批處理,其結構如圖所示:

1366041322_3439.jpg

總結

命令模式是一種使用頻率很是高的設計模式,它能夠將請求發送者與接收者解耦,請求發送者經過命令對象來間接引用請求接收者,使得系統具備更好的靈活性和可擴展性。在基於GUI的軟件開發,不管是在電腦桌面應用仍是在移動應用中,命令模式都獲得了普遍的應用。

主要優勢
  1. 下降系統的耦合度。因爲請求者與接收者之間不存在直接引用,所以請求者與接收者之間實現徹底解耦,相同的請求者能夠對應不一樣的接收者,一樣,相同的接收者也能夠供不一樣的請求者使用,二者之間具備良好的獨立性。
  2. 新的命令能夠很容易地加入到系統中。因爲增長新的具體命令類不會影響到其餘類,所以增長新的具體命令類很容易,無須修改原有系統源代碼,甚至客戶類代碼,知足「開閉原則」的要求。
  3. 能夠比較容易地設計一個命令隊列或宏命令(組合命令)。
  4. 爲請求的撤銷(Undo)和恢復(Redo)操做提供了一種設計和實現方案。
主要缺點

使用命令模式可能會致使某些系統有過多的具體命令類。由於針對每個對請求接收者的調用操做都須要設計一個具體命令類,所以在某些系統中可能須要提供大量的具體命令類,這將影響命令模式的使用。

適用場景
  1. 系統須要將請求調用者和請求接收者解耦,使得調用者和接收者不直接交互。請求調用者無須知道接收者的存在,也無須知道接收者是誰,接收者也無須關心什麼時候被調用。
  2. 系統須要在不一樣的時間指定請求、將請求排隊和執行請求。一個命令對象和請求的初始調用者能夠有不一樣的生命期,換言之,最初的請求發出者可能已經不在了,而命令對象自己仍然是活動的,能夠經過該命令對象去調用請求接收者,而無須關心請求調用者的存在性,能夠經過請求日誌文件等機制來具體實現。
  3. 系統須要支持命令的撤銷(Undo)操做和恢復(Redo)操做。
  4. 系統須要將一組操做組合在一塊兒造成宏命令。
相關文章
相關標籤/搜索