命令模式

命令模式最初来源于图形化用户界面设计,但现在广泛应用于企业设计,特别促进了控制器(请求和分发处理)和领域模型(应用逻辑)的分离。命令模式有助于系统更好地进行组织,并易于扩展。

  1. 问题
    所有系统都必须决定如何响应用户请求。在PHP中,这个决策过程通常是由分散的各个PHP页面来处理。比如当用户访问一个PHP页面时,用户明确地告诉系统他所要求的功能和接口。但现在PHP开发者日益倾向于在设计系统时采用单一入口的方式。无论是多个入口还是单个入口,接收者都必然将用户请求委托给一个更加关注于应用逻辑的层来进行处理。这个委托在用户请求不同页面时尤为重要。如果没有委托,代码重复将会不可避免地蔓延在整个项目中。
    让我们想象一下,假设一个有很多任务要执行的项目,需要允许某些用户登录,某些用户可以提交反馈。我们可以分别创建login.php和feedback.php页面来处理这些任务,并实例化专门的类以完成任务。不过遗憾的是,系统中的用户界面很难被精确地一一对应到系统任务。比如我们可能要求每个页面都有登录和反馈的功能。如果页面必须处理很多不同的任务,就应该考虑将任务进行封装。封装之后,向系统增加新任务就会变得简单,并且可以将系统中的各部分分离开来。当然,这时我们可以使用命令模式。
  2. 实现
    命令对象的接口极为简单,因为它只要求实现一个方法execute()。
1.png

在图中,Command被定义为一个抽象类。同样简单地,它也可以被定义为接口。将它定义为抽象类,因为有时基类也可以为它的衍生对象提供有用的公共功能。
命令模式由3部分组成:实例化命令对象的客户端(client)、部署命令对象的调用者(invoker)和接受命令的接收者(receiver)。
通过客户端,接收者可以在命令对象的构造方法中被传递给命令对象,或者通过某种工厂对象被获得。相对而言,后一种办法可以保持构造方法参数清晰明了,而且所有的Command对象都可以用完全相同的方式实例化。
创建一个具体的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被设计为与AccessManager(访问管理器)对象一起工作。AccessManager是一个虚构出来的类,它的任务就是处理用户登录系统的具体细节。注意Command::execute()方法要求使用CommandContext对象作为参数。通过CommandContext机制,请求数据可以被传递给Command对象,同时相应也可以被返回到视图层。以这种方式使用对象是很有好处的,因为我们可以不破坏接口就把不同的参数传递给命令对象。从本质上说,CommandContext只是将关联数组变量包装而成的对象,但我们仍会经常扩展它来执行额外的任务。下面是一个简单的CommandContext实现:

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对象,LoginCommand能够访问请求数据:提交的用户名和密码。我们使用了一个简单的类Registry,它带有用于生成通用对象的静态方法,可以返回LoginCommand所需要的AccessManager对象。如果AccessManager报告一个错误,则LoginCommand保存错误信息到CommandContext对象中以供表现层使用并返回false。如果一切正常,LoginCommand只返回true。注意Command对象不应该执行太多的逻辑。它们应该负责检查输入、处理错误、缓存对象和调用其他对象来执行一些必要的操作。如果你发现应用逻辑过多地出现在Command类中,通常需要考虑重构代码。这样的代码会导致代码重复,因为它们不可避免地会在不同的Command类中被复制粘贴。你至少需要考虑这些应用逻辑的功能应该属于哪部分代码。最好把这样的代码迁移到业务对象中或者放入一个外观层中。现在我们仍然缺少客户端代码(即用于创建命令对象的类)及调用者类(使用生成的命令的类)。在一个Web项目中,选择实例化哪个命令对象的最简单的办法是根据请求本身的参数来决定。下面是一个简化的客户端代码:

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对象的$action参数来构造的,该参数是从请求中被传到系统中的。如果文件和类都存在,那么会返回命令对象给调用者。我们可以在这里添加更多的错误检查,比如保证找到的类是Command类的子类,保证构造方法没有参数等,但目前的版本对我们来说已经足够说明问题。这种方式的优点是你可以随时将新的Command类添加到commands目录下,然后系统便立即支持它了。
下面是一个简单的调用者:

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{
            //成功
            //现在分发试图
        }
    }
}

$controller = new Controller();
//伪造用户请求
$context = $controller->getContext();
$context->addParam('action', 'login');
$context->addParam('username', 'bob');
$context->addParam('pass', 'tiddles');
$controller->process();

在调用Controller::process()之前,我们通过在控制器的构造函数中实例化的CommandContext对象上设置参数伪造了一个Web请求。process()方法将实例化命令对象的工作委托给CommandFactory对象,然后它在返回的命令对象上调用execute()方法。注意,控制器对命令内部是一无所知的。因为命令执行的细节与控制器是相互独立的,所以我们可以随时添加新的Command类而对当前的结构影响很小。
让我们再创建一个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;
    }
}

当这个类以FeedbackCommand.php的文件名来保存,并保存在正确的Commands目录下时,它就会被调用来响应Action为feedback的请求,而不需要对控制器或者CommandFactory做任何修改。
图11-9展示了命令模式的各个部分。

图片.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容