对于基本的Web开发,我们已经习惯了MVC架构。模型层(M)提供持久化数据对象与数据访问,控制层(C)完成业务逻辑处理,视图层(V)提供模板表现。其中控制层与模型层和视图层交互形成整个系统。

这种分层方式在逻辑上实现了解耦与分离,很多语言如Java和Python的框架都有各自的实现方式,如Struts采用Bean+JSP+Hibernate的方式实现。Django采用中间件的方式实现。无论是JAVA框架的实现方式还是Python框架的实现方式,都比较好的解决了MVC之间的交互问题,使得每个层次成为单独的构件,每一层的构件可复用在其它地方。如Java Bean除了可供JSP使用,还可包装成Soap服务。这样既保证了层之间的逻辑分离,也保证了层之间的物理分离。如Java Bean可以单独被部署到分布式应用服务器上。EJB就是一个很好的例子。而对于目前广泛应用的一些PHP框架,在实现上感觉缺少对物理独立性的考虑。

以Yii框架为例子来说,Yii框架的组织结构大致如下所示:

Site

|____resources

|____protected

|____config

|____controllers

|____models

|____views

|____components

|____framework

|____index.php

resources代表站点的一些资源性资料,如图片、样式表等。freamework为框架核心档。index.php为入口文件,所有访问路径均以index.php后跟参数为标准。而protected内就是业务逻辑需要的MVC三层及系统需要的一些配置信息了。protected内的componets下是系统的组件类,如果有与系统业务流程无关,但是在业务逻辑中需要的函数库,可以将它们作为组件放到这个目录下,如果配置文件内注册了该组件,在系统运行时会产生它们的实例以供调用。执行的流程大致如下图所示:


根据上图可以看出,Action需要同时与Model层与View层进行交互,在Action内,不仅有业务逻辑,还需调用Model层的方法和View层的方法。这样,业务逻辑与Model层、View层就存在了紧耦合的关系。这种层次结构应用在基本的网站上还是可以满足要求,但是不具备扩展性和可修改性。如果现在想把业务逻辑包装称REST风格或SOAP服务,那么只能重新写一次包含业务逻辑的Action,因为现在的Action里已经包含了页面的输出方法,而不是单纯的数据结果。对于需要使用同一动态数据展示不同表现形式的需求,也只能通过判断(修改)的方式而不能通过增加类或简单Action(扩展)的方式实现。这违反了面向对象的开放封闭原则。由于系统需求往往是频繁的变更的,如果我们常常的修改现有的代码,不仅会造成现有系统代码结构的混乱,而且极易造成隐藏的,难以发现的错误。所以,只有通过扩展来实现需求的变更,对系统来说才是最安全的。我们的工作才会更加的轻松和高效。

既然Yii提供的源码不能完美的解决扩展性和可修改性的问题,那么我们何不对它进行一次小小的改造,使它能胜任更加高的要求呢。俗话说的好,在软件的世界里,加一层能解决所有的问题。所以,我们也决定使用“加一层”的办法,以提高我们系统的可扩展性与可修改性。由于Yii是通过URI路由来查找Action并进行相应操作,最后也是通过Action方法产生响应结果的。所以,为了不破坏Yii的框架结构,我们只能在Action的下方增加一层,作为专门的业务逻辑层。为了偷懒,我们借鉴Java的叫法,称它为bean层。此时,Action就最为一个专门的包装层,包装bean的逻辑,然后与View层交互产生页面,或直接输出数据。这样如果我想把业务流程包装成SOAP服务,只需增加一个Action即可,无需重写业务逻辑。如果有多种视图显示要求,也无须重写业务逻辑,只需扩展Action即可。下面用代码来说明“这一层”是如何加上去的。

首先我们在protected目录下新建一个beans目录,然后在components内新建LoadBean.php文件,用于实例化bean类及获取bean对象。beans下的文件以XXXBean.php命名,如SiteBean.php。在beans下新建一个configure.php文件。此文件是bean加载的配置文件,以实现IOC之用。新结构如下所示:

Site

|____resources

|____protected

|____config

|____controllers

|____models

|____views

|____beans

|____configure.php

|____SiteBean.php

|____components

|____LoadBean.php

|____framework

|____index.php

beans下是专门负责业务逻辑的地方,Action内只需调用beans下的业务逻辑并提供输出即可,无需再写业务逻辑代码。为了达到以上要求,我们需要对Yii的代码做少许修改。首先找到框架核心内的CWebApplication.php文件,它的位置是framework/web/CWebApplication.php。 在该文件内添加一个getBeanPath与一个reloadBeans方法。代码如下:

publicfunctiongetBeanPath(){return$this->_controllerPath=$this->getBasePath().DIRECTORY_SEPARATOR.'beans';}publicfunctionreloadBeans(){$beanPath=$this->getBeanPath();if(is_dir($beanPath)){$current_dir=opendir($beanPath);while(($file=readdir($current_dir))!==false){if($file=='.'OR$file=='..')continue;require($beanPath.DIRECTORY_SEPARATOR.$file);}}}


然后找到该文件内的runController方法,在runController方法的开头处调用reloadBeans方法:

publicfunctionrunController($route){$this->reloadBeans();if(($ca=$this->createController($route))!==null){list($controller,$actionID)=$ca;$oldController=$this->_controller;$this->_controller=$controller;$controller->init();$controller->run($actionID);$this->_controller=$oldController;}elsethrownewCHttpException(404,Yii::t('yii','Unabletoresolvetherequest"{route}".',array('{route}'=>$route===''?$this->defaultController:$route)));}


第三步,打开刚才新建的LoadBean.php文件,添加以下代码:

classLoadBean{private$objs;publicfunctioninit(){$beanconfig=Yii::app()->basePath.'\beans\configure.php';$beans=require$beanconfig;foreach($beansas$bean){if(!$this->objs[$bean]instanceof$bean.'Bean'){$class=$bean.'Bean';if(class_exists($class)){$this->objs[$bean]=new$class($bean);}}}}publicfunctionobj($name){try{if(array_key_exists($name,$this->objs)){return$this->objs[$name];}else{thrownewException('beannameerror');}}catch(Exception$e){echo$e->getMessage();}}}


第四步,找到protected/config/main.php文件,添加以下代码:

'beans'=>array('class'=>'LoadBean',),

让系统在初始化时加载第三步添加的LoadBean类。

第五步,找到protected/beans/SiteBean.php,添加以下代码:

classSiteBeanextendsController{publicfunctionabc(){return123;}}

此abc方法即是我们的业务逻辑代码。

第六步,找到protected/beans/configure.php,添加以下代码:

returnarray('Site',);

此'Site'即为SiteBean类的'Site'名。

第七步,实现Action方法,找到protected/controllers/SiteController.php文件(如没有,可直接创建),代码如下:

classSiteControllerextendsController{publicfunctionactionIndex(){print_r(Yii::app()->beans->obj('Site')->abc());}}

Action类不直接与Bean类交互,而是通过组件类做代理进行通讯,使Action与Bean也实现了分离。


此时,利用浏览器访问http://yourdomain/index.php?r=Site/index,即可显示123。在此,我们实现了业务逻辑与Action的分离,增加了系统的扩展性与可修改性,bean实现了物理部署独立。形成了我们的四层架构。