企业模式

2017-10-25  本文已影响0人  简单asdf

PHP是一个为Web开发而设计的语言。在PHP5之后,PHP开始大力支持对象,因此你可以享受到设计模式带来的好处,就像使用其他面向对象语言(特别是Java)那样。
本章将举一个简单的例子来说明设计模式的使用。请注意,选择使用某种模式时,并不一定也要使用与该模式配合良好的其他所有模式,而且示例中介绍的部署这些模式的方法也不是唯一可行的方法。示例主要用于帮助理解模式的核心思想,你可以从中提取自己需要的内容,并应用于实际开发当中。
本章介绍的内容很多,是全书最长和最复杂的章节,相信读者很难一次性读完。本章分为一个简要介绍和两个主要部分。你可以在读完其中某部分时休息一下。
12.1节中介绍了一些独立的模式。尽管这些内容有时是相互关联的,但可以直接跳到任何一个你想了解的模式进行阅读和学习,在你有空的时候再阅读其他相关模式的内容。
本章包括以下内容。

12.1 架构概述

因为涉及的内容比较广泛,所以首先概述一下模式,然后介绍如何构建分层的应用程序。

12.1.1 模式

下面是本章将要讨论的设计模式。你可以从头看到尾,也可以根据需要和兴趣选择性地阅读。注意命令模式没有在此单独介绍(在第11章介绍过),但会在前端控制器和应用控制器模式中提及

12.1.2 应用程序和层

本章大部分模式是用来使程序中不同的“层”(tier,也称为layer)独立工作的。就像类的使命是执行特定的任务,企业应用系统中的层也是如此,不过更为粗犷。图12-1展示了一个系统中分工明确的各个层。

图片.png

图12-1所示的结构并不是固定不变的,其中一些层可以合并,而且层之间的交互策略可能根据系统的复杂度而不同。无论如何,图12-1展示的模型强调灵活性和重用,而许多企业应用就是根据“灵活”和“重用”的原则进行扩展的。

12.2 企业架构之外的基础模式

12.2.1 注册表

12.3 表现层

当一个请求到达系统时,系统必须能够理解请求中的需求是什么,然后调用适当的业务逻辑进行处理,最后返回相应结果。对于简单的程序,整个过程可能完全放在视图之中,只有重量级的逻辑和持久化操作相关的代码才放在封装好的类库中。
注解:一个视图是指视图层中一个单独的元素。它可能是一个PHP页面(或视图元素集合),负责显示数据和让用户生成新请求。在像Smarty这样的模板系统中,一个视图即指一个模板。
随着系统的增长,这种默认方案不能满足处理请求、调用业务逻辑和派发视图的要求。
本节我们将研究表现层管理以上3个主要功能的策略。视图层与命令和控制层的边界通常很模糊,因此我们常把这两个层统称为“表现层”。

12.3.1 前端控制器

本模式和传统PHP应用程序的“多入口”方式相反。前端控制器模式用一个中心来处理所有到来的请求,最后调用视图来讲结果呈现给用户。前端控制器模式是Java企业应用的核心模式之一。本模式在《J2EE核心模式》中有详细的讲解,它同时也是最有影响力的企业模式之一。在PHP中,这个模式并没有受到广泛喜爱,部分原因是初始化前端控制器所需要的开销会导致系统性能下降。
现在我写的大部分系统都开始向前端控制器模式转移。虽然我有时没有使用完整的前端控制器模式,但是我发现在项目中使用前端控制器模式确实可以提供我需要的灵活性。

  1. 问题
    当请求可以发送到系统中多个地方时,我们很难避免代码重复。你可能需要验证用户、把术语翻译成多种语言或者只访问公用数据。当多个页面都要执行同一个操作时,我们可以从一个页面复制该操作相关的代码并粘贴到另一个页面。但是这样的话,当需要修改系统中某个部分时,其他部分也要随着改变,给代码维护带来困难。因此我们要尽量避免这种情况。当然,首先要做的是把公共操作集中到类库代码中。但即使这样,对库函数和方法的调用代码仍然会分布到系统中各个部分。
    当系统控制器和视图混杂在一起时,管理视图的切换和选择是另一个难点。在一个复杂的系统中,随着输入和逻辑层中操作的成功执行,一个视图中的提交动作可能会产生任意数目的结果页面。从一个视图跳到另一个视图时,可能会产生混乱,特别当某个视图被用在多个地方的时候。
  2. 实现
    在核心部分,前端控制器模式定义了一个中心入口,每个请求都要从这个入口进入系统。前端控制器处理请求并选择要执行的操作。操作通常都定义在特定的Command对象中。Command对象是根据命令模式组织的。
    图12-4展示了一个前端控制器的结构。
图片.png

实际开发时,你可能会部署一些助手类来协助控制器的处理过程,但现在我们还是先从控制器的核心部分开始研究。下面是一个简单的Controller类:

namespace woo\controller;

//...
class Controller{
    private $applicationHelper;
    private function __construct(){}

    static function run(){
        $instance = new Controller();
        $instance->init();
        $instance->handleRequest();
    }

    function init(){
        $applicationHelper = ApplicationHelper::instance();
        $applicationHelper->init();
    }

    function handleRequest(){
        $request = new \woo\controller\Request();
        $cmd_r = new \woo\command\CommandResolver();
        $cmd = $cmd_r->getCommand($request);
        $cmd->execute($request);
    }
}

这个Controller类非常简单,而且没有考虑错误处理。系统中的控制器负责分配任务给其他类。其他类完成了绝大部分实际工作。
run()只是一个便捷方法,用于调用init()和handleRequest()。run()是静态方法,而且本类的构造方法被声明为private,因此客户端代码只能通过run()方法来实例化控制器类,并执行相关操作。我们可以使用只包含两行代码的index.php文件来完成这个工作:

require("woo/controller/Controller.php");
\woo\controller\Controller::run();

init()和handleRequest()方法的不同体现了PHP的特性。在某些编程语言中,init()只在应用第一次启动时运行,而handleRequest()在用户的每个请求到来时运行。尽管init()在每次请求中都会被调用,但是这个类还是注意到了启动和请求处理间的区别。
init()方法中获得ApplicationHelper(应用程序助手)类的一个对象实例。这个类的作用是管理应用程序的配置信息。控制器的init()方法调用ApplicaiontHelper中同名的init()方法,用于初始化应用程序要使用的数据。
handleRequest()方法通过CommandResolver来获取一个Command对象,然后调用Command对象的execute()方法来执行实际操作。

namespace woo\controller;
// ...
class ApplicationHelper{
    private static $instance;
    private $config = "/tmp/data/woo_options.xml";

    private function __construct(){}

    static function instance(){
        if(!self::$instance){
            self::$instance = new self();
        }
        return self::$instance;
    }

    function init(){
        $dsn = \woo\base\ApplicationRegistry::getDSN();
        if(!is_null($dsn)){
            return;
        }
        $this->getOptions();
    }

    private function getOptions(){
        $this->ensure(file_exists($this->config),"Could not find options file");
        $options = SimpleXml_load_file($this->config);
        print get_class($options);
        $dsn = (string)$options->dsn;
        $this->ensure($dsn, "No DSN found");
        \woo\base\ApplicationRegistry::setDSN($dsn);
        // 设置其他值
    }

    private function ensure($expr, $message){
        if(!$expr){
            throw new \woo\base\AppException($message);
        }
    }
}

这个类的作用是读取配置文件中的数据并使客户端代码可以访问这些数据。可以看到,这个类实现了单例模式。使用单例模式使它能够为系统中所有的类服务。另外,你也可以把这个类的代码当成一个标准类并确保它被传递给其他感兴趣的对象。本书在第9章及本章的前面部分已经讨论了使用单例模式需要注意的问题。
现在我们已经实现了ApplicationRegistry(应用注册表),我们还应重构代码,把ApplicationHelper改写为注册表,而不是两个任务重叠的单例对象。重构代码的建议前一节中已经提过(将ApplicationRegistry的核心功能从领域对象的存取中分离出来),留给读者当做练习。
因此init()方法只负责加载配置数据。实际上,它检查ApplicationRegistry,看数据是否已经被缓存。如果Registry对象中的值已经存在,init()就什么都不做。如果系统初始化要做大量工作,这样的缓存机制是很有用的。在将应用程序初始化和独立请求相分离的编程语言中,可以使用复杂的初始化操作。但在PHP中,你不得不尽量使用缓存来减少初始化操作。
缓存可以有效地保证复杂而且耗费时间的初始化过程只在第一次请求时发生,而之后所有的请求都能从中受益。
如果是第一次运行(或者缓存文件已被删除------这是一种简单而有效的强制重新读取配置信息的方法),getOptioins()方法将被调用。
在现实世界中,我们需要做比示例代码更多的工作。示例中所做的工作只是获取一个DSN。首先,getOptions()方法检查配置文件是否存在(路径存放在$config属性中),然后从配置文件中加载XML数据并设置DSN。
注解:在这些例子中,ApplicationRegistry和ApplicationHelper都使用了硬编码的文件路径。在实际项目中,这些文件路径应该是可配置的而且可以从一个注册表对象或一个配置对象中获取。实际的路径可以在安装时用构建工具(如PEAR或Phing,参见第15章和第19章)来设置。
注意类中使用了一个技巧来抛出异常,避免了在代码中到处使用下面这样的条件语句和throw语句:

if(!file_exists($this->config)){
    throw new \woo\base\AppException("Could not find options file");
}

这个技巧就是ApplicationHelper类在ensure()方法中集合了检测表达式和throw语句。只用一行代码就能确定条件是否为真(如果不为真,则抛出异常):

$this->ensure(file_exists($this->config),"Could not find options file");

缓存对系统开发者和使用者都有好处。系统可以方便地维护一个易于使用的XML配置文件,同时使用缓存意味着系统能以很快的速度访问配置文件中的数据。当然,如果类的用户还是程序员,或者并不经常修改配置,你可以直接在助手类中(或者是用一个单独的文件)包含PHP数据结构,不用把配置数据单独放到XML文件中。虽然这种写法有风险,但是代码执行效率最高。

namespace woo\command;
//...
class CommandResolver{
    private static $base_cmd;
    private static $default_cmd;

    function __construct(){
        if(!self::$base_cmd){
            self::$base_cmd = new \ReflectionClass("\woo\command\Command");
            self::$default_cmd = new DefaultCommand();
        }
    }

    function getCommand( \woo\controller\Request $request){
        $cmd = $request->getProperty('cmd');
        $sep = DIRECTORY_SEPARATOR;
        if(!$cmd){
            return self::$default_cmd;
        }
        $cmd = str_replace(array('.', $sep), "", $cmd);
        $filepath = "woo{$sep}command{$sep}{$cmd}.php";
        $classname = "woo\\command\\{$cmd}";
        if(file_exists($filepath)){
            @require_once("$filepath");
            if(class_exists($classname)){
                $cmd_class = new ReflectionClass($classname);
                if($cmd_class->isSubClassOf(self::$base_cmd)){
                    return $cmd_class->newInstance();
                }else{
                    $request->addFeedback("command '$cmd' is not a Command");
                }
            }
        }
        $request->addFeedback("command '$cmd' not found");
        return clone self::$default_cmd;
    }
}

这个简单的类用于查找请求中包含的cmd参数。假设参数被找到,并与命令目录中的类文件相匹配,而该文件也正好包含了cmd类,则该方法创建并返回相应类的实例。
如果其中任意条件未满足,getCommand()方法使用默认的Command对象。
你或许想知道,为什么实例化Command类时不需要提供参数:

上一篇 下一篇

猜你喜欢

热点阅读