美文网首页
企业模式

企业模式

作者: 简单asdf | 来源:发表于2017-10-25 08:09 被阅读0次

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

    • 架构概述:企业应用程序分层。
    • 注册(Registry)模式:管理应用程序数据。
    • 表现层:管理和响应用户请求,并把数据呈现给用户。
    • 业务逻辑层:处理系统的真实任务------解决业务问题。

    12.1 架构概述

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

    12.1.1 模式

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

    • 注册表:该模式用于使数据对进程中所有的类都有效。通过谨慎的序列化操作,注册表对象可以用于存储跨会话甚至跨应用实例的数据。
    • 前端控制器:在规模较大的系统中,该模式可用于尽可能灵活地管理各种不同的命令和视图。
    • 应用控制器:创建一个类来管理视图逻辑和命令选择。
    • 模板视图:创建模板来处理和显示用户界面,在显示标记中加入动态内容。尽量少使用原始代码。
    • 页面控制器:页面控制器满足和前端控制器相同的需求,但较为轻量级,灵活性也小一些。如果想快速得到结果而且系统也不太复杂的话,可以使用这种模式管理请求和处理页面逻辑。
    • 事务脚本:如果想要快速完成某个任务,可以使用本模式。通过简单的规划,用“过程式”的代码来实现程序逻辑。但本模式的可伸缩性不佳。
    • 领域模型:和事务脚本相反,使用本模式可以为业务参与者和过程构建基于对象的模型。

    12.1.2 应用程序和层

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

    图片.png

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

    • 视图层包括系统用户实际看到和交互的界面。它负责显示用户请求的结果及传递新的请求给系统。
    • 命令和控制层处理用户的请求。它委托业务逻辑层处理和满足请求,然后选择最合适的视图,将结果显示给用户。实际上,这个层和视图层常常合并为表现层。即使这样,显示的任务应当严格地与请求处理和业务逻辑调用分离开来。
    • 业务逻辑层负责根据请求执行业务操作。它执行需要的计算并整理结果数据。
    • 数据层负责保存和获取系统中的持久信息。在某些系统中,命令和控制层使用数据层来获取他所需要的业务对象。但在其他系统中,数据层通常尽可能地被隐藏起来。
      那么为什么要用这种方式划分系统呢?答案是解耦(decoupling)。通过分离业务逻辑层与视图层,当添加新的接口到系统时,系统内部只需要做很小的改动。
      假设有一个管理时间列表的系统。终端用户需要一个漂亮的HTML接口,而维护系统的管理员可能需要一个命令行接口来构建自动化系统,同时,你可能需要开发支持手机和其他手持设备访问的版本,甚至可能考虑使用REST式API或SOAP等协议。
      如果你以前把系统的底层逻辑和HTML视图层混合在一起(尽管这种写法备受批评,但在PHP项目中依然很普遍),上面所提的这些需求将会让你不得不重写代码。另一方面,如果已经创建了分层的系统,就可以直接使用新的显示方案而不用重新考虑业务逻辑和数据层。
      同样,项目的持久性策略也可能改变。你应该能够在对系统的其他层影响最小的情况下更换存储模型。
      将系统分层的另一个原因是测试。Web应用程序是很难测试的。任何一种自动测试在需要在一端解析HTML接口并在另一端使用在线数据库时都会很为难。也就是说,测试工作必须运行在完全部署的系统上,并且冒着破坏本应受保护的真实系统的风险。在分层系统中,任何需要与其他层直接打交道的类通常都扩展自抽象父类或者实现了同一个接口。这个父类型可以支持多态。在测试环境中,一个完整的层可以被一组虚拟的对象(通常称为stub或mock对象)所代替。例如,通过这种方法,我们可以使用虚拟的数据层来测试业务逻辑层。你可以在第18章读到更多关于测试的内容。
      即使系统只有一个简单的接口,并且你觉得测试时多余的时候,分层仍是非常有用的。通过创建独立分工的层,可以构建一个易于扩展和调试的系统。将具有同样功能的代码放在同一个地方可以减少代码重复(而不是将系统和数据库调用或显示方案绑定在一起),因此添加功能到系统中会相对简单,因为这些改变是纵向而不是横向的。
      在分层系统中,一个新功能可能需要一个新的接口组件、额外的请求处理、更多的业务逻辑和对存储机制的修改。这些修改是纵向的。在一个没有分层的系统中,如果要增加新的功能,则可能需要记住5个甚至更多和数据库相关的页面。新的接口可能会在数十个地方被调用,因此需要为系统增加这部分的代码。这就是横向的修改。
      当然,实际上我们并不能完全避免这种横向依赖,特别当修改页面的导航部分的时候。尽管如此,一个分层的系统有助于最小化横向修改的需要。
      本章所有例子都围绕一个虚拟的事件列表系统,系统的名称叫Woo,它是What's On Outside(外头发生了什么事)的缩写。
      系统由场所(venue,如剧院、俱乐部和电影院)、空间(space,如屏幕1和楼上)和事件(event,如电影The Long Good Friday、The Importance of Being Earnest)组成。
      本系统的操作包括创建场所、添加空间到场所和列出系统中的所有场所。
      记住,本章的目标是阐述主要的企业设计模式而不是构建一个实际系统。由于设计模式之间常常相互依赖,本章中的大部分示例也常常会互相重叠,以充分利用本章其他地方介绍的基础知识。本章的代码主要是用来解释企业模式的概念,因此无法符合实际系统中的所有标准,甚至为了简洁起见,还忽略了错误检查。读者应该把这些例子当成学习设计模式的途径,不要直接当做框架或程序中的一部分。

    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()方法来执行实际操作。

    • 应用程序助手
      ApplicationHelper类并不是前端控制器的核心,但前端控制器通常都需要通过应用助手类来获取基本的配置数据,因此我们我们需要讨论一下获取配置数据的策略。下面是一个简单的ApplicationHelper:
    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文件中。虽然这种写法有风险,但是代码执行效率最高。

    • 命令解析器
      控制器需要通过某种策略来决定如何解释一个HTTP请求,然后调用正确的代码来满足这个请求。你可以很容易地在Controller类中包含这个策略,但我更喜欢使用一个特定的类来完成这个任务。因为这样的代码易于重构和实现多态。
      前端控制器通常通过运行一个Command对象(本书在第11章中介绍过命令模式)来调用应用程序。Command对象通常根据请求中的参数或URL的结构(例如,可以使用Apache配置来确定URL中的哪个字段用于选择命令)来决定选择哪个命令。在下面的例子中,我们将使用一个简单的参数cmd。
      有多种方法可以用来根据给定的参数选择命令。你可以在一个配置文件或一个数据结构(逻辑方案)中测试该参数,或者直接查找文件系统(物理方案)中是否存在与参数相对应的类文件。
      逻辑方案更灵活一些,但是工作量也更大些(包括设置和维护)。你可以在12.3.2节找到使用该方案的例子。
      上一章介绍过一个使用物理方案的命令工厂的例子。下面对该例子做微小改动,使用反射(reflection)来增强安全性:
    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类时不需要提供参数:

    相关文章

      网友评论

          本文标题:企业模式

          本文链接:https://www.haomeiwen.com/subject/iuavuxtx.html