命令模式最初来源于图形化用户界面设计,但现在广泛应用于企业应用设计,特别促进了控制器(请求和分发处理)和领域模型(应用逻辑)的分离。命令模式有助于系统更好的进行组织,并易于扩展。
所有系统都必须决定如何响应用户请求。在 PHP 中,这个决策过程通常是由分散的各个 PHP 页面来处理。现在的 PHP 开发者日益倾向于在设计系统时采用单一入口的方式,接受者将用户请求委托给一个更加关注于逻辑的层来进行处理。这个委托在用户请求不同页面时尤为重要。如果没有委托,代码重复将会不可避免地蔓延在整个项目中。
假设一个有很多任务要执行的项目,需要允许某些用户登录,某些用户可以提交反馈。我们可以分别创建 login.php 和 feedback.php 页面来处理这些任务,并实例化类似于 login.class.php 和 feedback.class.php 这样专门的类来处理这些任务。不过对于一个项目而言,麻烦的是,可能很多页面都要有登录和反馈的功能。
如果页面必须处理很多不同的任务或者一个任务需要被很多页面调用,比如前面提到的登录和反馈,就应该考虑把这些任务进行封装。封装之后,向系统增加新任务就会变得简单,并且可以将系统中的各部分分离开来。这种思路就是命令模式。
命令对象的接口只有一个 execute() 方法,并且将其定义为抽象类。
命令模式由 3 部分组成:
首先我们来看 Command 类,用于命令的接受:
/* Command接口只有一个 execute() 方法 */
abstract class Command {
abstract function execute(CommandContext $context) {}
}
/* LoginCommand类 处理用户登录系统的具体细节 */
class LoginCommand extends Command {
function execute(CommandContext $context) {
$manager = Registry::getAccessManager();
$user = $context->get('username');
$pass = $context->get('pass');
$user_obj = $manager->login($user, $pass);
if (is_null($user_obj)) {
$context->setError($manager->gerError());
return false;
}
$context->addParam("user", $user_obj);
return ture;
}
}
Command 类保持简洁,子类 LoginCommand 类用于处理用户登录系统的具体细节。在 Command::execute() 中需要传入一个 CommandContext 对象作为参数。通过 CommandContext 机制,请求数据可以被传递给 Command 对象,同时响应也可以被返回到视图层。
以下是 CommandContext 的实现:
/* CommandContext将关联数组变量包装成对象,用于将请求数据传递给Command类 */
class CommandContext {
private $params = array();
private $error = "";
function __construct() {
$this->params = $_REQUEST;
}
function addParam($key, $val) {
$this->params[$key] = $val;
}
function getParam($key) {
return $this->params[$key];
}
function setError($error) {
$this->error = $error;
}
function getError() {
return $this->error;
}
}
因此,通过 CommandContext 对象,LoginCommand 能够访问请求数据:提交的用户名和密码。
现在,我们建立起客户端代码:
class CommandNotFoundException extends Exception {}
/* CommandFactory类在commands目录里查找指定的类文件 */
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;
}
}
这个类用于在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', 'abc');
$context->addParam('pass', '123');
$controller->process();
最后的 process() 方法将实力化命令对象的工作委托给 CommandFactory 对象,然后它在返回的命令对象上调用 execute() 方法。控制类可以在对命令内部一无所知的情况下工作,这样就使得命令执行的细节和控制器之间相互独立,便于我们可以随时扩展新的 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 = $context->send($email, $msg, $topic);
if (!$result) {
$context->setError($msgSystem->getError());
return false;
}
return true;
}
}
对于控制器而言,只需要发送 action 为 feedback 的请求过去,就能获取到正确的响应,而不需要对控制器或者 CommandFactory 做任何修改。
以下是命令模式的各个部分: