当前位置: 首页 > 新闻 > 信息荟萃
编号:216
PHP核心技术与最佳实践.pdf
http://www.100md.com 2019年12月20日
第1页
第8页
第15页
第24页
第35页
第739页

    参见附件(31714KB,807页)。

     PHP核心技术与最佳实践为用户提供深入的编程思想学习,让用户能够更全的了解底层原理、核心技术和开发技巧,帮助PHP程序员能够更好的进行提升自身技术。

    PHP核心技术与最佳实践内容

    本书是一本致力于为希望成为中高级PHP程序员的读者提供高效而有针对性指导的经典著作。

    全书分为5个部分:第一部分(1~2章)从不同的角度阐述了面向对象软件设计思想的核心概念、技术和原则,分析了面向对象的特性、设计模式的理念,指出了如何设计低耦合、高可扩展性的软件,等等;第二部分(3~6章)详细讲解了PHP中正则表达式的规范和使用技巧,PHP网络编程的原理、方法、技巧和一些重要的操作,PDO、数据库应用优化,数据库设计和MySQL的高级应用,PHP扩展引擎的原理与实践;第三部分(第7章)拨云见日,围绕PHP扩展开发进行了细致而深入的探讨,解析了PHP的底层实现和Zend虚拟机API,并用PHP扩展开发的实例带领读者走进PHP的底层世界,旨在让读者对PHP性能优化、底层原理进行深入的理解。第四部分(8~11章)重点讨论了缓存的设计、Memcached的原理与实践、NoSQL数据库Redis源码分析与应用实践、高性能PHP网站的架构和设计等内容;第五部分(12~14章)详细讲解了PHP代码的调试和测试、Hash算法和数据库的实现,以及PHP的编码规范,旨在帮助读者提高开发效率,养成良好编程习惯。

    PHP核心技术与最佳实践作者

    列旭松,资深PHP技术工程师,精通PHP及其相关技术,对PHP内核原理有较深入的理解,开发经验丰富。曾自主开发了关键字匹配服务器和消息队列SquirrelMQ。平时喜欢开发一些实用的PHP扩展,如PHP字典扩展(红黑树算法)和PHP索引扩展(B+树算法)。精通C语言,同时对Web服务器的架构和优化、高并发服务端编程、Redis和Memcached等技术有深入的研究和认识。活跃于PHPChina和ChinaUnix等专业社区,担任PHPChina论坛内核版块版主。

    陈文,资深PHP技术工程师,精通PHP及其相关技术,尤其擅长于PHP框架开发和应用架构。他还是一位资深的Java开发工程师,具有Fortran、Scala和C++语言的开发和使用背景,在传统软件和互联网开发领域都有丰富的实战经验。此外,他还擅长TCP/IP编程、多线程与并发程序设计、网络协议分析、数据库性能优化以及各种缓存技术,熟悉MySQL和Oracle等关系数据库产品。现从事网络安全软件开发,以及移动SI业务开发。对语言特性和软件设计思想有独到的见解,追求代码之美和高效率程序开发,爱好钻研底层技术,崇尚和提倡“以理论指导实践”。尤其爱好数学,认为数学是培养和锻炼思维和逻辑能力的重要工具,对算法有一定研究。长期活跃在PHPChina、ITeye和看雪论坛等社区,在PHPChina社区担任版主。

    PHP核心技术与最佳实践章节预览

    第1章 面向对象思想的核心概念

    第2章 面向对象的设计原则

    第3章 正则表达式基础与应用

    第4章 PHP网络技术及应用

    第5章 PHP与数据库基础

    第6章 PHP模板引擎的原理与实践

    第7章 PHP扩展开发

    第8章 缓存详解

    PHP核心技术与最佳实践截图

    PHP核心技术与最佳实践

    列旭松 陈文 著

    ISBN:978-7-111-40192-6

    本书纸版由机械工业出版社于2012年出版,电子版由华章分社(北京华

    章图文信息有限公司)全球范围内制作与发行。

    版权所有,侵权必究

    客服热线:+ 86-10-68995265

    客服信箱:service@bbbvip.com

    官方网址:www.hzmedia.com.cn

    新浪微博 @研发书局

    腾讯微博 @yanfabook目录 前言

    为什么要写这本书

    本书适合的对象

    如何阅读本书

    勘误和支持

    感谢

    第1章 面向对象思想的核心概念

    1.1 面向对象的“形”与“本”

    1.2 魔术方法的应用

    1.3 继承与多态

    1.4 面向接口编程

    1.5 反射

    1.6 异常和错误处理

    1.7 本章小结

    第2章 面向对象的设计原则

    2.1 面向对象设计的五大原则

    2.2 一个面向对象留言本的实例

    2.3 面向对象的思考

    2.4 本章小结

    第3章 正则表达式基础与应用

    3.1 认识正则表达式

    3.2 正则表达式中的元字符

    3.3 正则表达式匹配规则

    3.4 构造正则表达式

    3.5 正则在实际开发中的应用

    3.6 正则表达式的效率与优化

    3.7 本章小结

    第4章 PHP网络技术及应用

    4.1 HTTP协议详解

    4.2 抓包工具

    4.3 Socket进程通信机制及应用

    4.4 cURL工具及应用

    4.5 简单邮件传输协议SMTP

    4.6 WebService的前世今生

    4.7 Cookie详解4.8 Session详解

    4.9 本章小结

    第5章 PHP与数据库基础

    5.1 什么是PDO

    5.2 数据库应用优化

    5.3 数据库设计

    5.4 MySQL的高级应用

    5.5 本章小结

    第6章 PHP模板引擎的原理与实践

    6.1 代码分层的思想

    6.2 实现一个简单的模板引擎骨架

    6.3 模板引擎的编译

    6.4 完善模板引擎

    6.5 常用模板引擎

    6.6 本章小结

    第7章 PHP扩展开发

    7.1 为什么要开发PHP扩展

    7.2 搭建PHP扩展框架

    7.3 PHP内核中的变量

    7.4 PHP内核中的HashTable分析

    7.5 Zend API详解与扩展编写

    7.6 编写一个完整的扩展

    7.7 本章小结

    第8章 缓存详解

    8.1 认识缓存

    8.2 文件缓存

    8.3 Opcode缓存

    8.4 客户端缓存

    8.5 Web服务器缓存

    8.6 本章小结

    第9章 Memcached使用与实践

    9.1 为什么要用Memcached

    9.2 Memcached的安装及使用

    9.3 深入了解Memcached

    9.4 Memcached分布式布置方案

    9.5 本章小结

    第10章 Redis使用与实践10.1 Redis的安装及使用

    10.2 事务处理

    10.3 持久化

    10.4 主从同步

    10.5 虚拟内存

    10.6 扩展库phpredis安装及使用

    10.7 Redis应用实践

    10.8 深入了解Redis内核

    10.9 本章小结

    第11章 高性能网站架构方案

    11.1 如何优化网站响应时间

    11.2 MySQL响应速度提高方案:HandlerSocket

    11.3 MySQL稳定性提高方案:主从复制

    11.4 Web应用加速方案:Varnish

    11.5 异步计算方案:Gearman

    11.6 本章小结

    第12章 代码调试和测试

    12.1 调试PHP代码

    12.2 前端调试

    12.3 日志管理

    12.4 代码性能测试技术

    12.5 单元测试

    12.6 压力测试

    12.7 本章小结

    第13章 Hash算法与数据库实现

    13.1 Hash函数

    13.2 Hash算法

    13.3 Hash表

    13.4 一个小型数据库的实现

    13.5 本章小结

    第14章 PHP编码规范

    14.1 文件格式

    14.2 命名规范

    14.3 注释规范

    14.4 代码风格

    14.5 本章小结前言

    为什么要写这本书

    近几年,市场上关于PHP的书已经很多了,各种培训机构也如雨后

    春笋般不断增加。那为什么还要写这本书呢?这本书存在的意义又在哪

    里?这要从下面的几个问题说起。

    有没有这样一本PHP教材,它不讲HTML和CSS,也不讲JavaScript

    基础,甚至不讲PHP语法基础?

    有没有这样一本PHP教材,它不讲留言本或博客的开发,也不讲数

    据库的CRUD操作?

    有没有这样一本PHP教材,它专注于Web开发技术的最前沿,深入

    浅出,适合中高级程序员的进阶和提高?

    有没有这样一本PHP教材,它提倡面向对象的程序思想,提倡算法

    和数据结构的重要性,提倡对网络协议的深入理解,且没有大篇幅的代

    码,而是更多偏重于理论讲解?

    有没有这样一本PHP教材,它探讨PHP的扩展开发,探讨高并发大

    流量的架构,深入探讨NoSQL的内部实现和细节?

    以上几个问题也是我在早期PHP学习的过程中一直在寻找的答案,可是我并没有找到一本理想的PHP书籍,一本适合中高级程序员进阶的

    书籍。当怀着同样问题的旭松兄找到我时,我们不禁产生一个念

    头:“既然现在市场上缺少一本这样的书籍,我们何不自己写一本呢?

    利己利人的事值得去做。”然后一拍即合,说做就做,现在这本书经历

    长达一年多的酝酿和写作过程终于完稿了。

    我是在大学期间接触到PHP语言的,并马上被其简洁的语法和极高

    的开发效率所吸引,一头扎进PHP开发的世界中。随着学习的深入,并

    经常关注PHP社区的动态,我很快意识到一些PHP社区普遍存在的问

    题。比如PHP社区一直争论算法重不重要,面向对象好不好,代码质量

    重要还是开发速度重要的问题。还有譬如为什么我去大型互联网公司应聘PHP程序员,却不考察我对PHP语法和函数的掌握情况,而是会问我

    C语言、算法、网络协议、高并发处理、MVC理论这些看似和PHP不沾

    边的问题。

    PHP到底要怎么学,学什么,一个高级PHP程序员应该是什么样

    的,我想这也是很多PHP新手和工作一两年的PHP开发者的疑惑。这本

    书所要解决的就是这一系列的问题。

    在我看来,一本技术书籍的价值在于其对知识的提炼和与众不同的

    地方。举例来说,到一个书店去看书,你最想用笔抄下来或撕下来带走

    的那几页,就是对你帮助最大的东西,也是你认为这本书的价值所在。

    也是基于这个想法,我们思考这本书该写什么,怎么写,哪些地方对读

    者有帮助。我们试图从不同的角度带领读者来看PHP,进而给这本书注

    入一些不一样的东西。我们希望这是一件有意义的事。本书适合的对象

    PHP爱好者;

    想进阶的初级PHP程序员;

    对PHP扩展开发感兴趣的读者;

    对高并发感兴趣的读者;

    对NoSQL应用和实现原理感兴趣的读者;

    从事PHP网络应用,想知道HTTP协议、Socket等更多细节的开发人

    员;

    想就职于大型互联网公司的PHP程序员;

    开设相关课程的大专院校的学生;

    公司内部培训的学员。如何阅读本书

    本书一共有14章。每章节都可以单独阅读,由于部分知识点之间存

    在一定的衔接,故建议按先后顺序阅读。

    第1章为面向对象思想的核心概念。本章主要讲解面向对象开发的

    思想,重点讲解面向对象模型的建立,以及面向对象的一些基础概念。

    通过大量对比和实例,尤其是与Java的对比,力图从不同角度讲解PHP

    面向对象的特性,让PHP程序员看到不同的面向对象。求同存异是本章

    的核心思想。

    第2章为用面向对象思维写程序。本章用简练的语言讲解了面向对

    象设计的五大原则,这五大原则也是理解设计模式的基础所在,帮助读

    者站在一个更高的角度思考面向对象。

    第3章为正则表达式技巧与实战。本章详细介绍了正则的基础语

    法,通过大量的示例、通俗的语言讲解正则概念,引导读者理解正则的

    一系列规则。接下来,结合实际工作用安全过滤、URL重写等实例,加

    深对正则的应用和掌握。最后给出正则效率优化的一些普遍技巧和替代

    方案,让读者对正则的使用得心应手。

    第4章为PHP网络技术及应用。本章着重介绍了HTTP协议、Socket

    开发、WebService、Cookie和Session使用等。结合实战向读者阐述网络

    开发的核心和重点,特别是对HTTP协议的理解。HTTP协议是Web开发

    的基石,也是各种面试和开发中必然遇到的知识点。而Socket则是应用

    交互的桥梁,保证了有用的可扩展性。

    第5章为PHP与数据库基础。本章从不同角度分析了MySQL,介绍

    了PDO、MySQL优化、存储过程、事件调度机制以及MySQL安全防范

    等内容。

    第6章为PHP模板引擎的原理与最佳实践。本章通过实现一个简单

    的模板引擎,学习模板引擎的原理和使用方法,然后对比几大流行的模

    板引擎实现方案,简单介绍了各种实现方案的思想和优缺点,最后探讨

    模板引擎的意义。

    第7章为PHP扩展开发。本章的知识是本书核心内容,介绍了PHP扩展开发的几个重要知识点,如扩展框架搭建、PHP生命周期、PHP变量

    在内核中的实现方式、Zend引擎、内存管理等,让读者深入PHP底层,知其然也知其所以然。

    第8章为缓存。本章主要介绍了缓存的基本原理和三个衡量指标,通过几个实例加深读者对缓存的理解。利用本章知识,读者应该能设计

    一个比较合理的缓存方案。

    第9章为Memcached应用与内幕。本章深入剖析了Memcached的实

    现和内部结构,从而使读者掌握Memcached的高级应用,对构建复杂环

    境的缓存层有个清晰的认识。

    第10章为Redis应用与内幕。本章重点介绍了Redis的深入应用,如

    事务处理、主从同步、虚拟内存等,和第9章类似,探讨了Redis的实现

    内幕。合理利用Redis可以为我们解决大流量高并发的应用。

    第11章为高性能网站架构。本章探讨了高性能架构的基本出发点,重点以HandlerSocket、MySQL主从复制、反向代理缓存软件Varnish和

    任务分发框架Gearman为例,讲述几种高性能架构中会用到的技术。

    第12章为调试与测试。科学的调试方法有助于快速找出潜在的

    Bug、理解复杂应用的流程、提高开发效率。单元测试是代码质量的保

    障。在这一章的最后一节介绍了使用JMeter进行压力测试的方法。

    第13章为Hash算法与数据库的实现。本章介绍了Hash算法的基本原

    理,用此算法实现一个简单的、基于Hash的数据库,让读者意识到算法

    的重要性和可操作性。

    第14章为PHP编码规范。本章介绍了PHP开发中应遵循的基本代码

    规范,并提出合理建议。好的代码必然是规范的代码。

    本书第1、2、3、5、6、8、12、14章由陈文撰写,第7、9、10、11、13章由列旭松撰写,第4章由两人共同完成。勘误和支持

    由于我们的水平和开发经验有限,同时计算机技术更新较快,书中

    难免存在不足之处,有些章节内容可能从未来的某一天开始不再适用,还望读者理解和体谅,并恳请读者批评指正。您若对本书有什么好的建

    议或者对书中部分内容有疑惑,可与我们联系,我们将尽量为读者提供

    最满意的解答。期待得到您的真挚反馈。我们的联系方式如下:

    陈文:waitfox@qq.com

    列旭松:liexusong@qq.com感谢

    首先要感谢PHP之父Rasmus Lerdorf,是他创建了这个简单、轻

    松、有趣、快速而又高效的语言;其次,感谢PHP社区每一位充满活力

    的朋友,和你们的交流使我学到很多,本书有不少内容就来自于社区的

    智慧。

    在这里尤其要感谢机械工业出版社华章公司的大力支持,特别是杨

    福川和白宇两位编辑,在一年多的时间里,因为有了你们的耐心指导、逐字逐句认真审稿和改稿才有了本书的诞生。

    最后,还要感谢家人和朋友的支持。

    陈文第1章 面向对象思想的核心概念

    面向对象是什么?以下是维基百科对面向对象的解释:

    面向对象程序设计(Object-Oriented Programming,OOP)是一种

    程序设计范型,同时也是一种程序开发方法。它将对象作为程序的基本

    单元,将程序和数据封装其中,以提高软件的重用性、灵活性和可扩展

    性。

    面向过程、面向对象以及函数式编程被人们称为编程语言中的三大

    范式(实际上,面向过程与面向对象都同属于命令式编程),是三种不

    同编码和设计风格。其中面向对象的核心思想是对象、封装、可重用性

    和可扩展性。

    面向对象是一种更高级、更抽象的思维方式,面向过程虽然也是一

    种抽象,但面向过程是一种基础的抽象,面向对象又是建立在面向过程

    之上的更高层次的抽象,因此对面向对象的理解也就不是那么容易了。

    面向对象和具体的语言无关。在面向对象的世界里,常常提到的两

    种典型语言——C++和Java。它们都是很好的面向对象的开发语言。实

    际上,像C语言这种大家普遍认为的面向过程开发的主打语言,也能进

    行面向对象的开发,就连JavaScript这门很久之前一直被视作面向过程编

    程的语言,人们对它的认识也发生了改变,逐渐承认其是面向对象的语

    言,并且也接受了JavaScript独特的面向对象的语法。所以我们说面向对

    象只是种程序设计的理念,和具体的语言无关。不同的程序员既可以用

    C语言写出面向对象的风格来,也可以用Java写成面向对象的风格。这

    里并不是说面向对象的风格要优于面向过程,而是二者各有自己所擅长

    的领域。OOPL(Object-Oriented Programming Language)可以提高程序

    的封装性、复用性、可维护性,但仅仅是“可以”,能不能真正实现这些

    优点,还取决于编程和设计人员。就PHP而言,其不是一门纯的面向对

    象的语言,但是仍然可以使用PHP写出好的面向对象风格的代码。

    实际开发中,面向对象为什么让我们觉得那么难?面向对象究竟难

    在什么地方?为什么面向对象开发在PHP里一直不是很受重视,并且没

    有得到普及和推广?PHP对面向对象的支持到底如何?怎么学习面向对

    象的思维?在这里,我们将就面向对象一些概念展开讨论,其中重点讨论PHP

    特色的面向对象的风格和语法,并通过相互借鉴和对比,使读者认识

    PHP自身的特点,尤其是和其他语言中不同的地方。1.1 面向对象的“形”与“本”

    类是对象的抽象组织,对象是类的具体存在。

    2200年前的战国时期,赵国平原君的食客公孙龙在骑着白马进城

    时,被守城官以马不能入城拦下,公孙龙即兴演讲,口述“白马非马”一

    论,守城官无法反驳,于是公孙龙就骑着他的白马(不是马的)进城去

    了。这就是历史上最经典的一次对面向对象思维的阐述。

    公孙龙的“白马非马”论如下:

    “白马非马”,可乎?曰:“可。”曰“何哉?”曰:“马者,所以命形

    也;白者,所以命色也。命色者非命形也。故曰:‘白马非

    马’。”曰:“有白马不可谓无马也。不可谓无马者,非马也?有白马为

    有马,白之,非马何也?”曰:“求马,黄、黑马皆可致;求白马,黄、黑马不可致。使白马乃马也,是所求一也。所求一者,白者不异马也。

    所求不异,如黄、黑马有可有不可,何也?可与不可,其相非明。故

    黄、黑马一也,而可以应有马,而不可以应有白马,是白马之非马,审

    矣!”

    公孙龙乃战国时期的“名家”,名家的中心论题是所谓“名”(概念)

    和“实”(存在)的逻辑关系问题。名者,抽象也,类也。实者,具体

    也,对象也。从这个角度讲,公孙龙是我国早期的最著名的面向对象思

    维的思想家。

    “白马非马”这一论断的关键就在于“非”字,公孙龙一再强调白马与

    马的特征,通过把白马和马视为两个不同的类,用“非”这一关系,成功

    地把“白马”与“马”的关系由从属关系转移到“白马”这个对象与“马”这个

    对象的相等关系上,显然,二者不等,故“白马非马”。而我们正常的思

    维是,马是一个类,白马是马这个类的一个对象,二者属于从属关系。

    说“白马非马”,就是割裂马与白马之间的从属关系,偷换概念,故为诡

    辩也。

    白马非马这个典故,我们可以称之为诡辩。但我们把这个问题抽象

    出来,实际上讨论的就是类与类之间的界定、类的定义等一系列问题,类应该抽象到什么程度,其中即涉及了类与对象的本质问题,也涉及了

    类的设计过程中的一些原则。1.1.1 对象的“形”

    要回答类与对象本质这个问题,我想可以先从“形”的角度来回答。

    本节以PHP为例,来探讨对象的“形”与“本”的问题。

    类是我们对一组对象的描述。

    在PHP里,每个类的定义都以关键字class开头,后面跟着类名,紧

    接着一对花括号,里面包含有类成员和方法的定义。如下面代码所示:

    class person{

    publicname;

    publicgender;

    public function say{

    echothis->name,is,this->gender;

    }

    }

    在这里,我们定义了一个person类。代表了抽象出来的人这个概

    念,它含有姓名和性别这两个属性,还具有一个开口说话的方法,这个

    方法会告诉外界这个人的性别和姓名。我们接下来就可以产生这个类的

    实例:

    student=new person;

    student->name='Tom';

    student->gender='male';

    student->say;

    teacher=new person;

    teacher->name='Kate';

    teacher->gender='female';

    teacher->say;

    这段代码则实例化了person类,产生了一个student对象和teacher对

    象的实例。实际上也就是从抽象到具体的过程。现实世界中,仅仅

    说“人”是没有意义的,中国人把它叫“人”,美国人把它叫person或者

    human,如果高兴,把它叫God或者“板凳”都无所谓。但是只要你

    把“人”这个概念加上各种属性和方法,比如说有两条腿、直立行走、会

    说话,则无论是中国人,还是美国人,甚至外星人都是能理解你所描述

    的事物。所以,一个类的设计需要能充分展示其最重要的属性和方法,并且能与其他事物相区分。只有类本身有意义,从抽象到具体的实例化

    才会有意义。

    根据上面的实例代码,可以有下面的一些理解:

    类定义了一系列的属性和方法,并提供了实际的操作细节,这些方法可以用来对属性进行加工。

    对象含有类属性的具体值,这就是类的实例化。正是由于属性的不

    同,才能区分不同的对象。在上面例子里,由于student和teacher的性别

    和姓名不一样,才得以区分开二人。

    类与对象的关系类似一种服务与被服务、加工与被加工的关系,具

    体而言,就如同原材料与流水线的关系。只需要在对象上调用类中所存

    在的方法,就可以对类的属性进行加工,并且展示其功能。

    类是属性和方法的集合,那么在PHP里,对象是什么呢?比较普遍

    的说法就是“对象由属性和方法组成”。对象是由属性组成,这很好理

    解,一个对象的属性是它区别于另一个对象的关键所在。由于PHP的对

    象是用数组来模拟的,因此我们把对象转为数组,就能看到这个对象所

    拥有的属性了。

    继续使用上面代码,可以打印student对象:

    print_r((array)student);

    var_dump(student);

    到这里,可以很直观地认识到,对象就是一堆数据。既然如此,可

    以把一个对象存储起来,以便需要时用。这就是对象的序列化。

    所谓序列化,就是把保存在内存中的各种对象状态(属性)保存起

    来,并且在需要时可以还原出来。下面的代码实现了把内存中的对象当

    前状态保存到一个文件中:

    str=serialize(student);

    echostr;

    file_put_contents('store.txt',str);

    输出序列化后的结果:

    O:6:person:2:{s:4:name;s:3:Tom;s:6:gender;s:4:mail;}

    在需要时,反序列化取出这个对象:

    str=file_get_contents('store.txt');student=unserialize(str);

    student->say;

    注意 在序列化和反序列化时都需要包含类的对象的定义,不然有

    可能出现在反序列化对象时,找不到该对象的类的定义,而返回不正确

    的结果。

    可以看到,对象序列化后,存储的只是对象的属性。类是由属性和

    方法组成的,而对象则是属性的集合,由同一个类生成的不同对象,拥

    有各自不同的属性,但共享了类的代码空间中方法区域的代码。

    1.1.2 对象的“本”

    我们需要更深入地了解这种机制,看对象的“本”。对象是什么?对

    象在PHP中也是变量的一种,所以先看PHP源码中对变量的定义:

    zendzend.h

    typedef union_zvalue_value{

    long lval;long value

    double dval;double value

    struct{

    charval;

    int len;

    }str;

    HashTableht;hash table value

    zend_object_value obj;

    }zvalue_value;

    zvalue_value,就是PHP底层的变量类型,zend_object_value obj就是

    变量中的一个结构。接着看对象的底层实现。

    在PHP5中,对象在底层的实现是采取“属性数组+方法数组”来实现

    的。可以简单地理解为PHP对象在底层的存储如图1-1所示。图 1-1 对象的组成

    对象在PHP中是使用一种zend_object_value结构体来存储的。对象

    在ZEND(PHP底层引擎,类似Java的JVM)中的定义如下:

    zendzend.h

    typedef struct_zend_object{

    zend_class_entryce;这里就是类入口

    HashTableproperties;属性组成的HashTable

    HashTableguards;protects from_get_set...recursion

    }zend_object;

    ce是存储该对象的类结构,在对象初始化时保存了类的入口,相当于类指针的作用。properties是一个HashTable,用来存放对象属性。

    guards用来阻止递归调用。

    类的标准方法在zendzend_object_handlers.h文件中定义,具体实现

    则是在zendzend_object_handlers.c文件中。关于PHP变量的存储结构的

    底层实现,将在第7章中进行更深入的介绍。

    通过对上述源代码的简单阅读,可以更清晰地认识到对象也是一种

    很普通的变量,不同的是其携带了对象的属性和类的入口。

    1.1.3 对象与数组

    对象是什么,我们不好理解,也不容易回答,但是我们知道数组是

    什么。数组的概念比较简单。可以拿数组和对象对比来帮助我们理解对

    象。对象转化为数组,数组也能转换成对象。数组是由键值对数据组成

    的,数组的键值对和对象的属性属性值对十分相似。对象序列化后和

    数组序列化后的结果是惊人的相似。如下面的代码所示:

    student_arr=array('name'=>'Tom','gender'=>'male');

    echo\n;

    echo serialize(student_arr);

    输出为:

    a:2:{s:4:name;s:3:Tom;s:6:gender;s:4:male;}

    可以很清楚地看出,对象和数组在内容上一模一样!

    而对象和数组的区别在于:对象还有个指针,指向了它所属的类。

    在对student对象序列化时,我们看到了person这几个字符,这个标识

    符就标志了这个对象归属于person类,故在取出这个对象后,可以立即

    对其执行所包含的方法。如果对象中还包含对象呢?我们来看下一节的

    内容。

    1.1.4 对象与类

    在前面代码中定义了一个类,并创建了这个类的对象,把前面产生

    的对象作为这个新对象的一个属性,完整代码如代码清单1-1所示。代码清单1-1 object.php

    
    class person{

    publicname;

    publicgender;

    public function say{

    echothis->name,\tis,this->gender,\r\n;

    }

    }

    class family{

    publicpeople;

    publiclocation;

    public function_construct(p,loc){

    this->people=p;

    this->location=loc;

    }

    }

    student=new person;

    student->name='Tom';

    student->gender='male';

    student->say;

    tom=new family(student,'peking');

    echo serialize(student);

    student_arr=array('name'=>'Tom','gender'=>'male');

    echo\n;

    echo serialize(student_arr);

    print_r(tom);

    echo\n;

    echo serialize(tom);

    输出结果如下:

    Tom is male

    O:6:person:2:{s:4:name;s:3:Tom;s:6:gender;s:4:male;}

    a:2:{s:4:name;s:3:Tom;s:6:gender;s:4:male;}

    family Object

    (

    [people]=>person Object

    (

    [name]=>Tom

    [gender]=>male)

    [location]=>peking)

    O:6:family:2:{s:6:people;O:6:person:2:{s:4:name;s:3:Tom;s:6:gender;s:4:male;}s:8:location;s:6:peking;}

    可以看出,序列化后的对象会附带所属的类名,这个类名保证此对

    象能够在执行类的方法(也是自己所能执行的方法)时,可以正确地找

    到方法所在的代码空间(即对象所拥有的方法存储在类里)。另外,当

    一个对象的实例变量引用其他对象时,序列化该对象时也会对引用对象

    进行序列化。

    基于如上的分析,可以总结出对象和类的概念以及二者之间的关

    系:

    类是定义一系列属性和操作的模板,而对象则把属性进行具体化,然后交给类处理。

    对象就是数据,对象本身不包含方法。但是对象有一个“指针”指向

    一个类,这个类里可以有方法。方法描述不同属性所导致的不同表现。

    类和对象是不可分割的,有对象就必定有一个类和其对应,否则这

    个对象也就成了没有亲人的孩子(但有一个特殊情况存在,就是由标量

    进行强制类型转换的object,没有一个类和它对应。此时,PHP中一个

    称为“孤儿”的stdClass类就会收留这个对象)。

    理解了以上四个概念,结合现实世界从实现和存储理解对象和类,这样就不会把二者看成一个抽象、神秘的东西,也就能写出符合现实世

    界的类了。

    如果需要一个类,要从客观世界抽象出一套规律,就得总结这类事

    物的共性,并且让它可以与其他类进行区分。而这个区分的依据就是属

    性和方法。区分的办法就是实例化出一个对象,是骡子是马,拉出来遛

    遛。

    现在,你是否对“白马非马”这个典故有了新的认识?1.2 魔术方法的应用

    魔术方法是以两个下画线“_”开头、具有特殊作用的一些方法,可

    以看做PHP的“语法糖”。

    语法糖指那些没有给计算机语言添加新功能,而只是对人类来说

    更“甜蜜”的语法。语法糖往往给程序员提供了更实用的编码方式或者一

    些技巧性的用法,有益于更好的编码风格,使代码更易读。不过其并没

    有给语言添加什么新东西。PHP里的引用、SPL等都属于语法糖。

    实际上,在1.1节代码中就涉及魔术方法的使用。family类中的

    _construct方法就是一个标准魔术方法。这个魔术方法又称构造方法。具

    有构造方法的类会在每次创建对象时先调用此方法,所以非常适合在使

    用对象之前做一些初始化工作。因此,这个方法往往用于类进行初始化

    时执行一些初始化操作,如给属性赋值、连接数据库等。

    以代码清单1-1所示代码为例,family中的_construct方法主要做的事

    情就是在创建对象的同时对属性赋值。也可以这么使用:

    tom=new family(student,'peking');

    tom->people->say;

    这样做就不需要在创建对象后再去赋值了。有构造方法就有对应的

    析构方法,即_destruct方法,析构方法会在某个对象的所有引用都被删

    除,或者当对象被显式销毁时执行。这两个方法是常见也是最有用的魔

    术方法。

    1.2.1 _set和_get方法

    _set和_get是两个比较重要的魔术方法,如代码清单1-2所示。

    代码清单1-2 magic.php

    
    class Account{

    privateuser=1;

    privatepwd=2;

    }

    a=new Account;

    echoa->user;

    a->name=5;

    echoa->name;echoa->big;

    运行这段代码会怎样呢?结果报错如下:

    Fatal error:Cannot access private property Account::user in G:\bak\temp\tempcode\_sg.php on line 7

    所报错误大致是说,不能访问Account对象的私有属性user。在代码

    清单1-2的类定义里增加以下代码,其中使用了_set魔术方法。

    public function _set(name,value){

    echo Settingname tovalue\r\n;

    this->name=value;

    }

    public function _get(name){

    if(!isset(this->name)){

    echo'未设置';

    this->name=正在为你设置默认值;

    }

    returnthis->name;

    }

    再次运行,看到正常输出,没有报错。在类里以两个下画线开头的

    方法都属于魔术方法(除非是你自定义的),它们是PHP中的内置方

    法,有特殊含义。手册里把这两个方法归到重载。

    PHP的重载和Java等语言的重载不同。Java里,重载指一个类中可

    以定义参数列表不同但名字相同的多个方法。比如,Java也有构造函

    数,Java允许有多个构造函数,只要保证方法签名不一样就行;而PHP

    则在一个类中只允许有一个构造函数。

    PHP提供的“重载”指动态地“创建”类属性和方法。因此,_set和_get

    方法被归到重载里。

    这里可以直观看到,若类中定义了_set和_get这一对魔术方法,那

    么当给对象属性赋值或者取值时,即使这个属性不存在,也不会报错,一定程度上增强了程序的健壮性。

    我们注意到,在account类里,user属性的访问权限是私有的,私有

    属性意味着这个属性是类的“私有财产”,只能在类内部对其进行操作。

    如果没有_set这个魔术方法,直接在类的外部对属性进行赋值操作是会

    报错的,只能通过在类中定义一个public的方法,然后在类外调用这个

    公开的方法进行属性读写操作。现在有了这两个魔术方法,是不是对私有属性的操作变得更方便了

    呢?实际上,并没有什么奇怪的,因为这两个方法本身就是public的。

    它们和在对外的public方法中操作private属性的原理一样。只不过这对

    魔术方法使其操作更简单,不需要显式地调用一个public的方法,因为

    这对魔术方法在操作类变量时是自动调用的。当然,也可以把类属性定

    义成public的,这样就可以随意在类的外部进行读写。不过,如果只是

    为了方便,类属性在任意时候都定义成public权限显然是不合适的,也

    不符合面向对象的设计思想。

    1.2.2 _call和_callStatic方法

    如何防止调用不存在的方法而出错?一样的道理,使用_call魔术重

    载方法。

    _call方法原型如下:

    mixed _call(stringname,arrayarguments)

    当调用一个不可访问的方法(如未定义,或者不可见)时,_call

    会被调用。其中name参数是要调用的方法名称。arguments参数是一

    个数组,包含着要传递给方法的参数,如下所示:

    public function _call(name,arguments){

    switch(count(arguments)){

    case 2:

    echo arguments[0]arguments[1],PHP_EOL;

    break;

    case 3:

    echo array_sum(arguments),PHP_EOL;

    break;

    default:

    echo '参数不对',PHP_EOL;

    break;

    }

    }

    a->make(5);

    a->make(5,6);

    以上代码模拟了类似其他语言中的根据参数类型进行重载。跟_call

    方法配套的魔术方法是_callStatic。当然,使用魔术方法“防止调用不存

    在的方法而报错”,并不是魔术方法的本意。实际上,魔术方法使方法

    的动态创建变为可能,这在MVC等框架设计中是很有用的语法。假设

    一个控制器调用了不存在的方法,那么只要定义了_call魔术方法,就能

    友好地处理这种情况。试着理解代码清单1-3所示代码。这段代码通过使用_callStatic这一

    魔术方法进行方法的动态创建和延迟绑定,实现一个简单的ORM模

    型。

    代码清单1-3 simpleOrm.php

    
    abstract class ActiveRecord{

    protected static table;

    protected fieldvalues;

    public select;

    static function findById(id){

    query = selectfrom

    .static::table

    .where id=id;

    return self::createDomain(query);

    }

    function _get(fieldname){

    return this->fieldvalues[fieldname];

    }

    static function _callStatic(method,args){

    field = preg_replace('^findBy(\w)','{1}',method);

    query = selectfrom

    .static::table

    .wherefield = 'args[0]';

    return self::createDomain(query);

    }

    private static function createDomain(query){

    klass = get_called_class;

    domain = newklass;

    domain->fieldvalues = array;

    domain->select = query;

    foreach(klass::fields asfield=>type){

    domain->fieldvalues[field]='TODO:set from sql result';

    }

    return domain;

    }

    }

    class Customer extends ActiveRecord{

    protected static table='custdb';

    protected static fields=array(

    'id'=>'int','email'=>'varchar','lastname'=>'varchar');

    }

    class Sales extends ActiveRecord{

    protected static table='salesdb';

    protected static fields=array(

    'id'=>'int','item'=>'varchar','qty'=>'int');

    }

    assert(selectfrom custdb where id=123==

    Customer::findById(123)->select);

    assert(TODO:set from sql result==

    Customer::findById(123)->email);

    assert(selectfrom salesdb where id=321==

    Sales::findById(321)->select);

    assert(selectfrom custdb where Lastname='Denoncourt'==

    Customer::findByLastname('Denoncourt')->select);

    再举个类似的例子。PHP里有很多字符串函数,假如要先过滤字符

    串首尾的空格,再求出字符串的长度,一般会这么写:

    strlen(trim(str));

    如果要实现JS里的链式操作,比如像下面这样,应该怎么实现?str->trim->strlen

    很简单,先实现一个String类,对这个类的对象调用方法进行处理

    时,触发_call魔术方法,接着执行call_user_func即可。

    1.2.3 _toString方法

    再看另外一个魔术方法_TOstring(在这里故意这么写,是要说明

    PHP中方法不区分大小写,但实际开发中还需要注意规范)。

    当进行测试时,需要知道是否得出正确的数据。比如打印一个对象

    时,看看这个对象都有哪些属性,其值是什么,如果类定义了_toString

    方法,就能在测试时,echo打印对象体,对象就会自动调用它所属类定

    义的_toString方法,格式化输出这个对象所包含的数据。如果没有这个

    方法,那么echo一个对象将报错,例如Catchable fatal error:Object of

    class Account could not be converted to string语法错误,实际上这是一个

    类型匹配失败错误。不过仍然可以用print_r和var_dump函数输出一个

    对象。当然,_toString是可以定制的,所提供的信息和样式更丰富,如

    代码清单1-4所示。

    代码清单1-4 magic_2.php

    
    class Account{

    public user = 1;

    private pwd = 2;

    自定义的格式化输出方法

    public function_toString{

    return当前对象的用户名是{this->user},密码是{this->pwd};

    }

    }

    a = new Account;

    echoa;

    echo PHP_EOL;

    print_r(a);

    运行这段代码发现,使用_toString方法后,输出的结果是可定制

    的,更易于理解。实际上,PHP的_toString魔术方法的设计原型来源于

    Java。Java中也有这么一个方法,而且在Java中,这个方法被大量使

    用,对于调试程序比较方便。实际上,_toString方法也是一种序列化,我们知道PHP自带serializeunserialize也是进行序列化的,但是这组函数

    序列化时会产生一些无用信息,如属性字符串长度,造成存储空间的无

    谓浪费。因此,可以实现自己的序列化和反序列化方法,或者

    json_encodejson_decode也是一个不错的选择为什么直接echo一个对象就会报语法错误,而如果这个对象实现

    _toString方法后就可以直接输出呢?原因很简单,echo本来可以打印一

    个对象,而且也实现了这个接口,但是PHP对其做了个限制,只有实现

    _toString后才允许使用。从下面的PHP源代码里可以得到验证:

    ZEND_VM_HANDLER(40,ZEND_ECHO,CONST|TMP|VAR|CV,ANY)

    {

    zend_opopline = EX(opline);

    zend_free_op free_op1;

    zval z_copy;

    zval z =GET_OP1_ZVAL_PTR(BP_VAR_R);

    此处的代码预留了把对象转换为字符串的接口

    if(OP1_TYPE ! = IS_CONST&&

    Z_TYPE_P(z) == IS_OBJECT&&Z_OBJ_HT_P(z)->get_method ! = NULL&&

    zend_std_cast_object_tostring(z,&z_copy,IS_STRING TSRMLS_CC) == SUCCESS){

    zend_print_variable(&z_copy);

    zval_dtor(&z_copy);

    }else{

    zend_print_variable(z);

    }

    FREE_OP1;

    ZEND_VM_NEXT_OPCODE;

    }

    由此可见,魔术方法并不神奇。

    有比较才有认知。最后,针对本节代码给出一个Java版本的代码,供各位读者用来对比两种语言中重载和魔术方法的异同,如代码清单1-

    5所示。

    代码清单1-5 Account.java

    import org.apache.commons.lang3.builder.ToStringBuilder;

    类的重载演示Java版本

    @author wfox

    @date@verson

    public class Account{

    private String user;用户名

    private String pwd;密码

    public Account{

    System.out.println(构造函数);

    }

    public Account(String user,String pwd){

    System.out.println(重载构造函数);

    System.out.println(user+---+pwd);

    }

    public void say(String user){

    System.out.println(用户是:+user);

    }

    public void say(String user,String pwd){

    System.out.println(用户:+user);

    System.out.println(密码+pwd);

    }

    public String getUser{

    return user;

    }

    public void setUser(String user){

    this.user = user;

    }

    public String getPwd{

    return pwd;

    }

    public void setPwd(String pwd){

    }

    @Override

    public String toString{

    return ToStringBuilder.reflectionToString(this);

    }

    public static void main(String……){Account account = new Account;

    account.setUser(张三);

    account.setPwd(123456);

    account.say(李四);

    account.say(王五,123);

    System.out.println(account);

    }

    }

    运行上述代码,输出如图1-2所示。

    图 1-2 Java里的构造方法和重载演示

    可以看出,Java的构造方法比PHP好用,PHP由于有了_set_get这一

    对魔术方法,使得动态增加对象的属性字段变得很方便,而对Java来

    说,要实现类似的效果,就不得不借助反射API或直接修改编译后字节

    码的方式来实现。这体现了动态语言的优势,简单、灵活。1.3 继承与多态

    面向对象的优势在于类的复用。继承与多态都是对类进行复用,它

    们一个是类级别的复用,一个是方法级别的复用。提到继承必提组合,二者有何异同?PHP到底有没有多态?若没有,则为什么没有?有的

    话,和其他语言中的多态又有什么区别?这些都是本节所要讲述的内

    容。

    1.3.1 类的组合与继承

    在1.1节的代码中定义了两个类,一个是person,一个是family;在

    family类中创建person类中的对象,把这个对象视为family类的一个属

    性,并调用它的方法处理问题,这种复用方式叫“组合”。还有一种复用

    方式,就是继承。

    类与类之间有一种父与子的关系,子类继承父类的属性和方法,称

    为继承。在继承里,子类拥有父类的方法和属性,同时子类也可以有自

    己的方法和属性。

    可以把1.1节的组合用继承实现,如代码清单1-6所示。

    代码清单1-6 family_extends.php

    
    class person{

    public name='Tom';

    public gender;

    static money=10000;

    public function_construct{

    echo'这里是父类',PHP_EOL;

    }

    public function say{

    echo this->name,\tis,this->gender,\r\n;

    }

    }

    class family extends person{

    public name;

    public gender;

    public age;

    static money=100000;

    public function_construct{

    parent::_construct;调用父类构造方法

    echo'这里是子类',PHP_EOL;

    }

    public function say{

    parent::say;

    echo this->name,\tis\t,this->gender,,and

    is\t,this->age,PHP_EOL;

    }

    public function cry{

    echo parent::money,PHP_EOL;

    echo'%>_<%',PHP_EOL;

    echo self::money,PHP_EOL;调用自身构造方法

    echo'(^_^)';

    }

    }poor=new family;

    poor->name='Lee';

    poor->gender='female';

    poor->age=25;

    poor->say;

    poor->cry;

    运行上面的代码,可以得到如下输出结果:

    这里是父类

    这里是子类

    Lee is female

    Lee is female,and is 25

    10000

    %>_<%

    100000

    (^_^)

    从上面代码中可以了解继承的实现。在继承中,用parent指代父

    类,用self指代自身。使用“::”运算符(范围解析操作符)调用父类的方

    法。“::”操作符还用来作为类常量和静态方法的调用,不要把这两种应

    用混淆。

    既然提到静态,就再强调一点,如果声明类成员或方法为static,就

    可以不实例化类而直接访问,同时也就不能通过一个对象访问其中的静

    态成员(静态方法除外),也不能用“::”访问一个非静态方法。比如,把上例中的poor->cry;换成poor::cry,按照这个规则,应该是要报

    错的。可能试验时,并没有报错,而且能够正确输出。这是因为

    用“::”方式调用一个非静态方法会导致一个E_STRICT级别的错误,而这

    里的PHP设置默认没有开启这个级别的报错提示。打开PHP安装目录下

    的php.ini文件,设置如下:

    error_reporting=E_ALL|E_STRICT

    display_errors=On

    再次运行,就会看到错误提示。因此,用“::”访问一个非静态方法

    不符合语法,但PHP仍然能够正确地执行代码,这只是PHP所做的一

    个“兼容”或者说“让步”。在开发时,设置最严格的报错等级,在部署时

    可适当调低。

    组合与继承都是提高代码可重用性的手段。在设计对象模型时,可

    以按照语义识别类之间的组合关系和继承关系。比如,通过一些总结,得出了继承是一种“是、像”的关系,而组合是一种“需要”的关系。利用

    这条规律,就可以很简单地判断出父亲与儿子应该是继承关系,父亲与

    家庭应该是组合关系。还可以从另外一个角度看,组合偏重整体与局部的关系,而继承偏重父与子的关系,如图1-3所示。

    图 1-3 继承和组合的对照

    从方法复用角度考虑,如果两个类具有很多相同的代码和方法,可

    以从这两个类中抽象出一个父类,提供公共方法,然后两个类作为子

    类,提供个性方法。这时用继承语意更好。继承的UML图如图1-4所

    示。图 1-4 继承的UML图

    而组合就没有这么多限制。组合之间的类可以关系(体现为复用代

    码)很小,甚至没有关系,如图1-5所示。

    图 1-5 组合的UML图

    然而在编程中,继承与组合的取舍往往并不是这么直接明了,很难

    说出二者是“像”的关系还是“需要”的关系,甚至把它拿到现实世界中建

    模,还是无法决定应该是继承还是组合。那应该怎么办呢?有什么标准吗?有的。这个标准就是“低耦合”。

    耦合是一个软件结构内不同模块之间互连程度的度量,也就是不同

    模块之间的依赖关系。

    低耦合指模块与模块之间,尽可能地使模块间独立存在;模块与模

    块之间的接口尽量少而简单。现代的面向对象的思想不强调为真实世界

    建模,变得更加理性化一些,把目标放在解耦上。

    解耦是要解除模块与模块之间的依赖。

    按照这个思想,继承与组合二者语义上难于区分,在二者均可使用

    的情况下,更倾向于使用组合。为什么呢?继承存在什么问题呢?

    1)继承破坏封装性。

    比如,定义鸟类为父类,具有羽毛属性和飞翔方法,其子类天鹅、鸭子、鸵鸟等继承鸟这个类。显然,鸭子和鸵鸟不需要飞翔这个方法,但作为子类,它们却可以无区别地使用飞翔这个方法,显然破坏了类的

    封装性。而组合,从语义上来说,要优于继承。

    2)继承是紧耦合的。

    继承使得子类和父类捆绑在一起。组合仅通过唯一接口和外部进行

    通信,耦合度低于继承。

    3)继承扩展复杂。

    随着继承层数的增加和子类的增加,将涉及大量方法重写。使用组

    合,可以根据类型约束,实现动态组合,减少代码。

    4)不恰当地使用继承可能违反现实世界中的逻辑。

    比如,人作为父类,雇员、经理、学生作为子类,可能存在这样的

    问题,经理一定是雇员,学生也可能是雇员,而使用继承的话一个人就

    无法拥有多个角色。这种问题归结起来就是“角色”和“权限”问题。在权

    限系统中很可能存在这样的问题,经理权利和职位大于主管,但出于分

    工和安全的考虑,经理没有权限直接操作主管所负责的资源,技术部经

    理也没权限直接命令市场部主管。这就要求角色和权限系统的设计要更灵活。不恰当的继承可能导致逻辑混乱,而使用组合就可以较好地解决

    这个问题。

    当然,组合并非没有缺点。在创建组合对象时,组合需要一一创建

    局部对象,这一定程度上增加了一些代码,而继承则不需要这一步,因

    为子类自动有了父类的方法,如代码清单1-7所示。

    代码清单1-7 mobile.php

    
    继承拥有比组合更少的代码量

    class car{

    public function addoil{

    echoAdd oil\r\n;

    }

    }

    class bmw extends car{

    }

    class benz{

    public car;

    public function_construct{

    this->car=new car;

    }

    public function addoil{

    this->car->addoil;

    }

    }

    bmw=new bmw;

    bmw->addoil;

    benz=new benz;

    benz->addoil;

    显然,组合比继承增加了代码量。组合还有其他的一些缺点,不过

    总体说来,是优点大于缺点。

    继承最大的优点就是扩展简单,但是其缺点大于优点,所以在设计

    时,需要慎重考虑。那应该如何使用继承呢?

    精心设计专门用于被继承的类,继承树的抽象层应该比较稳定,一

    般不要多于三层。

    对于不是专门用于被继承的类,禁止其被继承,也就是使用final修饰

    符。使用final修饰符既可防止重要方法被非法覆写,又能给编辑器寻找

    优化的机会。

    优先考虑用组合关系提高代码的可重用性。

    子类是一种特殊的类型,而不只是父类的一个角色。

    子类扩展,而不是覆盖或者使父类的功能失效。底层代码多用组合,顶层业务层代码多用继承。底层用组合可以

    提高效率,避免对象臃肿。顶层代码用继承可以提高灵活性,让业务使

    用更方便。

    思考题 设计一个log类,需要用到MySQL中的CURD操作,是应

    该使用继承呢还是组合?请给出理由。

    继承并非一无是处,而组合也不是完美无缺的。如果既要组合的灵

    活,又要继承的代码简洁,能做到吗?

    这是可以做到的,譬如多重继承,就具有这个特性。多重继承里一

    个类可以同时继承多个父类,组合两个父类的功能。C++里就是使用的

    这种模型来增强继承的灵活性的,但是多重继承过于灵活,并且会带

    来“菱形问题”,故为其使用带来了不少困难,模型变得复杂起来,因此

    在大多数语言中,都放弃了多重继承这一模型。

    多重继承太复杂,那么还有其他方式能比较好地解决这个问题吗?

    PHP5.4引入的新的语法结构Traits就是一种很好的解决方案。Traits的思

    想来源于C++和Ruby里的Mixin以及Scala里的Traits,可以方便我们实现

    对象的扩展,是除extend、implements外的另外一种扩展对象的方式。

    Traits既可以使单继承模式的语言获得多重继承的灵活,又可以避免多

    重继承带来的种种问题。

    1.3.2 各种语言中的多态

    多态确切的含义是:同一类的对象收到相同消息时,会得到不同的

    结果。而这个消息是不可预测的。多态,顾名思义,就是多种状态,也

    就是多种结果。

    以Java为例,由于Java是强类型语言,因此变量和函数返回值是有

    状态的。比如,实现一个add函数的功能,其参数可能是两个int型整

    数,也可能是两个float型浮点数,而返回值可能是整型或者浮点型。在

    这种情况下,add函数是有状态的,它有多种可能的运行结果。在实际

    使用时,编译器会自动匹配适合的那个函数。这属于函数重载的概念。

    需要说明的是,重载并不是面向对象里的东西,和多态也不是一个概

    念,它属于多态的一种表现形式。

    多态性是一种通过多种状态或阶段描述相同对象的编程方式。它的真正意义在于:实际开发中,只要关心一个接口或基类的编程,而不必

    关心一个对象所属于的具体类。

    很多地方会看到“PHP没有多态”这种说法。事实上,不是它没有,而是它本来就是多态的。PHP作为一门脚本语言,自身就是多态的,所

    以在语言这个级别上,不谈PHP的多态。在PHP官方手册也找不到对多

    态的详细描述。

    既然说PHP没有多态这个概念(实际上是不需要多态这个概念),那为什么又要讲多态呢?可以看下面的例子,如代码清单1-8所示。

    代码清单1-8 Polymorphism.php

    
    class employee{

    protected function working{

    echo'本方法需重载才能运行';

    }

    }

    class teacher extends employee{

    public function working{

    echo'教书';

    }

    }

    class coder extends employee{

    public function working{

    echo'敲代码';

    }

    }

    function doprint(obj){

    if(get_class(obj)=='employee'){

    echo'Error';

    }else{

    obj->working;

    }

    }

    doprint(new teacher);

    doprint(new coder);

    doprint(new employee);

    通过判断传入的对象所属的类不同来调用其同名方法,得出不同结

    果,这是多态吗?如果站在C++角度,这不是多态,这只是不同类对象

    的不同表现而已。C++里的多态指运行时对象的具体化,指同一类对象

    调用相同的方法而返回不同的结果。看个C++的例子,如代码清单1-9所

    示。

    代码清单1-9 C++多态的例子

    include

    include

    C++中用虚函数实现多态

    using namespace std;

    class father{

    public:

    father:age(30){cout<<父类构造法,年龄<
    ~father{cout<<父类析构<<\n;}

    void eat{cout<<父类吃饭吃三斤<<\n;}virtual void run{cout<<父类跑10000米<<\n;}虚函数

    protected:

    int age;

    };

    class son:public father{

    public:

    son{cout<<子类构造法<<\n;}

    ~son{cout<<子类析构<<\n;}

    void eat{cout<<儿子吃饭吃一斤<<\n;}

    void run{cout<<儿子跑100米<<\n;}

    void cry{cout<<哭泣<<\n;}

    };

    int main(int argc,charargv[])

    {

    fatherpf=new son;

    pf->eat;

    pf->run;

    delete pf;

    system(PAUSE);

    return EXIT_SUCCESS;

    }

    上面的代码首先定义一个父类,然后定义一个子类,这个子类继承

    父类的一些方法并且有自己的方法。通过fatherpf=new son;语句创建一

    个派生类(子类)对象,并且把该派生类对象赋给基类(父类)指针,然后用该指针访问父类中的eat和run方法。图1-6所示是运行结果。

    图 1-6 运行结果

    由于父类中的run方法加了virtual关键字,表示该函数有多种形态,可能被多个对象所拥有。也就是说,多个对象在调用同一名字的函数时

    会产生不同的效果。

    这个例子和PHP的例子有什么不同呢?C++的这个例子所创建的对

    象是一个指向父类的子对象,还可以创建更多派生类对象,然后上转型

    为父类对象。这些对象,都是同一类对象,但是在运行时,却都能调用到派生类同名函数。而PHP中的例子则是不同类的对象调用。

    由于PHP是弱类型的,并且也没有对象转型机制,所以不能像

    C++或者Java那样实现fatherpf=new son;把派生类对象赋给基类对象,然后在调用函数时动态改变其指向。在PHP的例子中,对象都是确定

    的,是不同类的对象。所以,从这个角度讲,这还不是真正的多态。

    代码清单1-8所示代码通过判断对象的类属性实现“多态”,此外,还可以通过接口实现多态,如代码清单1-10所示。

    代码清单1-10 通过接口实现多态

    
    interface employee{

    public function working;

    }

    class teacher implements employee{

    public function working{

    echo'教书';

    }

    }

    class coder implements employee{

    public function working{

    echo'敲代码';

    }

    }

    function doprint(employeei){

    ii->working;

    }

    a=new teacher;

    b=new coder;

    doprint(a);

    doprint(b);

    这是多态吗?这段代码和代码清单1-8相比没有多少区别,不过这

    段代码中doprint函数的参数是一个接口类型的变量,符合“同一类型,不同结果”这一条件,具有多态性的一般特征。因此,这是多态。

    如果把代码清单1-8中doprint函数的obj参数看做一种类型(把所有

    弱类型看做一种类型),那就可以认为代码清单1-8中的代码也是一种

    多态。

    再次把三段代码放在一起品味,可以看出:区别是否是多态的关键

    在于看对象是否属于同一类型。如果把它们看做同一种类型,调用相同

    的函数,返回了不同的结果,那么它就是多态;否则,不能称其为多

    态。由此可见,弱类型的PHP里多态和传统强类型语言里的多态在实现

    和概念上是有一些区别的,而且弱类型语言实现起多态来会更简单,更

    灵活。

    本节解决了什么是多态,什么不是多态的问题。至于多态是怎么实现的,各种语言的策略是不一样的。但是,最终的实现无非就是查表和

    判断。总结如下:

    多态指同一类对象在运行时的具体化。

    PHP语言是弱类型的,实现多态更简单、更灵活。

    类型转换不是多态。

    PHP中父类和子类看做“继父”和“继子”关系,它们存在继承关系,但不存在血缘关系。因此子类无法向上转型为父类,从而失去多态最典

    型的特征。

    多态的本质就是if…else,只不过实现的层级不同。1.4 面向接口编程

    这里,首先强调一个概念,面向接口编程并不是一种新的编程范

    式。本章开头提到的三大范式中并没有提到面向接口。其次,这里是狭

    义的接口,即interface关键字。广义的接口可以是任何一个对外提供服

    务的出口,比如提供数据传输的USB接口、淘宝网对其他网站开放的支

    付宝接口。

    1.4.1 接口的作用

    接口定义一套规范,描述一个“物”的功能,要求如果现实中

    的“物”想成为可用,就必须实现这些基本功能。接口这样描述自己:

    “对于实现我的所有类,看起来都应该像我现在这个样子”。

    采用一个特定接口的所有代码都知道对于那个接口会调用什么方

    法。这便是接口的全部含义。接口常用来作为类与类之间的一个“协

    议”。接口是抽象类的变体,接口中所有方法都是抽象的,没有一个有

    程序体。接口除了可以包含方法外,还能包含常量。

    比如用接口描述发动机,要求机动车必须要有run功能,至于怎么

    实现(摩托还是宝马),应该是什么样(前驱还是后驱),不是接口关

    心的。因为接口为抽象而生。作为质检总局,要判断这辆车是否合格,只要按“接口”的定义一条一条验证,这辆车不能run,那它就是废品,不能通过验收。但是,如果汽车实现了接口中本来不存在的方法

    music,并不认为有什么问题。接口就是一种契约。因此,在程序里,接口的方法必须被全部实现,否则将报fetal错误,如代码清单1-11所

    示。

    代码清单1-11 interface.php

    
    interface mobile

    {

    public function run;驱动方法

    }

    class plain implements mobile

    {

    public function run

    {

    echo我是飞机;

    }

    public function fly

    {echo飞行;

    }

    }

    class car implements mobile{

    public function run{

    echo我是汽车\r\n;

    }

    }

    class machine

    {

    function demo(mobilea)

    {

    a->fly;mobile接口是没有这个方法的

    }

    }

    obj=new machine;

    obj->demo(new plain);运行成功

    obj->demo(new car);运行失败

    在这段代码里,定义一个机动车接口,其中含有一个发动机功能。

    然后用一个飞机类实现这个接口,并增加了飞行方法。最后,在一个机

    械检测类中对机动车进行测试(用类型约束指定要测试的是机动车这个

    接口)。但是,此检测线测试的却是机动车接口中不存在的fly方法,直

    到遇到car的实例因不存在fly方法而报错。

    这段代码实际上是错误的,不符合接口语义。但是在PHP里,对

    plain的实例进行检测时是可以运行的。也就是说,在PHP里,只关心是

    否实现这个方法,而并不关心接口语义是否正确。

    按理说,接口应该是起一个强制规范和契约的作用,但是这里对接

    口的约束并没有起效,也打破了契约,对检测站这个类的行为失去控

    制。可以看看在Java里是怎么处理的,如图1-7所示。图 1-7 Java中接口是一种类型

    Java认为,接口就是一种type,即类型。如果你打破了我们之间的

    契约,你的行为变得无法控制,那就是非法的。这符合逻辑,也符合现

    实世界。这就真正起到接口作为规范的作用了。

    接口不仅规范接口的实现者,还规范接口的执行者,不允许调用接

    口中本不存在的方法。当然这并不是说一个类如果实现了接口,就只能

    实现接口中才有的方法,而是说,如果针对的是接口,而不是具体的

    类,则只能按接口的约定办事。这样的语法规定对接口的使用是有利

    的,让程序更健壮。根据这个角度讲,为了保证接口的语义,通常一个

    接口的实现类仅实现该接口所具有的方法,做到专一,当然这也不是一

    成不变的。

    由上面的例子可以看出,PHP里接口作为规范和契约的作用打了折

    扣。上面例子实际就是一个典型面向接口编程的例子。根据这个例子,可以很自然地想到使用接口的场合,比如数据库操作、缓存实现等。不

    用关心我们所面对的数据库是MySQL还是Oracle,只需要关心面向

    Database接口进行具体业务的逻辑相关的代码,这就是面向接口编程的

    来历。

    在这里,Database就如同employee一样,针对这个接口实现就好

    了。缓存功能也一样,我们不关注缓存是内存缓存还是文件缓存,或者

    是数据库缓存,只关注它是否实现了Cache接口,并且它只要实现了

    Cache接口,就实现了写入缓存和读取某条缓存中的数据及清除缓存这

    几个关键的功能点。

    通常在大型项目里,会把代码进行分层和分工。核心开发人员和技

    术经理编写核心的流程和代码,往往是以接口的形式给出,而基础开发

    人员则针对这些接口,填充代码,如数据库操作等。这样,核心技术人

    员把更多精力投入到了技术攻关和业务逻辑中。前端针对接口编程,只

    管在Action层调用Service,不管实现细节;而后端则要负责Dao和

    Service层接口实现。这样,就实现了代码分工与合作。

    1.4.2 对PHP接口的思考

    PHP的接口自始至终一直在被争议,有人说接口很好,有人说接口

    像鸡肋。首先要明白,好和不好的判断标准是什么。无疑,这是和

    JavaC++相比。在上面的例子中,已经讨论了PHP的接口在“面向契约编

    程”中是不足的,并没有起到应有的作用。

    其实,在上面的interface.php代码中,machine类的声明应该在plain

    类前面。接口提供了一套规范,这是系统提供的,然后machine类提供

    一组针对接口的API并实现,最后才是自定义的类。在Java里,接口之

    所以盛行(多线程的runable接口、容器的collection接口等)就是因为系

    统为我们做了前面两部分的工作,而程序员,只需要去写具体的实现

    类,就能保证接口可用可控。

    为什么要用接口?接口到底有什么好处?接口本身并不提供实现,只是提供一个规范。如果我们知道一个类实现了某个接口,那么就知道

    了可以调用该接口的哪些方法,我们只需要知道这些就够了。

    PHP中,接口的语义是有限的,使用接口的地方并不多,PHP中接

    口可以淡化为设计文档,起到一个团队基本契约的作用,代码清单1-12所示。

    代码清单1-12 cache_imp.php

    
    interface cache{

    @describe:缓存管理,项目经理定义接口,技术人员负责实现

    const maxKey=10000;最大缓存量

    public function getc(key);获取缓存

    public function setc(key,value);设置缓存

    public function flush;清空缓存

    }

    由于PHP是弱类型,且强调灵活,所以并不推荐大规模使用接口,而是仅在部分“内核”代码中使用接口,因为PHP中的接口已经失去很多

    接口应该具有的语义。从语义上考虑,可以更多地使用抽象类。至于抽

    象类和接口的比较,不再赘述。

    另外,PHP5对面向对象的特性做了许多增强,其中就有一个

    SPL(标准PHP库)的尝试。SPL中实现一些接口,其中最主要的就是

    Iterator迭代器接口,通过实现这个接口,就能使对象能够用于foreach结

    构,从而在使用形式上比较统一。比如SPL中有一个DirectoryIterator

    类,这个类在继承SplFileInfo类的同时,实现Iterator、Traversable、SeekableIterator这三个接口,那么这个类的实例可以获得父类SplFileInfo

    的全部功能外,还能够实现Iterator接口所展示的那些操作。

    Iterator接口的原型如下:

    current

    This method returns the current index's value.You are solely

    responsible for tracking what the current index is as the

    interface does not do this for you.

    key

    This method returns the value of the current index's key.For

    foreach loops this is extremely important so that the key

    value can be populated.

    next

    This method moves the internal index forward one entry.

    rewind

    This method should reset the internal index to the first element.

    valid

    This method should return true or false if there is a current

    element.It is called after rewindor next.

    如果一个类声明了实现Iterator接口,就必须实现这五个方法,如果

    实现了这五个方法,那么就可以很容易对这个类的实例进行迭代。这

    里,DirectoryIterator类之所以拿来就能用,是因为系统已经实现了

    Iterator接口,所以可以像下面这样使用:
    dir=new DirectoryIterator(dirname(_FILE_));

    foreach(dir asfileinfo){

    if(!fileinfo->isDir){

    echo

    fileinfo->getFilename,\t,fileinfo->getSize,PHP_EOL;

    }

    }

    可以想象,如果不用DirectoryIterator类,而是自己实现,不但代码

    量增加了,而且循环时候的风格也不统一了。如果自己写的类也实现了

    Iterator接口,那么就可以像Iterator那样工作。

    为什么一个类只要实现了Iterator迭代器,其对象就可以被用作

    foreach的对象呢?其实原因很简单,在对PHP实例对象使用foreach语法

    时,会检查这个实例有没有实现Iterator接口,如果实现了,就会通过内

    置方法或使用实现类中的方法模拟foreach语句。这是不是和前面提到的

    _toString方法的实现很像呢?事实上,_toString方法就是接口的一种变

    相实现。

    接口就是这样,接口本身什么也不做,系统悄悄地在内部实现了接

    口的行为,所以只要实现这个接口,就可以使用接口提供的方法。这就

    是接口“即插即用”思想。

    我们都知道,接口是对多重继承的一种变相实现,而在讲继承时,我们提到了用来实现混入(Mixin)式的Traits,实际上,Traits可以被视为

    一种加强型的接口。

    来看一段代码:

    
    trait Hello{

    public function sayHello{

    echo'Hello';

    }

    }

    trait World{

    public function sayWorld

    echo'World';

    }

    }

    class MyHelloWorld{

    use Hello,World;

    public function sayExclamationMark{

    echo'!';

    }

    }

    o = new MyHelloWorld;

    o ->sayHello;

    o ->sayWorld;

    o ->sayExclamationMark;

    ·>

    上面的代码运行结果如下:Hello World!

    这里的MyHelloWorld同时实现了两个Traits,从而使其可以分别调

    用两个Traits里的代码段。从代码中就可以看出,Traits和接口很像,不

    同的是Traits是可以导入包含代码的接口。从某种意义上来说,Traits和

    接口都是对“多重继承”的一种变相实现。

    总结关于接口的几个概念:

    接口作为一种规范和契约存在。作为规范,接口应该保证可用性;

    作为契约,接口应该保证可控性。

    接口只是一个声明,一旦使用interface关键字,就应该实现它。可

    以由程序员实现(外部接口),也可以由系统实现(内部接口)。接口

    本身什么都不做,但是它可以告诉我们它能做什么。

    PHP中的接口存在两个不足,一是没有契约限制,二是缺少足够多

    的内部接口。

    接口其实很简单,但是接口的各种应用很灵活,设计模式中也有很

    大一部分是围绕接口展开的。1.5 反射

    面向对象编程中对象被赋予了自省的能力,而这个自省的过程就是

    反射。

    反射,直观理解就是根据到达地找到出发地和来源。比方说,我给

    你一个光秃秃的对象,我可以仅仅通过这个对象就能知道它所属的类、拥有哪些方法。

    反射指在PHP运行状态中,扩展分析PHP程序,导出或提取出关于

    类、方法、属性、参数等的详细信息,包括注释。这种动态获取信息以

    及动态调用对象方法的功能称为反射API。

    1.5.1 如何使用反射API

    以1.1节的代码为模板,直观地认识反射的使用,如代码清单1-13所

    示。

    代码清单1-13 reflection.php

    
    class person{

    public name;

    public gender;

    public function say{

    echothis->name,\tis,this->gender,\r\n;

    }

    public function_set(name,value){

    echoSetting name tovalue\r\n;

    this->name=value;

    }

    public function_get(name){

    if(!isset(this->name)){

    echo'未设置';

    this->name=正在为你设置默认值;

    }

    return this->name;

    }

    }

    student=new person;

    student->name='Tom';

    student->gender='male';

    student->age=24;

    现在,要获取这个student对象的方法和属性列表该怎么做呢?如以

    下代码所示:

    获取对象属性列表

    reflect=new ReflectionObject(student);

    props=reflect->getProperties;

    foreach(props as prop){

    printprop->getName.\n;}

    获取对象方法列表

    m=reflect->getMethods;

    foreach(m as prop){

    printprop->getName.\n;

    }

    也可以不用反射API,使用class函数,返回对象属性的关联数组以

    及更多的信息:

    返回对象属性的关联数组

    var_dump(get_object_vars(student));

    类属性

    var_dump(get_class_vars(get_class(student)));

    返回由类的方法名组成的数组

    var_dump(get_class_methods(get_class(student)));

    假如这个对象是从其他页面传过来的,怎么知道它属于哪个类呢?

    一句代码就可以搞定:

    获取对象属性列表所属的类

    echo get_class(student);

    反射API的功能显然更强大,甚至能还原这个类的原型,包括方法

    的访问权限,如代码清单1-14所示。

    代码清单1-14 使用反射API

    反射获取类的原型

    obj = new ReflectionClass('person');

    className = obj->getName;

    Methods = Properties = array;

    foreach(obj->getPropertiesasv)

    {

    Properties[v->getName] = v;

    }

    foreach(obj->getMethodsasv)

    {

    Methods[v->getName]=v;

    }

    echoclass{className}\n{\n;

    is_array(Properties)&&ksort(Properties);

    foreach(Properties ask => v)

    {

    echo\t;

    echov->isPublic?'public':'',v->isPrivate?'private':'',v->isProtected?'protected':'',v->isStatic?'static':'';

    echo\t{k}\n;

    }

    echo\n;

    if(is_array(Methods))ksort(Methods);

    foreach(Methods ask => v)

    {

    echo\tfunction{k}{}\n;

    }

    echo}\n;

    输出如下:class person

    {

    public gender

    public name

    function _get{}

    function _set{}

    function say{}

    }

    不仅如此,PHP手册中关于反射API更是有几十个,可以说,反射

    完整地描述了一个类或者对象的原型。反射不仅可以用于类和对象,还

    可以用于函数、扩展模块、异常等。

    1.5.2 反射有什么作用

    反射可以用于文档生成。因此可以用它对文件里的类进行扫描,逐

    个生成描述文档。

    既然反射可以探知类的内部结构,那么是不是可以用它做hook实现

    插件功能呢?或者是做动态代理呢?抛砖引玉,代码清单1-15是个简单

    的举例。

    代码清单1-15 proxy.php

    
    class mysql{

    function connect(db){

    echo连接到数据库{db[0]}\r\n;

    }

    }

    class sqlproxy{

    private target;

    function_construct(tar){

    this->target[]=new tar;

    }

    function_call(name,args){

    foreach(this->target asobj){

    r=new ReflectionClass(obj);

    if(method=r->getMethod(name)){

    if(method->isPublic&&!method->isAbstract){

    echo方法前拦截记录LOG\r\n;

    method->invoke(obj,args);

    echo方法后拦截\r\n;

    }

    }

    }

    }

    }

    obj=new sqlproxy('mysql');

    obj->connect('member');

    这里简单说明一下,真正的操作类是mysql类,但是sqlproxy类实现

    了根据动态传入参数,代替实际的类运行,并且在方法运行前后进行拦

    截,并且动态地改变类中的方法和属性。这就是简单的动态代理。

    在平常开发中,用到反射的地方不多:一个是对对象进行调试,另

    一个是获取类的信息。在MVC和插件开发中,使用反射很常见,但是反射的消耗也很大,在可以找到替代方案的情况下,就不要滥用。

    PHP有Token函数,可以通过这个机制实现一些反射功能。从简单

    灵活的角度讲,使用已经提供的反射API是可取的。

    很多时候,善用反射能保持代码的优雅和简洁,但反射也会破坏类

    的封装性,因为反射可以使本不应该暴露的方法或属性被强制暴露了出

    来,这既是优点也是缺点。

    思考题 为什么要使用反射,反射存在的必要性是什么?或者说,反射为什么会存在?

    (已知一些情况:C语言是面向过程的编程语言,PHP、C++、Java

    是具有面向对象风格的编程语言。C语言和C++中没有对反射的原生支

    持,而PHP和Java具有反射API。可以思考一下,为什么CC++语言里没

    有反射,以及CC++语言里是否需要反射?)1.6 异常和错误处理

    在语言级别上,通常具有许多错误处理模式,但这些模式往往建立

    在约定俗成的基础上,也就是说这些错误都是预知的。但是在大型程序

    中,如果每次调用都去逐一检查错误,会使代码变得冗长复杂,到处充

    斥着if…else,并且严重降低代码的可读性。而且人的因素也是不可信赖

    的,程序员可能并不会把这些问题当一回事,从而导致业务异常。在这

    种背景下,就逐渐形成了异常处理机制,或者强迫消除这些问题,或者

    把问题提交给能解决它的环境。这就把“描述在正常过程中做什么事的

    代码”和“出了问题怎么办的代码”进行分离。

    1.6.1 如何使用异常处理机制

    异常的思想最早可以追溯到20世纪60年代,其在C++、Java中发扬

    光大,PHP则部分借鉴了这两种语言的异常处理机制。

    PHP里的异常,是程序运行中不符合预期的情况及与正常流程不同

    的状况。一种不正常的情况,就是按照正常逻辑不该出错,但仍然出错

    的情况,这属于逻辑和业务流程的一种中断,而不是语法错误。PHP里

    的错误则属于自身问题,是一种非法语法或者环境问题导致的、让编译

    器无法通过检查甚至无法运行的情况。

    在各种语言里,异常(exception)和错误(error)的概念是不一样

    的。在PHP里,遇到任何自身错误都会触发一个错误,而不是抛出异常

    (对于一些情况,会同时抛出异常和错误)。PHP一旦遇到非正常代

    码,通常都会触发错误,而不是抛出异常。在这个意义上,如果想使用

    异常处理不可预料的问题,是办不到的。比如,想在文件不存在且数据

    库连接打不开时触发异常,是不可行的。这在PHP里把它作为错误抛

    出,而不会作为异常自动捕获。

    以经典的除零问题为例,如代码清单1-16所示。

    代码清单1-16 exception.php

    exception.php

    
    a=null;

    try{

    a=50;echoa,PHP_EOL;

    }catch(exceptione){

    e->getMessage;

    a=-1;

    }

    echo a;

    运行结果如图1-8所示。

    图 1-8 PHP里的除零错误

    代码清单1-17所示是Java代码。

    代码清单1-17 ExceptionTry.java

    ExceptionTry.java

    public class ExcepetionTry{

    public static void tpthrows ArithmeticException{

    int a;

    a=50;

    System.out.println(运算结果:+a);

    }

    public static void main(String[]args){

    int a;

    try{

    a=50;

    System.out.println(运算结果:+a);

    }catch(ArithmeticException e){

    e.printStackTrace;

    }finally{

    a=-1;

    System.out.println(运算结果:+a);

    }

    try{

    ExcepetionTry.tp;

    }catch(Exception e){

    System.out.println(异常被捕获);

    }

    }

    }

    运行结果如图1-9所示。图 1-9 Java里的除零异常

    把tp方法中的第二条语句改为如下形式:

    a=51;

    修改后的结果如图1-10所示。

    图 1-10 Java里的异常

    由以上运行结果可以看到,对于除零这种“异常”情况,PHP认为这

    是一个错误,直接触发错误(warning也是错误,只是错误等级不一

    样),而不会自动抛出异常使程序进入异常流程,故最终a值并不是预

    想中的-1,也就是说,并没有进入异常分支,也没有处理异常。PHP只

    有你主动throw后,才能捕获异常(一般情况是这样,也有一些异常PHP

    可以自动捕获)。

    而对于Java,则认为除零属于ArithmeticException,会对其进行捕

    获,并对异常进行处理。

    也就是说,PHP通常是无法自动捕获有意义的异常的,它把所有不

    正常的情况都视作了错误,你要想捕获这个异常,就得使用if…else结

    构,保证代码是正常的,然后判断如果除数为0,则手动抛出异常,再捕获。Java有一套完整的异常机制,内置很多异常类会自动捕获各种各

    样的异常。但PHP这个机制不完善。PHP内建的常见异常类主要有

    pdoexception、reflection exception。

    注意 其实PHP和Java之间之所以有这个差距,根本原因就在于,在Java里,异常是唯一的错误报告方式,而在PHP中却不是这样。通俗

    一点讲,就是这两种语言对异常和错误的界定存在分歧。什么是异常,什么是错误,两种语言的设计者存在不同的观点。

    也就是说,PHP只有手动抛出异常后才能捕获异常,或者是有内建

    的异常机制时,会先触发错误,再捕获异常。那么PHP里的异常用法应

    该是什么样的呢?看下面的例子。

    先定义两个异常类,它们需要继承自系统的exception,如代码清单

    1-18所示。

    代码清单1-18 定义两个异常类

    class emailException extends exception{

    }

    class pwdException extends exception{

    function_toString

    {

    return
Exception{this->getCode}:

    {this->getMessage}

    in File:{this->getFile}on line:{this->getLine}
;

    改写抛出异常结果

    }

    }

    然后就是实际的业务,根据业务需求抛出不同异常,如代码清单1-

    19所示。

    代码清单1-19 根据业务需求抛出不同异常

    function reg(reginfo=null){

    if(empty(reginfo)||!isset(reginfo)){

    throw new Exception(参数非法);

    }

    if(empty(reginfo['email'])){

    throw new emailException(邮件为空);

    }

    if(reginfo['pwd']!=reginfo['repwd']){

    throw new pwdException(两次密码不一致);

    }

    echo'注册成功';

    }

    上面的代码判断传入的参数,根据业务进行异常分发。首先,如果

    没有传入任何参数(这个参数可以是POST进来,也可以是别的地方赋值得到),就把异常分发给exception超类,跳出注册流程;如果Email

    地址不存在,那么把异常分发给自定义的emailException异常,跳出注

    册流程;如果两次密码不一致,则将异常分发给自定义的

    pwdException,跳出注册流程。

    现在异常分发了,但还不算完,还需要对异常进行分拣并做处理。

    代码如下所示:

    try{

    reg(array('email'=>'waitfox@qq.com','pwd'=>123456,'repwd'=>12345678));

    reg;

    }catch(emailExceptionee){

    echoee->getMessage;

    }catch(pwdExceptionep){

    echoep;

    echo PHP_EOL,'特殊处理';

    }catch(Exceptione){

    echoe->getTraceAsString;

    echo PHP_EOL,'其他情况,统一处理';

    }

    这一段代码用于捕获所抛出的各种异常,进行分门别类的处理。

    提示 可以尝试不同注册条件,看看异常分拣的流程。需要注意,exception作为超类应该放在最后捕获。不然,捕获这个异常超类后,后

    面的捕获就终止了,而这个超类不能提供针对性的信息和处理。

    在这里,对表单进行异常处理,通过重写异常类、手动抛出错误的

    方式进行异常处理。这是一种业务异常,可以人为地把所有不符合要求

    的情况都视作业务异常,和通常意义上的代码异常相区别。

    那PHP里的异常应该怎么用?在什么时候抛出异常,什么时候捕获

    呢?什么场景下能应用异常?在下面三种场景下会用到异常处理机制。

    1.对程序的悲观预测

    如果一个程序员对自己的代码有“悲观情绪”,这里并不是指该程序

    员代码质量不高,而是他认为自己的代码无法一一处理各种可预见、不

    可预见的情况,那该程序员就会进行异常处理。假设一个场景,程序员

    悲观地认为自己的这段代码在高并发条件下可能产生死锁,那么他就会

    悲观地抛出异常,然后在死锁时进行捕获,对异常进行细致的处理。

    2.程序的需要和对业务的关注如果程序员希望业务代码中不会充斥大堆的打印、调试等处理,通

    常他们会使用异常机制;或者业务上需要定义一些自己的异常,这个时

    候就需要自定义一个异常,对现实世界中各种各样的业务进行补充。比

    如上班迟到,这种情况认为是一个异常,要收集起来,到月底集中处

    理,扣你工资;如果程序员希望有预见性地处理可能发生的、会影响正

    常业务的代码,那么它需要异常。在这里,强调了异常是业务处理中必

    不可少的环节,不能对异常视而不见。异常机制认为,数据一致很重

    要,在数据一致性可能被破坏时,就需要异常机制进行事后补救。

    举个例子,比如有个上传文件的业务需求,要把上传的文件保存在

    一个目录里,并在数据库里插入这个文件的记录,那么这两步就是互相

    关联、密不可分的一个集成的业务,缺一不可。文件保存失败,而插入

    记录成功就会导致无法下载文件;而文件保存成功数据库写入失败,则

    会导致没有记录的文件成为死文件,永远得不到下载。

    那么假设文件保存成功后没有提示,但是保存失败会自动抛出异

    常,访问数据库也一样,插入成功没有提示,失败则自动抛出异常,就

    可以把这两个有可能抛出异常的代码段包在一个try语句里,然后用

    catch捕捉错误,在catch代码段里删除没有被记录到数据库的文件或者

    删除没有文件的记录,以保证业务数据的一致性。因此,从业务这个角

    度讲,异常偏重于保护业务数据一致性,并且强调对异常业务的处理。

    如果代码中只是象征性地try…catch,然后打印一个报错,最后

    over。这样的异常不如不用,因为其没有体现异常思想。所以,合理的

    代码应该如下:

    
    try{

    可能出错的代码段

    if(文件上传不成功)throw(上传异常);

    if(插入数据库不成功)throw(数据库操作异常);

    }catch(异常){

    必须的补救措施,如删除文件、删除数据库插入记录,这个处理很细致

    }

    ....

    ·>

    也可以如下:

    
    上传{

    if(文件上传不成功)throw(上传异常);

    if(插入数据库不成功)throw(数据库操作异常);

    }

    其他代码...

    try{

    上传;

    其他;}catch(上传异常){

    必须的补救措施,如删除文件,删除数据库插入记录

    }catch(其他异常){

    记录log

    }

    ·>

    上面两种捕获异常的方式中,前一种是在异常发生时立刻捕获;后

    一种是分散抛异常集中捕获。那到底应该是哪一种呢?

    如果业务很重要,那么异常越早处理越好,以保证程序在意外情况

    下能保持业务处理的一致性。比如一个操作有多个前提步骤,突然最后

    一个步骤异常了,那么其他前提操作都要消除掉才行,以保证数据的一

    致性。并且在这种核心业务下,有大量的代码来做善后工作,进行数据

    补救,这是一种比较悲观的、而又重要的异常。我们应把异常消灭在局

    部,避免异常的扩散。

    如果异常不是那么重要,并且在单一入口、MVC风格的应用中,为了保持代码流程的统一,则常常采用后一种异常处理方式。这种异常

    处理方式更多强调业务流程的走向,对善后工作并不是很关心。这是一

    种次要异常,其将异常集中处理从而使流程更专一。

    异常处理机制可以把每一件事当做事务考虑,还可以把异常看成一

    种内建的恢复系统。如果程序某部分失败,异常将恢复到某个已知稳定

    的点上,而这个点就是程序的上下文环境,而try块里面的代码就保存

    catch所要知道的程序上下文信息。因此,如果很看重异常,就应该分散

    进行try……catch处理。

    3.语言级别的健壮性要求

    在健壮性这点上,PHP是不足的。以Java为例,Java是一种面向企

    业级开发的语言,强调健壮性。Java中支持多线程,Java认为,多线程

    被中断这种情况是彻彻底底的无法预料和避免的。所以Java规定,凡是

    用了多线程,就必须正视这种情况。你要么抛出,不管它;要么捕获,进行处理。总之,你必须面对InterruptedException异常,不准回避。也

    就是异常发生后应对重要数据业务进行补救,当然你可以不做,但是你

    必须意识到,异常有可能发生。

    这类异常是强制的。更多异常是非强制的,由程序员决定。Java对

    异常的分类和约束,保证了Java程序的健壮性。异常就是无法控制的运行时错误,会导致出错时中断正常逻辑运

    行,该异常代码后面的逻辑都不能继续运行。那么try…catch处理的好

    处就是:可以把异常造成的逻辑中断破坏降到最小范围内,并且经过补

    救处理后不影响业务逻辑的完整性;乱抛异常和只抛不捕获,或捕获而

    不补救,会导致数据混乱。这就是异常处理的一个重要作用,就是通过

    精确控制运行时的流程,在程序中断时,有预见地用try缩小可能出错的

    影响范围,及时捕获异常发生并做出相应补救,以使逻辑流程仍然能回

    到正常轨道上。

    1.6.2 怎样看PHP的异常

    PHP中的异常机制是不足的,绝大多数情况下无法自动抛出异常,必须用if…else先进行判断,再手动抛出异常。手动抛异常的意义不是很

    大,因为这意味着在代码里已经充分预期到错误的出现,也就算不上真

    正的“异常”,而是意料之中。同时,这种方式还会使你陷入纷繁复杂的

    业务逻辑判断和处理中。

    Java语言做得比较好的就是定义了一堆内置的常见异常,不需要程

    序员判断各种异常情况后再手动抛出,编译器会代我们进行判断业务是

    否发生错误,若发生了,则自动抛出异常。作为程序员,只需要关心异

    常的捕获和随后补救,而不是像PHP那样关注到底会发生哪些异常,用

    if…else逐一判断,逐一抛出异常。

    有没有什么机制使得PHP可以自动抛出异常呢?有,那就是结合

    PHP中的错误处理主动抛出异常。

    使用异常能一定程度上会降低耦合性,但是也不能滥用。滥用异常

    的后果就是很可能导致代码被多处挂起,流程变得更复杂,难于理解。

    但是可以肯定,异常在PHP里有很大的价值,越复杂的应用,越需要合

    理考虑使用异常。

    提示 需要提醒读者关注,SPL里定义了一大堆exception,如

    BadMethodCallException、LogicException等,同时这些异常之间还存在

    层级关系。这些异常只是一个空壳,什么方法都没有,需要自己填充。

    它们实际上起到一个命名参考的作用。

    1.6.3 PHP中的错误级别错误处理本来不属于面向对象的范畴,但是既然讲到异常,就不得

    不提及异常的同胞兄弟——错误。

    PHP错误处理比异常的价值大得多。PHP错误的概念已经和异常做

    过比较,这里通过对PHP异常的认知,给PHP错误下个最直观最通俗的

    结论:PHP错误就是会使脚本运行不正常的情况。

    PHP错误有很多种,包括warning、notice、deprecated、fetal error

    等。这和一般意义的错误概念有些差别。所以,notice不叫通知,而叫

    通知级别的错误,warning也不叫警告,而叫警告级别的错误。

    错误大致分为以下几类。

    deprecated是最低级别的错误,表示“不推荐,不建议”。比如在PHP

    5中使用ereg系列的正则匹配函数就会报此类错误。这种错误一般由于

    使用不推荐的、过时的函数或语法造成的。其虽不影响PHP正常流程,但一般情况下建议修正。

    其次是notice。这种错误一般告诉你语法中存在不当的地方。如使

    用变量但是未定义就会报此错。最常见的,数组索引是字符时没有加引

    号,PHP就视为一个常量,先查找常量表,找不到再视为变量。虽然

    PHP是脚本语言,语法要求不严,但是仍然建议对变量进行初始化。这

    种错误不影响PHP正常流程。

    warning是级别比较高的错误,在语法中出现很不恰当的情况时才

    会报此错误,比如函数参数不匹配。这种级别的错误会导致得不到预期

    结果,故需要修改代码。

    更高级别的错误是fetal error。这是致命错误,直接导致PHP流程终

    结,后面的代码不再执行。这种问题非改不可。

    最高级别的错误是语法解析错误prase error。上面提到的错误都属

    于PHP代码运行期间错误,而语法解析错误属于语法检查阶段错误,这

    将导致PHP代码无法通过语法检查。

    错误级别不止这几个,最主要的都在前面提到了。PHP手册中一共

    定义了16个级别的错误,最常用的就这几个。代码清单1-20演示了常见

    级别的错误。代码清单1-20 error.php

    Error.php

    
    date='2012-12-20';

    if(ereg(([0-9]{4})-([0-9]{1,2})-([0-9]{1,2}),date,regs)){

    echoregs[3].regs[2].regs[1];

    }else{

    echoInvalid date format:date;

    }

    if(i>5){

    echo'i没有初始化啊',PHP_EOL;

    }

    a = array('o'=>2,4,6,8);

    echoa[o];

    result=array_sum(a,3);

    echo fun;

    echo'致命错误后呢?还会执行吗?';

    echo'最高级别的错误',55;

    这段代码演示至少四个级别的错误,如果看不全,应确保你的

    php.ini文件做了如下设定:

    error_reporting=E_ALL|E_STRICT

    display_errors=On

    error_reporting指定错误级别,上面的设置是最严格的错误级别,具

    体设置可以参php.ini。

    提示 有一个技巧我想你会用到,那就是在代码质量或者环境不可

    控时(比如数据库连接失败),使用error_reporting(0),这样就能屏蔽

    错误了,正式部署时可以采取这样的策略,防止错误消息泄露敏感信

    息。另外一个技巧就是在函数前加@符号,抑制错误信息输出,如

    @mysql_connect。

    1.6.4 PHP中的错误处理机制

    PHP里有一套错误处理机制,可以使用set_error_handler接管PHP错

    误处理,也可以使用trigger_error函数主动抛出一个错误。

    set_error_handler函数设置用户自定义的错误处理函数。函数用于

    创建运行期间的用户自己的错误处理方法。它需要先创建一个错误处理

    函数,然后设置错误级别。语法如下:

    set_error_handler(error_function,error_types)参数描述如下:

    error_function:规定发生错误时运行的函数。必需。

    error_types:规定在哪个错误报告级别会显示用户定义的错误。可

    选。默认为E_ALL。

    提示 如果使用该函数,会完全绕过标准PHP错误处理函数,如果

    有必要,用户定义的错误处理程序必须终止(die)脚本。

    如果在脚本执行前发生错误,由于在那时自定义程序还没有注册,因此就不会用到这个自定义错误处理程序。这先实现一个自定义的异常

    处理函数,如代码清单1-21所示。

    代码清单1-21 自定义的异常处理函数

    
    function customError(errno,errstr,errfile,errline)

    {

    echo错误代码:[{errno}]{errstr}\r\n;

    echo错误所在的代码行:{errline}文件{errfile}\r\n;

    echoPHP版本,PHP_VERSION,(,PHP_OS,)\r\n;

    die;

    }

    set_error_handler(customError,E_ALL|E_STRICT);

    a=array('o'=>2,4,6,8);

    echoa[o];

    在这个函数里,可以对错误的详情进行格式化输出,也可以做任何

    要做的事情,比如判断当前环境和权限给出不同的错误提示,可使用

    errer_log函数将错误记入log文件,还可以细化处理,针对errno的不同

    进行对应的处理。

    自定义的错误处理函数一定要有这四个输入变量errno、errstr、errfile、errline。

    errno是一组常量,代表错误的等级,同时也有一组整数和其对应,但一般使用其字符串值表示,这样语义更好一点。比如E_WARNING,其二进制掩码为4.,表示警告信息。

    接下来,就是将这个函数作为回调参数传递给set_error_handler。这

    样就能接管PHP原生的错误处理函数了。要注意的是,这种托管方式并

    不能托管所有种类的错误,如E_ERROR、E_PARSE、E_CORE_ERROR、E_CORE_WARNING、E_COMPILE_ERROR、E_COMPILE_WARNING,以及E_STRICT中的部分。这些错误会以最

    原始的方式显示,或者不显示。

    set_error_handler函数会接管PHP内置的错误处理,你可以在同一个

    页面使用restore_error_handler;取消接管。

    注意 如果使用自定义的set_error_handler接管PHP的错误处理,先

    前代码里的错误抑制@将失效,这种错误也会被显示。

    在PHP异常中,异常处理机制是有限的,无法自动抛出异常,必须

    手动进行,并且内置异常有限。PHP把许多异常看做错误,这样就可以

    把这些“异常”像错误一样用set_error_handler接管,进而主动抛出异常。

    代码如下所示:

    function customError(errno,errstr,errfile,errline)

    {

    自定义错误处理时,手动抛出异常

    throw new Exception(level.'|'.{errstr});

    }

    set_error_handler(customError,E_ALL|E_STRICT);

    try

    {

    a=50;

    }

    catch(Exceptione)

    {

    echo'错误信息:',e->getMessage;

    }

    这样就能捕获到异常和非致命的错误,就能按照1.6.1节里讲述的方

    法进行处理了,这样可以弥补PHP异常处理机制的部分不足。

    这种“曲折迂回”的处理方式存在的问题就是:必须依靠程序员自己

    来掌控对异常的处理,对于异常高发区、敏感区,如果程序员处理不

    好,就会导致前面所提到的业务数据不一致的问题。其优点在于,可以

    获得程序运行时的上下文信息,以进行针对性的补救。

    fetal error这样的错误虽然捕获不到,也无法在发生此错误后恢复流

    程处理,但是还是可以使用一些特殊方法对这种错误进行处理的。这需

    要用到一个函数——register_shutdown_function,此函数会在PHP程序终

    止或者die时触发一个函数,给PHP来一个短暂的“回光返照”。在PHP 4

    的时代,类不支持析构函数,常用这个函数模拟实现析构函数。实例代

    码如下:

    
    class Shutdown

    {public function stop

    {

    if(error_get_last)

    {

    print_r(error_get_last);

    }

    die('Stop.');

    }

    }

    register_shutdown_function(array(new Shutdown,'stop'));

    a=new a;将因为致命错误而失败

    echo'必须终止';

    可以运行看看效果。对于fetal error还能做点收尾工作,但是PHP流

    程的终止是必然的。对于Parse error级别的错误,你只有傻眼了,除了

    可以修改配置文件php.ini,什么都做不了,修改的内容如下:

    log_errors=On

    error_log=usrlogphp.log

    这样一旦PHP发生了错误,就会被记入log文件,方便以后查询。

    和exception类似,错误处理也有对应抛出错误的函数,那就是

    trigger_error函数,如下所示:

    
    divisor=0;

    if(divisor==0){

    trigger_error(Cannot divide by zero,E_USER_ERROR);

    }

    echo'break';

    关于错误处理,主要就是这些内容,还有一些错误处理和调试相

    关,我们将会放到后面的章节进行讲解。

    提示 在PHP中,错误和异常是两个不同的概念,这种设计从根本

    上导致了PHP的异常和其他语言相异。以Java为例,Java中,异常是错

    误唯一的报告方式。说到底,两者的区别就是对异常和错误的认识不同

    而产生的。PHP的异常绝大部分必须通过某种办法手动抛出,才能被捕

    获到,是一种半自动化的异常处理机制。

    无论是错误还是异常,都可以使用handler接管系统已有的处理机

    制。1.7 本章小结

    本章主要介绍面向对象思想的程序的组成元素——类和对象。类是

    一个动作和属性的模板,对象是数据的集合。结合PHP自身实际情况,着重讲述PHP里面向对象的一些比较模糊的知识点,包括魔术方法、接

    口、多态、类的复用、反射、异常机制等。接口是一种类型,从接口的

    实现讲述接口是怎么实现“即插即用”的。

    然后,对异常机制进行探讨。讲述异常应该是什么样的,应该怎么

    用,并且阐述了PHP中的异常为什么会这样,应该在什么场合使用异常

    等。PHP起初没有异常机制,后期为了进军企业级开发,才模仿Java加

    进去的,故有了错误处理和异常处理的并存,这种形式导致PHP异常处

    理不伦不类,通过和Java对比,让我们了解到了异常的真实含义。错误

    处理是对异常处理的一种补充。

    到底面向过程和面向对象孰优孰劣呢?答案是:二者间并无高低优

    劣之别,它们各有优劣。

    其实在OO发展中,暴露出一些问题,如深入对象内部读写状态存

    在的困难,现实和开发中不对应造成的建模困难,数据与逻辑绑定造成

    的类型臃肿。比如前面提到的反射,就是因为面向对象的封装导致读写

    内部状态比较困难而产生的。

    面向对象存在的问题是越来越多的语言引入函数式编程的特征,如

    闭包、回调等。PHP也引入一些函数式编程的概念,有兴趣的读者可以

    自行研究。第2章 面向对象的设计原则

    第1章已经说过,面向对象是一种高度抽象的思维。在面向对象设

    计中,类是基本单位,各种设计都是围绕着类来进行的。可以说,类与

    类之间的关系,构成了设计模式的大部分内容。

    在初学阶段,可以认为类就是属性+函数组成的,实际上在底层存

    储上也确实是这样的,但是,这些仅仅是确定一个独立的类。而类与类

    之间的关系是设计模式所要探讨的内容。

    经典的设计模式有23种,每种都是对代码复用和设计的总结,就设

    计模式而言,除了熟读GOF经典外,推荐《敏捷软件开发——原则、方

    法与实践》一书。本章并不就具体的设计模式展开讨论,而是讨论一些

    基本的设计原则,并给出一些小的实例,最后,作为前两章的总结,探

    讨一下PHP中的面向对象的一些问题。2.1 面向对象设计的五大原则

    在面向对象的设计中,如何通过很小的设计改变就可以应对设计需

    求的变化,这是设计者极为关注的问题。为此不少OO先驱提出了很多

    有关面向对象的设计原则用于指导OO的设计和开发。下面是几条与类

    设计相关的设计原则。

    面向对象设计的五大原则分别是单一职责原则、接口隔离原则、开

    放-封闭原则、替换原则、依赖倒置原则。这五大原则也是23种设计模

    式的基础[1]。

    2.1.1 单一职责原则

    亚当·斯密曾就制针业做过一个分工产生效率的例子[2]。对于一个没

    有受过相应训练,又不知道怎样使用这种职业机械的工人来讲,即使他

    竭尽全力地工作,也许一天连一根针也生产不出来,当然更生产不出20

    根针了。但是,如果把这个行业分成各种专门的组织,再把这种组织分

    成许多个部门,其中大部分部门也同样分为专门的组织。把制针分为18

    种不同工序,这18种不同操作由18个不同工人来担任。那么,尽管他们

    的机器设备都很差,但他们尽力工作,一天也能生产12磅针。每磅中等

    型号针有4000根,按这个数字计算,十多个人每天就可以制造48000根

    针,而每个人每天能制造4800根针。如果他们各自独立地工作,谁也不

    专学做一种专门的业务,那么他们之中无论是谁都绝不可能一天制造20

    根针,也许连1根针也制造不出来。这就是企业管理中的分工,在面向

    对象的设计里,叫做单一职责原则(Single Pesponsibility Principle,SRP)。

    在《敏捷软件开发》中,把“职责”定义为“变化的原因”,也就是

    说,就一个类而言,应该只有一个引起它变化的原因。这是一个最简

    单,最容易理解却最不容易做到的一个设计原则。说得简单一点,就是

    怎样设计类以及类的方法界定的问题。这种问题是很普遍的,比如在

    MVC的框架中,很多人会有这样的疑惑,对于表单插入数据库字段过

    滤与安全检查应该是放在control层处理还是model层处理,这类问题都

    可以归到单一职责的范围。

    再比如在职员类里,将工程师、销售人员、销售经理等都放在职员类里考虑,其结果将会非常混乱。在这个假设下,职员类里的每个方法

    都要用if…else判断是哪种情况,从类结构上来说将会十分臃肿,并且上

    述三种职员类型,不论哪一种发生需求变化,都会改变职员类,这是我

    们所不愿意看到的!

    从上面的描述中应该能看出,单一职责有两个含义:一个是避免相

    同的职责分散到不同的类中,另一个是避免一个类承担太多职责。

    那为什么要遵守SRP呢?

    (1)可以减少类之间的耦合

    如果减少类之间的耦合,当需求变化时,只修改一个类,从而也就

    隔离了变化;如果一个类有多个不同职责,它们耦合在一起,当一个职

    责发生变化时,可能会影响其他职责。

    (2)提高类的复用性

    修理电脑比修理电视机简单多了。主要原因就在于电视机各个部件

    之间的耦合性太高,而电脑则不同,电脑的内存、硬盘、声卡、网卡、键盘灯部件都可以很容易地单独拆卸和组装。某个部件坏了,换上新的

    即可。

    上面的例子就体现了单一职责的优势。由于使用了单一职责,使

    得“组件”可以方便地“拆卸”和“组装”。

    不遵守SRP会影响对该类的复用性。当只需要复用该类的某一个职

    责时,由于它和其他的职责耦合在一起,也就很难分离出。

    遵守SRP在实际代码开发中有没有什么应用?有的。以数据持久层

    为例,所谓的数据持久层主要指的是数据库操作,当然,还包括缓存管

    理等。以数据库操作为例,如果是一个复杂的系统,那么就可能涉及多

    种数据库的相互读写等,这时就需要数据持久层支持多种数据库。应该

    怎么做?定义多个数据库操作类?你的想法已经很接近了,再进一步,就是使用工厂模式。

    工厂模式(Factory)允许你在代码执行时实例化对象。它之所以被

    称为工厂模式是因为它负责“生产”对象。以数据库为例,工厂需要的就是根据不同的参数,生成不同的实例化对象。最简单的工厂就是根据传

    入的类型名实例化对象,如传入MySQL,就调用MySQL的类并实例

    化,如果是SQLite,则调用SQLite的类并实例化,甚至可以处理TXT、Excel等“类数据库”。工厂类也就是这样的一个类,它只负责生产对象,而不负责对象的具体内容。

    先定义一个接口,规定一些通用的方法,如代码清单2-1所示。

    代码清单2-1 定义一个适配器接口

    
    interface Db_Adapter{

    数据库连接

    @paramconfig 数据库配置

    @return resource

    public function connect(config);

    执行数据库查询

    @param stringquery 数据库查询SQL字符串

    @param mixedhandle 连接对象

    @return resource

    public function query(query,handle);

    }

    ·>

    这是一个简化的接口,并没有提供所有方法,其定义了MySQL数

    据库的操作类,这个类实现了Db_Adapter接口,具体如代码清单2-2所

    示。

    代码清单2-2 定义MySQL数据库的操作类

    
    class Db_Adapter_Mysql implements Db_Adapter

    {

    private_dbLink;数据库连接字符串标示

    数据库连接函数

    @paramconfig 数据库配置

    @throws Db_Exception

    @return resource

    public function connect(config)

    {

    if(this->_dbLink=@mysql_connect(config->host.

    (empty(config->port)?'':':'.config->port),config->user,config->password,true)){

    if(@mysql_select_db(config->database,this->_dbLink)){

    if(config->charset){

    mysql_query(SET NAMES'{config->charset}',this->_dbLink);

    }

    returnthis->_dbLink;

    }

    }

    数据库异常

    throw new Db_Exception(@mysql_error(this->_dbLink));

    }

    执行数据库查询

    @param stringquery 数据库查询SQL字符串

    @param mixedhandle 连接对象

    @return resource

    public function query(query,handle)

    {

    if(resource=@mysql_query(query,handle)){

    returnresource;

    }

    }

    }

    ·>

    接下来是SQLite数据库的操作类,同样实现了Db_Adapter接口,如

    代码清单2-3所示。

    代码清单2-3 SQLite数据库的操作类

    
    class Db_Adapter_sqlite implements Db_Adapter

    {

    private_dbLink;数据库连接字符串标示

    数据库连接函数

    @paramconfig 数据库配置

    @throws Db_Exception

    @return resource

    public function connect(config)

    {

    if(this->_dblink=sqlite_open(config->file,0666,error)){

    returnthis->_dblink;

    }

    数据库异常

    throw new Db_Exception(error);

    }

    执行数据库查询

    @param stringquery 数据库查询SQL字符串

    @param mixedhandle 连接对象

    @return resource

    public function query(query,handle)

    {

    if(resource=@sqlite_query(query,handle)){

    returnresource;

    }

    }

    }

    好了,如果现在需要一个数据库操作的方法的话怎么做?只需定义

    一个工厂类,根据传入不同的参数生成需要的类即可,如代码清单2-4

    所示。

    代码清单2-4 定义一个工厂类

    
    class sqlFactory

    {

    public static function factory(type)

    {

    if(include_once'Drivers'.type.'.php'){

    classname='Db_Adapter_'.type;

    return newclassname;

    }else{

    throw new Exception('Driver not found');

    }

    }

    }

    ·>要调用时,就可以这么写:

    db=sqlFactory::factory('MySQL');

    db=sqlFactory::factory('SQLite');

    我们把创建数据库连接这块程序单独拿出来,程序中的CURD就不

    用关心是什么数据库了,只要按照规范使用对应的方法即可。

    工厂方法让具体的对象解脱了出来,使其并不再依赖具体的类,而

    是抽象。除了数据库操作这种显而易见的设计外,还有什么地方会用到

    工厂类呢?那就是SNS中的动态实现。

    下面的图片来自国内某SNS网站,属于当前新鲜事页面,可以看到

    针对不同行为,其生成了不同动态。比如,参加了某个小组,动态显示

    的就是“XX参加了YY小组”;收到某某的礼物,别人看到的多台就

    是“XX收到了YY的ZZ礼物”,如图2-1所示。图 2-1 某SNS网站的动态展示

    以上这种动态应该怎么设计呢,最容易想到的就是用工厂模式,根

    据传入的操作不同,结合模板而生成不同的动态,如代码清单2-5所

    示。

    代码清单2-5 工厂模式

    

    

    

    

    

    

    

    

    

    以上代码是一个动态的生成配置,通过FEED的类型匹配到key,取

    到对应的bean,然后创建不同的动态,用的就是工厂模式。

    设计模式里面的命令模式也是SRP的体现,命令模式分离“命令的

    请求者”和“命令的实现者”方面的职责。举一个很好理解的例子,就是

    你去餐馆吃饭,餐馆存在顾客、服务员、厨师三个角色。作为顾客,你

    只要列出菜单,传给服务员,由服务员通知厨师去实现。作为服务员,只需要调用准备饭菜这个方法(对厨师大喊“该炒菜了”),厨师听到要

    炒菜的请求,就立即去做饭。在这里,命令的请求和实现就完成了解

    耦。

    模拟这个过程,首先定义厨师角色,厨师进行实际的做饭、烧汤的

    工作。详细代码如代码清单2-6所示。

    代码清单2-6 餐馆的示例

    厨师类,命令接受者与执行者

    class cook{

    public function meal{

    echo'番茄炒鸡蛋',PHP_EOL;

    }

    public function drink{

    echo'紫菜蛋花汤',PHP_EOL;

    }

    public function ok{

    echo'完毕',PHP_EOL;

    }

    }

    然后是命令接口

    interface Command{

    命令接口public function execute;

    }

    现在轮到服务员出场,服务员是命令的传送者,通常你到饭馆吃饭

    都是叫服务员吧,不可能直接叫厨师,一般都是叫“服务员,给我来盘

    番茄炒西红柿”,而不会直接叫“厨师,给我来盘番茄炒西红柿”。所

    以,服务员是顾客和厨师之间的命令沟通者。模拟这个过程的代码如代

    码清单2-7所示。

    代码清单2-7 模拟服务员与厨师的过程

    class MealCommand implements Command{

    privatecook;

    绑定命令接受者

    public function_construct(cookcook){

    this->cook=cook;

    }

    public function execute{

    this->cook->meal;把消息传递给厨师,让厨师做饭,下同

    }

    }

    class DrinkCommand implements Command{

    privatecook;

    绑定命令接受者

    public function_construct(cookcook){

    this->cook=cook;

    }

    public function execute{

    this->cook->drink;

    }

    }

    现在顾客可以按照菜单叫服务员了,如代码清单2-8所示。

    代码清单2-8 模拟顾客与服务员的过程

    class cookControl{

    privatemealcommand;

    privatedrinkcommand;

    将命令发送者绑定到命令接收器上面来

    public function addCommand(Commandmealcommand,Commanddrinkcommand){

    this->mealcommand=mealcommand;

    this->drinkcommand=drinkcommand;

    }

    public function callmeal{

    this->mealcommand->execute;

    }

    public function calldrink{

    this->drinkcommand->execute;

    }

    }

    好了,现在完成整个过程,如代码清单2-9所示。

    代码清单2-9 实现命令模式

    control=new cookControl;

    cook=new cook;

    mealcommand=new MealCommand(cook);

    drinkcommand=new DrinkCommand(cook);control->addCommand(mealcommand,drinkcommand);

    control->callmeal;

    control->calldrink;

    从上面的例子可以看出,原来设计模式并非纯理论的东西,而是来

    源于实际生活,就连普通的餐馆老板都懂设计模式这门看似高深的学

    问。其实,在经济和管理活动中,对流程的优化就是对各种设计模式的

    摸索和实践。所以,设计模式并非计算机编程中的专利。事实上,设计

    模式的起源不是计算机学科,而是源于建筑学。

    在设计模式方面,不仅以上这两种体现了SRP,还有别的(比如代

    理模式)也体现了SRP。SRP不只是对类设计有意义,对以模块、子系

    统为单位的系统架构设计同样有意义。

    模块、子系统也应该仅有一个引起它变化的原因,如MVC所倡导

    的各个层之间的相互分离其实就是SRP在系统总体设计中的应用。图2-2

    是来自CI框架的流程图。图 2-2 MVC中的流程

    SRP是最简单的原则之一,也是最难做好的原则之一。我们会很自

    然地将职责连接在一起。找到并且分离这些职责是软件设计需要达到的

    目的。

    一些简单的应该遵循的做法如下:

    根据业务流程,把业务对象提炼出来。如果业务流层的链路太复

    杂,就把这个业务对象分离为多个单一业务对象。当业务链标准化后,对业务对象的内部情况做进一步处理。把第一次标准化视为最高层抽

    象,第二次视为次高层抽象,以此类推,直到“恰如其分”的设计层次。

    职责的分类需要注意。有业务职责,还要有脱离业务的抽象职责,从认识业务到抽象算法是一个层层递进的过程。就好比命令模式中的顾

    客,服务员和厨师的职责,作为老板(即设计师)的你需要规划好各自

    的职责范围,既要防止越俎代庖,也要防止互相推诿。

    2.1.2 接口隔离原则

    设计应用程序的时候,如果一个模块包含多个子模块,那么我们应

    该小心对该模块做出抽象。设想该模块由一个类实现,我们可以把系统

    抽象成一个接口。但是要添加一个新的模块扩展程序时,如果要添加的

    模块只包含原系统中的一些子模块,那么系统就会强迫我们实现接口中

    的所有方法,并且还要编写一些哑方法。这样的接口被称为胖接口或者

    被污染的接口,使用这样的接口将会给系统引入一些不当的行为,这些

    不当的行为可能导致不正确的结果,也可能导致资源浪费。

    1.接口隔离

    接口隔离原则(Interface Segregation Principle,ISP)表明客户端不

    应该被强迫实现一些他们不会使用的接口,应该把胖接口中的方法分

    组,然后用多个接口代替它,每个接口服务于一个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好得多。

    ISP的主要观点如下:

    1)一个类对另外一个类的依赖性应当是建立在最小的接口上的。ISP可以达到不强迫客户(接口的使用方)依赖于他们不用的方

    法,接口的实现类应该只呈现为单一职责的角色(遵守SRP原则)。

    ISP还可以降低客户之间的相互影响——当某个客户程序要求提供

    新的职责(需求变化)而迫使接口发生改变时,影响到其他客户程序的

    可能性会最小。

    2)客户端程序不应该依赖它不需要的接口方法(功能)。

    客户端程序不应该依赖它不需要的接口方法(功能),那依赖什

    么?依赖它所需要的接口。客户端需要什么接口就提供什么接口,把不

    需要的接口剔除,这就要求对接口进行细化,保证其纯洁性。

    比如在应用继承时,由于子类将继承父类中的所有可用的方法;而

    父类中的某些方法,在子类中可能并不需要。例如,普通员工和经理都

    继承自雇员这个接口,员工需要每天写工作日志,而经理则不需要。因

    此不能用工作日志来卡经理,也就是经理不应该依赖于提交工作日志这

    个方法。

    可以看出,ISP和SRP在概念上是有一定交叉的。事实上,很多设

    计模式在概念上都有交叉,甚至你很难判断一段代码属于哪一种设计模

    式。

    ISP强调的是接口对客户端的承诺越少越好,并且要做到专一。当

    某个客户程序的要求发生变化,而迫使接口发生改变时,影响到其他客

    户程序的可能性小。这实际上就是接口污染的问题。

    2.对接口的污染

    过于臃肿的接口设计是对接口的污染。所谓接口污染就是为接口添

    加不必要的职责,如果开发人员在接口中增加一个新功能的主要目的只

    是减少接口实现类的数目,则此设计将导致接口被不断地“污染”并“变

    胖”。

    接口污染会给系统带来维护困难和重用性差等方面的问题。为了能

    够重用被污染的接口,接口的实现类就被迫要实现并维护不必要的功能

    方法。“接口隔离”其实就是定制化服务设计的原则。使用接口的多重继承

    实现对不同的接口的组合,从而对外提供组合功能——达到“按需提供

    服务”。

    看下面这个例子,如图2-3所示。

    图 2-3 存在污染的接口设计

    客户A需要A服务,只要针对客户A的方法发生改变,客户B和客户

    C就会受到影响。故这种设计需要对接口进行隔离,如图2-4所示。图 2-4 减少接口中的污染由图2-4可知,如果针对客户A的方法发生改变,客户B和客户C并

    不会受到任何影响。你可能会想,这样做接口那岂不是会很多?这个问

    题问得很好,接口既要拆,但也不能拆得太细,这就得有个标准,这就

    是高内聚。接口应该具备一些基本的功能,能独一完成一个基本的任

    务。

    图2-4所示只是个抽象的例子,在实际应用中,会遇到如下问题:

    比如,我需要一个能适配多种类型数据库的DAO实现,那么首先应实现

    一个数据库操作的接口,其中规定一些数据库操作的基本方法,如连接

    数据库、增删查改、关闭数据库等。这是一个最少功能的接口。对于一

    些MySQL中特有的而其他数据库不具有或性质不同的方法,如PHP里可

    能用到的MySQL的pconnect方法,其他数据库里并不存在和这个方法相

    同的概念,这个方法也就不应该出现在这个基本的接口里,那这个基本

    的接口应该有哪些基本的方法呢?PDO已经告诉你了。

    PDO是一个抽象的数据接口层,它告诉我们一个基本的数据库操作

    接口应该实现哪些基本的方法。接口是一个高层次的抽象,所以接口里

    的方法应该是通用的、基本的、不易变化的。

    还有一个问题,那些特有的方法应该怎么实现?根据ISP原则,这

    些方法可以在另一个接口中存在,让这个“异类”同时实现这两个接口。

    对于接口的污染,可以考虑下面这两条处理方式:

    利用委托分离接口。

    利用多继承分离接口。

    委托模式中,有两个对象参与处理同一个请求,接受请求的对象将

    请求委托给另一个对象来处理,如策略模式、代理模式等中都应用到了

    委托的概念。至于其实现,在反射那一节其实已经实现了,这里就不再

    细讲了。

    利用多继承分离接口,在接口一节也做了相应的讲 ......

您现在查看是摘要介绍页, 详见PDF附件(31714KB,807页)