命令模式

命令模式最初來源于圖形化用戶界面設(shè)計(jì),但現(xiàn)在廣泛應(yīng)用于企業(yè)設(shè)計(jì)个初,特別促進(jìn)了控制器(請(qǐng)求和分發(fā)處理)和領(lǐng)域模型(應(yīng)用邏輯)的分離乖寒。命令模式有助于系統(tǒng)更好地進(jìn)行組織,并易于擴(kuò)展院溺。

  1. 問題
    所有系統(tǒng)都必須決定如何響應(yīng)用戶請(qǐng)求楣嘁。在PHP中,這個(gè)決策過程通常是由分散的各個(gè)PHP頁面來處理珍逸。比如當(dāng)用戶訪問一個(gè)PHP頁面時(shí)逐虚,用戶明確地告訴系統(tǒng)他所要求的功能和接口。但現(xiàn)在PHP開發(fā)者日益傾向于在設(shè)計(jì)系統(tǒng)時(shí)采用單一入口的方式弄息。無論是多個(gè)入口還是單個(gè)入口痊班,接收者都必然將用戶請(qǐng)求委托給一個(gè)更加關(guān)注于應(yīng)用邏輯的層來進(jìn)行處理。這個(gè)委托在用戶請(qǐng)求不同頁面時(shí)尤為重要摹量。如果沒有委托,代碼重復(fù)將會(huì)不可避免地蔓延在整個(gè)項(xiàng)目中馒胆。
    讓我們想象一下缨称,假設(shè)一個(gè)有很多任務(wù)要執(zhí)行的項(xiàng)目,需要允許某些用戶登錄祝迂,某些用戶可以提交反饋睦尽。我們可以分別創(chuàng)建login.php和feedback.php頁面來處理這些任務(wù),并實(shí)例化專門的類以完成任務(wù)型雳。不過遺憾的是当凡,系統(tǒng)中的用戶界面很難被精確地一一對(duì)應(yīng)到系統(tǒng)任務(wù)。比如我們可能要求每個(gè)頁面都有登錄和反饋的功能纠俭。如果頁面必須處理很多不同的任務(wù)沿量,就應(yīng)該考慮將任務(wù)進(jìn)行封裝。封裝之后冤荆,向系統(tǒng)增加新任務(wù)就會(huì)變得簡單朴则,并且可以將系統(tǒng)中的各部分分離開來。當(dāng)然钓简,這時(shí)我們可以使用命令模式乌妒。
  2. 實(shí)現(xiàn)
    命令對(duì)象的接口極為簡單,因?yàn)樗灰髮?shí)現(xiàn)一個(gè)方法execute()外邓。
1.png

在圖中撤蚊,Command被定義為一個(gè)抽象類。同樣簡單地损话,它也可以被定義為接口侦啸。將它定義為抽象類,因?yàn)橛袝r(shí)基類也可以為它的衍生對(duì)象提供有用的公共功能。
命令模式由3部分組成:實(shí)例化命令對(duì)象的客戶端(client)匹中、部署命令對(duì)象的調(diào)用者(invoker)和接受命令的接收者(receiver)夏漱。
通過客戶端,接收者可以在命令對(duì)象的構(gòu)造方法中被傳遞給命令對(duì)象顶捷,或者通過某種工廠對(duì)象被獲得挂绰。相對(duì)而言,后一種辦法可以保持構(gòu)造方法參數(shù)清晰明了服赎,而且所有的Command對(duì)象都可以用完全相同的方式實(shí)例化葵蒂。
創(chuàng)建一個(gè)具體的Command類:

abstract class Command{
    abstract function execute(CommandContext $context);
}

class LoginCommand extends Command{
    function execute(CommandContext $context){
        $manger = Registry::getAccessManager();
        $user = $context->get('username');
        $pass = $context->get('pass');
        $user_obj = $manager->login($user, $pass);
        if(is_null($user_obj)){
            $context->setError($manager->getError());
            return false;
        }
        $context->addParam("user", $user_obj);
        return true;
    }
}

LoginCommand被設(shè)計(jì)為與AccessManager(訪問管理器)對(duì)象一起工作。AccessManager是一個(gè)虛構(gòu)出來的類重虑,它的任務(wù)就是處理用戶登錄系統(tǒng)的具體細(xì)節(jié)践付。注意Command::execute()方法要求使用CommandContext對(duì)象作為參數(shù)。通過CommandContext機(jī)制缺厉,請(qǐng)求數(shù)據(jù)可以被傳遞給Command對(duì)象永高,同時(shí)相應(yīng)也可以被返回到視圖層。以這種方式使用對(duì)象是很有好處的提针,因?yàn)槲覀兛梢圆黄茐慕涌诰桶巡煌膮?shù)傳遞給命令對(duì)象命爬。從本質(zhì)上說碘菜,CommandContext只是將關(guān)聯(lián)數(shù)組變量包裝而成的對(duì)象垄分,但我們?nèi)詴?huì)經(jīng)常擴(kuò)展它來執(zhí)行額外的任務(wù)。下面是一個(gè)簡單的CommandContext實(shí)現(xiàn):

class CommandContext{
    private $params = array();
    private $error = "";

    function __construct(){
        $this->params = $_REQUEST;
    }

    function addParam($key, $val){
        $this->params[$key] = $val;
    }

    function get($key){
        return $this->params[$key];
    }

    function setError($error){
        $this->error = $error;
    }

    function getError(){
        return $this->error;
    }
}

因此通過使用CommandContext對(duì)象概作,LoginCommand能夠訪問請(qǐng)求數(shù)據(jù):提交的用戶名和密碼嗜价。我們使用了一個(gè)簡單的類Registry艇抠,它帶有用于生成通用對(duì)象的靜態(tài)方法,可以返回LoginCommand所需要的AccessManager對(duì)象久锥。如果AccessManager報(bào)告一個(gè)錯(cuò)誤家淤,則LoginCommand保存錯(cuò)誤信息到CommandContext對(duì)象中以供表現(xiàn)層使用并返回false。如果一切正常奴拦,LoginCommand只返回true媒鼓。注意Command對(duì)象不應(yīng)該執(zhí)行太多的邏輯。它們應(yīng)該負(fù)責(zé)檢查輸入错妖、處理錯(cuò)誤绿鸣、緩存對(duì)象和調(diào)用其他對(duì)象來執(zhí)行一些必要的操作。如果你發(fā)現(xiàn)應(yīng)用邏輯過多地出現(xiàn)在Command類中暂氯,通常需要考慮重構(gòu)代碼潮模。這樣的代碼會(huì)導(dǎo)致代碼重復(fù),因?yàn)樗鼈儾豢杀苊獾貢?huì)在不同的Command類中被復(fù)制粘貼痴施。你至少需要考慮這些應(yīng)用邏輯的功能應(yīng)該屬于哪部分代碼擎厢。最好把這樣的代碼遷移到業(yè)務(wù)對(duì)象中或者放入一個(gè)外觀層中【苛鳎現(xiàn)在我們?nèi)匀蝗鄙倏蛻舳舜a(即用于創(chuàng)建命令對(duì)象的類)及調(diào)用者類(使用生成的命令的類)。在一個(gè)Web項(xiàng)目中动遭,選擇實(shí)例化哪個(gè)命令對(duì)象的最簡單的辦法是根據(jù)請(qǐng)求本身的參數(shù)來決定芬探。下面是一個(gè)簡化的客戶端代碼:

class CommandNotFoundException extends Exception{}

class CommandFactory{
    private static $dir = 'commands';

    static function getCommand($action='Default'){
        if(preg_match('/\W/',$action)){
            throw new Exception("illegal characters in action");
        }
        $class = UCFirst(strtolower($action))."Command";
        $file = self::$dir.DIRECTORY_SEPARATOR."{$class}.php";
        if(!file_exists($file)){
            throw new CommandNotFoundException("could not find '$file'");
        }
        require_once($file);
        if(!class_exists($class)){
            throw new CommandNotFoundException("no '$class' class located");
        }
        $cmd = new $class();
        return $cmd;
    }
}

CommandFactory類在commands目錄里查找特定的類文件。文件名是通過CommandContext對(duì)象的$action參數(shù)來構(gòu)造的厘惦,該參數(shù)是從請(qǐng)求中被傳到系統(tǒng)中的偷仿。如果文件和類都存在,那么會(huì)返回命令對(duì)象給調(diào)用者宵蕉。我們可以在這里添加更多的錯(cuò)誤檢查酝静,比如保證找到的類是Command類的子類,保證構(gòu)造方法沒有參數(shù)等羡玛,但目前的版本對(duì)我們來說已經(jīng)足夠說明問題别智。這種方式的優(yōu)點(diǎn)是你可以隨時(shí)將新的Command類添加到commands目錄下,然后系統(tǒng)便立即支持它了稼稿。
下面是一個(gè)簡單的調(diào)用者:

class Controller{
    private $context;
    function __construct(){
        $this->context = new CommandContext();
    }
  
    function getContext(){
        return $this->context;
    } 

    function process(){
        $cmd = CommandFactory::getCommand($this->context->get('action'));
        if(!cmd->execute($this->context)){
            //處理失敗
        }else{
            //成功
            //現(xiàn)在分發(fā)試圖
        }
    }
}

$controller = new Controller();
//偽造用戶請(qǐng)求
$context = $controller->getContext();
$context->addParam('action', 'login');
$context->addParam('username', 'bob');
$context->addParam('pass', 'tiddles');
$controller->process();

在調(diào)用Controller::process()之前薄榛,我們通過在控制器的構(gòu)造函數(shù)中實(shí)例化的CommandContext對(duì)象上設(shè)置參數(shù)偽造了一個(gè)Web請(qǐng)求。process()方法將實(shí)例化命令對(duì)象的工作委托給CommandFactory對(duì)象渺杉,然后它在返回的命令對(duì)象上調(diào)用execute()方法蛇数。注意,控制器對(duì)命令內(nèi)部是一無所知的是越。因?yàn)槊顖?zhí)行的細(xì)節(jié)與控制器是相互獨(dú)立的,所以我們可以隨時(shí)添加新的Command類而對(duì)當(dāng)前的結(jié)構(gòu)影響很小碌上。
讓我們再創(chuàng)建一個(gè)Command類:

class FeedbackCommand extends Command{
    function execute(CommandContext $context){
        $msgSystem = Registry::getMessageSystem();
        $email = $context->get('email');
        $msg = $context->get('msg');
        $topic = $context->get('topic');
        $result = $msgSystem->send($email, $msg, $topic);
        if(!$result){
            $context->setError($msgSystem->getError());
            return false;
        }
        return true;
    }
}

當(dāng)這個(gè)類以FeedbackCommand.php的文件名來保存倚评,并保存在正確的Commands目錄下時(shí),它就會(huì)被調(diào)用來響應(yīng)Action為feedback的請(qǐng)求馏予,而不需要對(duì)控制器或者CommandFactory做任何修改天梧。
圖11-9展示了命令模式的各個(gè)部分。

圖片.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末霞丧,一起剝皮案震驚了整個(gè)濱河市呢岗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛹尝,老刑警劉巖后豫,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異突那,居然都是意外死亡挫酿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門愕难,熙熙樓的掌柜王于貴愁眉苦臉地迎上來早龟,“玉大人惫霸,你說我怎么就攤上這事〈械埽” “怎么了壹店?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長芝加。 經(jīng)常有香客問我硅卢,道長,這世上最難降的妖魔是什么妖混? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任老赤,我火速辦了婚禮,結(jié)果婚禮上制市,老公的妹妹穿的比我還像新娘抬旺。我一直安慰自己,他們只是感情好祥楣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布开财。 她就那樣靜靜地躺著,像睡著了一般误褪。 火紅的嫁衣襯著肌膚如雪责鳍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天兽间,我揣著相機(jī)與錄音历葛,去河邊找鬼。 笑死嘀略,一個(gè)胖子當(dāng)著我的面吹牛恤溶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播帜羊,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咒程,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了讼育?” 一聲冷哼從身側(cè)響起帐姻,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奶段,沒想到半個(gè)月后饥瓷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忧饭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年扛伍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片词裤。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刺洒,死狀恐怖鳖宾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逆航,我是刑警寧澤鼎文,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站因俐,受9級(jí)特大地震影響拇惋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜抹剩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一撑帖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧澳眷,春花似錦胡嘿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拓瞪,卻和暖如春缴罗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背祭埂。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工面氓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛆橡。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓侧但,卻偏偏與公主長得像,于是被迫代替她去往敵國和親航罗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1 場景問題# 1.1 如何開機(jī)## 估計(jì)有些朋友看到這個(gè)標(biāo)題會(huì)非常奇怪屁药,電腦裝配好了粥血,如何開機(jī)?不就是按下啟動(dòng)按...
    七寸知架構(gòu)閱讀 2,828評(píng)論 1 59
  • 目錄 本文的結(jié)構(gòu)如下: 什么是命令模式 為什么要用該模式 模式的結(jié)構(gòu) 代碼示例 優(yōu)點(diǎn)和缺點(diǎn) 適用環(huán)境 模式應(yīng)用 總...
    w1992wishes閱讀 1,112評(píng)論 2 9
  • 3.5 隊(duì)列請(qǐng)求## 所謂隊(duì)列請(qǐng)求酿箭,就是對(duì)命令對(duì)象進(jìn)行排隊(duì)复亏,組成工作隊(duì)列,然后依次取出命令對(duì)象來執(zhí)行缭嫡。多用多線程或...
    七寸知架構(gòu)閱讀 2,018評(píng)論 4 54
  • 生活場景分析 今天來學(xué)習(xí)命令模式缔御,先從一個(gè)生活中的例子入手吧,這樣理解起來也比較容易妇蛀。大家應(yīng)該有用過那種萬能遙控器...
    西木柚子閱讀 730評(píng)論 2 6
  • 也許流進(jìn)黑暗的 就是曾經(jīng)抓不住的 輕聲嘆息 問何從何去 只見指縫間的黑粒 蒼白無力的吶喊 還停不住的音符的振動(dòng) 拼...
    框框a閱讀 210評(píng)論 0 1