想想从一个电商网站上买一个东西,“进入首页,搜索商品,选型购买,登录下单,支付完成”,这里面有多少个对象。在我的理解中,对象是一个物,无论是一个真实的物还是一个虚拟的物,但不会是一个动作。比如“登录”是一个动作而不是一个物,所以“登录”不应该是一个对象(类定义),而应该是用户对象里面的一个方法。所以上面那一句的流程中,明显涉及的对象有:商品、用户、购物车、订单,隐含的对象有:页面控制器。
但是,随着业务的复杂,有些动作又会衍生出一个对象,比如当“登录”接入多种第三方OAuth登录时,就会多出一个“OAuth”控制器;“搜索”功能需要比较强大时,必然要加入一个“搜索引擎”控制器。有些对象可能又需要从中拆分出对象,比如“商品”对象可能会需要拆分出“属性参数”对象等等。这个问题似乎很复杂,但也并非无迹可循,我的经验是:从最直接的名词开始规划类和对象,动词和名词内部的名词根据发展需要再进行扩充。
对象规划与职能划分
什么叫职能划分,就是一个对象做它自身的事情。说起来似乎是一个很基本的原则,但是很遗憾,我看过的不少开源PHP项目都没有这个理念。在谈这个问题之前,我想先谈谈嵌入式C语言中的对象。
有研究过C语言和单片机的会知道,C语言里面一个C文件加上相对应的H文件就相当于一个类,是不是感觉和JAVA很像,一个文件就是一个类。当然这个不仅限于这两三门语言,应该是绝大部分语言所共同遵循的规则。在单片机程序开发中,一个硬件模块应该对应一个C和H文件,如一个温度传感器就应该有一个类似于 Temp.c和Temp.h的文件,里面的应该有的方法就是 初始化传感器 和 读取温度。在这个C文件(类)里面,所拥有的函数应该只和这个模块的功能(温度)相关,你可以添加类似于以字符串、符点数、整数返回等等的读取温度方法,但不能加上例如 “符点数转字符串” , “显示温度” 这样的方法。为什么呢?“符点数转字符串”虽然是这里面的一个必须方法,但它是一个通用方法,大部分其它地方都可能会用到,如果是自己实现的话那应该放到一个公共函数文件(如 pubfunc.c)里面去。当然C库里面已经有实现好了的就不要重复造轮子了。“显示温度”看似是一个跟温度非常相关的功能,但是它跟温度传感器并没有关联。你可能会用黑白屏显示、也可能会用彩色屏显示、也可能会用语音播报,这种情况就不能把显示温度放在Temp.c里面,而是应该在业务C文件里把数据读出来运算处理,最后把数据传到对应的显示设备的C文件中去展示。
说到这里,大家可能已经隐约感觉到分层这个理念了,分层稍后再提,先看回网站中应该怎么对象划分。PHP中有函数和方法两种不同的function,函数是应该是公共的,就像前面提到的pubfunc.c一样,还有一些类也是公共的,比如分页类、加密类等,这些文件里面不应该与项目的业务逻辑有耦合关系,应该拿出来给另外一个项目也是通用的。而另一方面的项目功能模块呢,应该是职责明确的,比如用户控制器就应该有读写用户信息、登录注册等等,而不应该有订单数量这种东西。
为什么要MVC和怎么MVC
MVC即是模型-视图-控制器的意思,但实践中,我发现这种统一的MVC说法并不能适应到程序编程的各行各业。毕竟编程有 嵌入式开发、电脑软件开发、手机APP开发、网站开发、游戏开发等等,对应不同的场景应该会有略有不同的具体实现。在此我仅对我自己所使用的网站MVC模式作出介绍,有不当之处恳请提出。
如下图所示,浏览器发出的请求分成两大类,一类是页面请求(红色箭头 蓝色箭头),一类是AJAX纯数据请求(绿色箭头 蓝色箭头),服务器上的代码资源也分为两类,一个是PHP框架的(青底黑框表示),一个是自主开发的(白底蓝框表示)。
1、浏览器发出到服务器,框架通过URL路由分发请求到控制器里,当中可能会做了URL优化什么的。
2、页面请求(根据URL判断)全部转发到页面控制器中(暂时只有IndexController一个),然后调用逻辑控制器;AJAX请求则直接分发到对应的逻辑控制器,逻辑控制器通过一定的策略判断需要AJAX返回还是函数return(如可选参数)。
3、比较简单的逻辑直接在逻辑控制器中处理,直接使用“表模型”访问数据库,我这里说的“表模型”是指没有定义Model类,但是使用对象的方式去操作数据库,通常以表为操作单位,相当于ThinkPHP框架中的M()方法。
4、 对于比较复杂的逻辑,可以进一步封装在一个Model模型中,Thinkphp中称为“虚拟模型”,是指这个模型不一定会有对应的数据表,当然也可能有对 应的表。对于到达何种复杂度就封装到Model中,我经验不足暂无法下定论,因为现在为止我的项目还没有使用“虚拟模型”,也就是说我把MVC三层中把C 层拆分出了两层,而M层至今留空。至于为何这样做,稍后再分析。
5、到这里已经到达了数据库了,取回数据顺着蓝色箭头反方向返回,数据再次 来到了逻辑控制器。如果是AJAX数据请求,则直接echo输出数据或者操作结果,或者用TP内置的ajaxReturn()方法,两者有数据 header的区别,至此AJAX请求就处理结束了,剩下就交给前端JS去处理了。如果是页面请求,则把数据返回给页面控制器,注意这里是函数 return而不是打印输出。
6、页面控制器收集好各个调用到的逻辑控制器返回的数据,利用框架内置的模板引擎或者Smarty引擎,将数据赋值到页面文件中,最后渲染页面输出。
多用户端(模块)和继承
前文再续就书接上一回,上回讲到 我的项目中M层一直为空的。为什么呢?网站这一种程序,通常都会有多端的情况,就是会有 PC端、WAP端、管理端、APP端等等,这个在Thinkphp3.2中称为“模块”。我目前项目中就有 Home(PC端)、Mobile(移动端)、Admin(管理端) 三大模块了。那三大模块就写三份程序吗?显然不应该这样,因为它们之间绝大部分的逻辑是相同的,应该使用继承,而我们的项目中 Home 模块功能最基础、Mobile次之,Admin则是权限最高的模块,大部分写/修改操作只允许在Admin模块中有。所以我项目现有的结构是 Home模块中的类是基类,Mobile继承Home,Admin也继承Home,以后如果感觉有必要的话,也可能会使用一个Common模块,Home、Mobile、Admin都继承于Common。
然后这样的关键就来了,既然有那么多的模块,那么多的类,那么多的模型,如果要新增一个功能那应该写在哪里呢?我们的决定是,很长一段时间内都暂不使用Model类,避免大量继承过来并没有新增功能的“空Model”而导致找查找代码浪费时间;对于新增的功能,如果是一类全新的功能,比如折扣功能,则在几个模块中都新建一个逻辑控制器文件,继承方式同前文所提。新的方法如果可能在PC和移动端都用到的话,就写在Home的控制器里面,如果只有移动端用到的话就写在Mobile的控制器里面,如果是后台管理方面的功能则写在Admin的控制器里面。
实践证明,这样一个决策是可行的,直到现在Mobile模块里面还有半数的控制器是空的(即与PC一致没有新增功能),如果是加上了Model层的话,目测各个类里面的方法会写得很分散,各种继承错综复杂,那些方法要到各个模块下的控制器、模型各种到处找。
我是PHP程序猿,我只有一半对象(PHP的吉祥物是一只象,即半对象,而且PHP可以用或完全不用对象来写程序