命令模式最初來源于圖形化用戶界面設(shè)計(jì),但現(xiàn)在廣泛應(yīng)用于企業(yè)設(shè)計(jì)个初,特別促進(jìn)了控制器(請(qǐng)求和分發(fā)處理)和領(lǐng)域模型(應(yīng)用邏輯)的分離乖寒。命令模式有助于系統(tǒng)更好地進(jìn)行組織,并易于擴(kuò)展院溺。
- 問題
所有系統(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í)我們可以使用命令模式乌妒。 - 實(shí)現(xiàn)
命令對(duì)象的接口極為簡單,因?yàn)樗灰髮?shí)現(xiàn)一個(gè)方法execute()外邓。
在圖中撤蚊,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è)部分。