PHP 核心技术与最佳实践


第1 章面向对象思想的核心概念 面向对象是什么?以下是维基百科对面向对象的解释: 面向对象程序设计(Object-Oriented Programming, O O P )是一种程序设计范型,同时也 是_ 种程序开发方法。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的 重用性、灵活性和可扩展性。 面向过程、面向对象以及函数式编程被人们称为编程语言中的三大范式(实际上,面向过 程与面向对象都同属于命令式编程),是三种不同编码和设计风格。其中面向对象的核心思想 是对象、封装、可重用性和可扩展性。 面向对象是一种更高级、更抽象的思维方式,面向过程虽然也是一种抽象,但面向过程是 一种基础的抽象,面向对象又是建立在面向过程之上的更高层次的抽象,因此对面向对象的理 解也就不是那么容易了。 面向对象和具体的语言无关。在面向对象的世界里,常常提到的两种典型语言— C + + 和Java。它们都是很好的面向对象的开发语言。实际上,像C 语言这种大家普遍认为的面向 过程开发的主打语言,也能进行面向对象的开发,就连JavaScript这门很久之前一直被视作 面向过程编程的语言,人们对它的认识也发生了改变,逐渐承认其是面向对象的语言,并且 也接受了 JavaScript独特的面向对象的语法。所以我们说面向对象只是种程序设计的理念, 和具体的语言无关。不同的程序员既可以用C 语言写出面向对象的风格来,也可以用Java写 成面向对象的风格。这里并不是说面向对象的风格要优于面向过程,而是二者各有自己所擅 长的领域。OOPL ( Object- Oriented Programming Language ) 可以提高程序的封装性、复用性、 可维护性,但仅仅是“可以” ,能不能真正实现这些优点,还取决于编程和设计人员。就 PHP而言,其不是一门纯的面向对象的语言,但是仍然可以使用PHP写出好的面向对象风格 的代码。 实际开发中,面向对象为什么让我们觉得那么难?面向对象究竟难在什么地方?为什么面 向对象开发在PHP里一直不是很受重视,并且没有得到普及和推广? PHP对面向对象的支持到 底如何?怎么学习面向对象的思维? 在这里,我们将就面向对象一些概念展开讨论,其中重点讨论PHP特色的面向对象的风格 和语法,并通过相互借鉴和对比,使读者认识PHP自身的特点,尤其是和其他语言中不同的 地方。 1 . 1 面向对象的“形” 与“本" 类是对象的抽象组织,对象是类的具体存在。 2200年前的战国时期,赵国平原君的食客公孙龙在骑着白马进城时,被守城官以马不能人 城拦下,公孙龙即兴演讲,口述“白马非马” 一论,守城官无法反驳,于是公孙龙就骑着他的 白马(不是马的)进城去了。这就是历史上最经典的一次对面向对象思维的阐述。 公孙龙的“白马非马” 论如下: “ 白马非马” ,可乎?曰:“可。” 曰“何哉?” 曰:“马者,所以命形也;白者,所以命色 也。命色者非命形也。故曰:„ 白马非马‟ 。" 曰:“有白马不可谓无马也。不可谓无马者,非马 也?有白马为有马,白之,非马何也?” 曰:“求马,黄、黑马皆可致;求白马,黄、黑马不可 致。使白马乃马也,是所求一也。所求一者,白者不异马也。所求不异,如黄、黑马有可有不 可,何也?可与不可,其相非明。故黄、黑马一也,而可以应有马,而不可以应有白马,是白 马之非马,审矣!‟‟ 2 < ♦ PHP核心技术与最佳实践 公孙龙乃战国时期的“名家”,名家的中心论题是所谓“名” (概念)和“实” (存在)的 逻辑关系问题。名者,抽象也,类也。实者,具体也,对象也。从这个角度讲,公孙龙是我国 早期的最著名的面向对象思维的思想家。 “白马非马” 这一论断的关键就在于“非” 字,公孙龙一再强调白马与马的特征,通过把 白马和马视为两个不同的类,用“非” 这一关系,成功地把“ 白马” 与“马‛ 的关系由从属 关系转移到“白马” 这个对象与“马” 这个对象的相等关系上,显然,二者不等,故‚白马非 马”。而我们正常的思维是,马是一个类,白马是马这个类的一个对象,二者属于从属关系。 说“白马非马" ,就是割裂马与白马之间的从属关系,偷换概念,故为诡辩也。 白马非马这个典故,我们可以称之为诡辩。但我们把这个问题抽象出来,实际上讨论的就 是类与类之间的界定、类的定义等一系列问题,类应该抽象到什么程度,其中即涉及了类与对 象的本质问题,也涉及了类的设计过程中的一些原则。 1 . 1 . 1 对象的“形” 要回答类与对象本质这个问题,我想可以先从“形” 的角度来回答。本节以PHP为例,来 探讨对象的“形” 与“本” 的问题。 类是我们对一组对象的描述。 在PHP里,每个类的定义都以关键字class开头,后面跟着类名,紧接着一对花括号,里 面包含有类成员和方法的定义。如下面代码所示: c l a s s p e r s o n { p u b l i c $name; p u b l i c $gender; p u b l i c f u n c t i o n s a y () { e c h o $ t h i s - > name, "is ", $ t h i s - > gend e r ; 第1 章面向对象思想的核心概念♦ > 3 在这里,我们定义了一个p e ^o n类。代表了抽象出来的人这个概念,它含有姓名和性别这 两个属性,还具有一个开口说话的方法,这个方法会告诉外界这个人的性别和姓名。我们接下 来就可以产生这个类的实例: $ s t u d e n t ■ n e w p e r s o n (); $ s t u d e n t - > n a m e ■ • T o m •; $ s t u d e n t - > g e n d e r = ' m a l e •; S s t u d e n t - > s a y (); $ t e a c h e r = n e w p e r s o n (); $ t e a c h e r - > n a m e = • K a t e •; $ t e a c h e r - > g e n d e r = ' f e m a l e •; $ t e a c h e r - > s a y (); 这段代码则实例化了 person类,产生了一个student对象和teacher对象的实例。实际上也 就是从抽象到具体的过程。现实世界中,仅仅说“人” 是没有意义的,中国人把它叫“人‛, 美国人把它叫person或者h um a n ,如果高兴,把它叫“ God” 或者“板凳” 都无所谓。但是只 要你把“人” 这个概念加上各种属性和方法,比如说有两条腿、直立行走、会说话,则无论是 中国人,还是美国人,甚至外星人都是能理解你所描述的事物。所以,一个类的设计需要能充 分展示其最重要的属性和方法,并且能与其他事物相区分。只有类本身有意义,从抽象到具体 的实例化才会有意义。 根据上面的实例代码,可以有下面的一些理解: □ 类定义了一系列的属性和方法,并提供了实际的操作细节,这些方法可以用来对属性进 行加工。 □ 对象含有类属性的具体值,这就是类的实例化。正是由于属性的不同,才能区分不同 的对象。在上面例子里,由于student和teacher的性别和姓名不一样,才得以区分开 二人。 □ 类与对象的关系类似一种服务与被服务、加工与被加工的关系,具体而言,就如同原材 料与流水线的关系。只需要在对象上调用类中所存在的方法,就可以对类的属性进行加 工,并且展示其功能。 类是属性和方法的集合,那么在PH P里,对象是什么呢?比较普遍的说法就是‚对象由属 性和方法组成”。对象是由属性组成,这很好理解,一个对象的属性是它区别于另一个对象的 关键所在。由于PH P的对象是用数组来模拟的,因此我们把对象转为数组,就能看到这个对象 所拥有的属性了。 继续使用上面代码,可以打印student对象: p r i n t _ r ( (array) $ s t u d e n t ) ; v a r _ d u m p ( $ s t u d e n t ) ; 到这里,可以很直观地认识到,对象就是一堆数据。既然如此,可以把一个对象存储起 来,以便需要时用。这就是对象的序列化。 所谓序列化,就是把保存在内存中的各种对象状态(属性)保存起来,并且在需要时可以 4 ♦ > PHP核心技术与最佳实践 还原出来。下面的代码实现了把内存中的对象当前状态保存到一个文件中: $ s t r = s e r i a l i z e ( $ s t u d e n t ) ; e c h o $str; f i l e _ p u t一c o n t e n t s (• store.txt •, $ s t r ) ; 输出序列化后的结果: 0 : 6 : " p e r s o n " : 2 : { s : 4 :" n a m e " ;s : 3 :"Torn";s :6:" g e n d e r " ;s :4:" m a i l " ; } 在需要时,反序列化取出这个对象: $ s t r ■ f i l e一g e t一c o n t e n t s (' store-txt •); $ s t u d e n t ■ u n s e r i a l i z e ( $ s t r ) ; $ s t u d e n t - > s a y <); 注意在序列化和反序列化时都需要包含类的对象的定义,不然有可能出现在反序列化对象 时,找不到该对象的类的定义,而返回不正确的结果。 可以看到,对象序列化后,存储的只是对象的属性。类是由属性和方法组成的,而对象则 是属性的集合,由同一个类生成的不同对象,拥有各自不同的属性,但共享了类的代码空间中 方法区域的代码。 1 . 1 . 2 对象的“本” 我们需要更深人地了解这种机制,看对象的“本”。对象是什么?对象在P H P中也是变童 的一种,所以先看PH P源码中对变量的定义: # z e n d / z e n d h t y p e d e f u n i o n _ z v a 1 u e _ v a 1 u e { l o n g lval; /* l o n g v a l u e */ d o u b l e d v a l ; / * d o u b l e v a l u e */ s t r u c t { c h a r *val; int len; } str; H a s h T a b l e *ht; /* h a s h t a b l e v a l u e */ z e n d _ o b j e c t一v a l u e obj ; ) z v a l u e _ v a l u e ; zvalue_value, 就是 PHP 底层的变量类型,zend_object_ value o b j就是变量中的一个结构。接着看对象的底层实现。 在PHP5中,对象在底层的实现是采取“属性数组+ 方法数组” 来实现的。可以简单地理解为PH P对象在底 层的存储如图1-1所示。图1 - 1对象的组成 第1章面向对象思想的核心概念♦ > 5 对象在PHP中是使用一种zend_object_va|ue结构体来存储的。对象在ZEND (PHP底层引 擎,类似Java的JV M )中的定义如下: # z e n d / z e n d . h t y p e d e f s t r u c t _ z e n d一o b j e c t { z e n d一c l a s s _ e n t r y *ce; // 这里就是类入口 H a s h T a b l e * p r o p e r t i e s ; // 属性组成的 H a s h T a b l e H a s h T a b l e * gua r d s ; /* p r o t e c t s f r o m — g e t / — se t ... r e c u r s i o n */ } z e n d一obje c t ; ce是存储该对象的类结构,在对象初始化时保存了类的人口,相当于类指针的作用。properties 是一个 HashTable, 用来存放对象属性。guards用来阻止递归调用。 类的标准方法在zend/zend_object_handlers.h文件中定义,具体实现则是在zend/zend_ object_handlers_C文件中。关于PHP变量的存储结构的底层实现,将在第7 章中进行更深人的 介绍。 通过对上述源代码的简单阅读,可以更淸晰地认识到对象也是一种很普通的变量, 不同的 是其携带了对象的属性和类的人口。 1 . 1 . 3 对象与数组 对象是什么,我们不好理解,也不容易回答,但是我们知道数组是什么。数组的概念比较 简单。可以拿数组和对象对比来帮助我们理解对象。对象转化为数组,数组也能转换成对象。 数组是由键值对数据组成的,数组的键值对和对象的属性/ 属性值对十分相似。对象序列化后 和数组序列化后的结果是惊人的相似。如下面的代码所示: $ s t u d e n t _ a r r = a r r a y《• n a m e • = > • T o m、'gender • ■> ' m a l e *); e c h o H \n n ; e c h o s e r i a l i z e ( $ s t u d e n t _ a r r ) ; 输出为: a:2 : {s:4 :" n a m e " ; s : 3 : " T o m " ; s : 6 : " g e n d e r " ; s : 4 :" m a l e " ; } 可以很清楚地看出,对象和数组在内容上一模一样! 而对象和数组的区别在于:对象还有个指针,指向了它所属的类。在对student对象序列化 时,我们看到了 “person” 这几个字符,这个标识符就标志了这个对象归属于person类,故在 取出这个对象后,可以立即对其执行所包含的方法。如果对象中还包含对象呢?我们来看下一 节的内容。 1 . 1 . 4 对象与类 在前面代码中定义了一个类,并创建了这个类的对象,把前面产生的对象作为这个新对象 的一个属性,完整代码如代码清单1-1所示。 6 ♦ > PHP核心技术与最佳实践 ___________________________ 代码清单 1-1 object php < ? p h p c l a s s p e r s o n { p u b l i c $ n a m e ; p u b l i c $gender; p u b l i c f u n c t i o n s a y () { e c h o $ t h i s - > name, ** \tis ", $ t h i s - > gender, n \r\n"; cl a s s f a m i l y { p u b l i c $peop l e ; p u b l i c $ location; p u b l i c f u n c t i o n 一 c o n s t r u c t ( $p, $loc) { $ t h i s - > p e o p l e = $p; $ t h i s - > l o c a t i o n = $loc; $ s t u d e n t = n e w p e r s o n (); $ s t u d e n t - > n a m e = ' T o m * ; $ s t u d e n t - > g e n d e r ≫ ' m a l e '; $ s t u d e n t - > s a y (); $ t o m = n e w f a m i l y ( $stu d e n t , ' p e k i n g ' ); e c h o s e r i a l i z e ( $ s t u d e n t ) ; $ s t u d e n t _ a r r = a r r a y ('n a m e ' ■> “ T o m、'gender* = > * m a l e ' ); e c h o "\n"; e c h o s e r i a l i z e ( $ s t u d e n t _ a r r ) ; prin t_r ( $tom); e c h o n \n"; e c h o s e r i a l i z e ( $ t o m ) ; 输出结果如下: T o m is m a l e 0 : 6 : " p e r s o n " : 2 : { s : 4 :" n a m e " ; s : 3 :" T o m " ;s :6 : " g e n d e r " ; s : 4 :" m a l e " ; } a : 2 :{ s : 4 :" n a m e " ;s : 3 : " T o m " ; s : 6 : " g e n d e r " ; s : 4 :" m a l e " ; } f a m i l y O b j e c t ( [ p e o p l e ] = > p e r s o n O b j e c t ( [ name] - > T o m [g e n d e r ] = > m a l e ) [ l o c a t i o n ] = > p e k i n g 0:6: " f a m i l y” :2 : {s : 6: ”p e o p l e‛; 0: 6: p e r s o n : 2 : { s : 4 : n a m e "; s : 3: T o m ; s : 6 : " g e n d e r ; s : 4 : " m a l e " ; } s : 8 :" l o c a t i o n " ; s : 6 :" p e k i n g " ; } 第1 章面向对象思想的核心概念♦ > 7 可以看出,序列化后的对象会附带所属的类名,这个类名保证此对象能够在执行类的方法 (也是自己所能执行的方法)时,可以正确地找到方法所在的代码空间(即对象所拥有的方法 存储在类里)。另外,当一个对象的实例变量引用其他对象时,序列化该对象时也会对引用对 象进行序列化。 基于如上的分析,可以总结出对象和类的概念以及二者之间的关系: □ 类是定义一系列属性和操作的模板,而对象则把属性进行具体化,然后交给类处理。 □ 对象就是数据,对象本身不包含方法。但是对象有一个“指针” 指向一个类,这个类里 可以有方法。 □ 方法描述不同属性所导致的不同表现。 □ 类和对象是不可分割的,有对象就必定有一个类和其对应,否则这个对象也就成了没有 亲人的孩子(但有一个特殊情况存在,就是由标量进行强制类型转换的object, 没有一 个类和它对应。此时,PHP中一个称为“孤儿‛的stdClass类就会收留这个对象)。 理解了以上四个概念,结合现实世界从实现和存储理解对象和类,这样就不会把二者看成 一个抽象、神秘的东西,也就能写出符合现实世界的类了。 如果需要一个类,要从客观世界抽象出一套规律,就得总结这类事物的共性,并且让它可 以与其他类进行区分。而这个区分的依据就是属性和方法。区分的办法就是实例化出一个对 象,是骡子是马,拉出来遛遛。 现在,你是否对“白马非马” 这个典故有了新的认识? 1 . 2 魔术方法的应用 魔术方法是以两个下画线开头、具有特殊作用的一些方法,可以看做PHP的‚语 法糖‛。 & 语法糖指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜" 的语法。语 法糖往往给程序员提供了更实用的编码方式或者一些技巧性的用法,有益于更好的编码风 格,使代码更易读。不过其并没有给语言添加什么新东西。PHP里的引用、SPL等都属于 语法糖。 实际上,在1. 1节代码中就涉及魔术方法的使用。family类中的_ construct方法就是一个 标准魔术方法。这个魔术方法又称构造方法。具有构造方法的类会在每次创建对象时先调用此 方法,所以非常适合在使用对象之前做一些初始化工作。因此,这个方法往往用于类进行初始 化时执行一些初始化操作,如给属性賦值、连接数据库等。 以代码淸单1-1所示代码为例,family中的— construct方法主要做的事情就是在创建对象的 同时对诚性賦值。也可以这么使用: $ t o m ≪ n e w f a m i l y ( $stu d e n t , ' p e k i n g ' ); $ t o m - > p e o p l e - > s a y (); 8 ♦ > PHP核心技术与最佳实践 这样做就不需要在创建对象后再去賦值了。有构造方法就有对应的析构方法,即— destruct 方法,析构方法会在某个对象的所有引用都被删除,或者当对象被显式_______销毁时执行。这两个方 法是常见也是最有用的魔术方法。 1.2.1 — set 和—get 方法 _ s e t和_ 8过是两个比较重要的魔术方法,如代码清单1-2所示。 _______________________________ 代码清单 1-2 magic, php_______________________________ < ? p h p c l a s s A c c o u n t { p r i v a t e $ u s e r * 1 ; p r i v a t e $ p w d = 2; } $a - n e w A c c o u n t (); e c h o $a - > user; $a - > n a m e = 5 ; e c h o $a - > n a m e ; e c h o $a - > b i g ; 运行这段代码会怎样呢?结果报错如下: F a t a l err o r : C a n n o t a c c e s s p r i v a t e p r o p e r t y A c c o u n t :: $ u s e r in G : \ b a k \ t e m p \ t e m p c o d e \ _ s g . p h p o n lin e 7 所报错误大致是说,不能访问Account对象的私有属性user。在代码清单1-2的类定义里增 加以下代码,其中使用了_ s e t魔术方法。 p u b l i c f u n c t i o n 一 s et ( $name, $value) { e c h o " S e t t i n g $narne to $ v a l u e \r\n"; $ t h i s - > $ n a m e = $ valu e ; ) p u b l i c f u n c t i o n _ g e t < $ n a m e ) { i f (! iss e t ( $ t h i s - > $ n a m e ) ) { e c h o •未设置•; $ t h i s - > $ n a m e = " 正在为你设直默认值‛; ) r e t u r n $ t h i s - > $name; ) 再次运行,看到正常输出,没有报错。在类里以两个下画线开头的方法都属于魔术方法 (除非是你自定义的),它们是PHP中的内置方法,有特殊含义。手册里把这两个方法归到 重载。 PHP的重载和Java等语言的重载不同。Java里,重载指一个类中可以定义参数列表不同但 名字相同的多个方法。比如,Java也有构造函数,Java允许有多个构造函数,只要保证方法签 名不一样就行;而PHP则在一个类中只允许有一个构造函数。 第1 章面向对象思想的核心概念♦ > 9 PH P提供的“重载” 指动态地“创建” 类属性和方法。因此,— set和— get方法坪归到 重载里。 这里可以直观看到,若类中定义了— set和— get这一对魔术方法,那么当给对象属性赋值 或者取值时,即使这个属性不存在,也不会报错,一定程度上增强了程序的健壮性。 我们注意到,在account类里,user属性的访问权限是私有的,私有属性意味着这个屈性是 类的“私有财产” ,只能在类内部对其进行操作。如果没有_ set这个魔术方法,直接在类的外 部对属性进行賦值操作是会报错的,只能通过在类中定义一个public的方法,然后在类外调用 这个公开的方法进行属性读写操作。 现在有了这两个魔术方法,是不是对私有W 性的操作变得更方便了呢?实际上,并没有什 么奇怪的,因为这两个方法本身就是public的。它们和在对外的public方法中操作private域性 的原理一样。只不过这对魔术方法使其操作更简单,不需要显式地调用一个p ublic的方法,因 为这对魔术方法在操作类变量时是自动调用的。当然,也可以把类属性定义成public的,这样 就可以随意在类的外部进行读写。不过,如果只是为了方便,类属性在任意时候都定义成public 权限& 然是不合适的,也不符合面向对象的设计思想。 1. 2. 2 — call 和—callStatic 方法 如何防止调用不存在的方法而出错? 一样的道理,使用— c a ll魔术重载方法。 _ c a ll方法原型如下: mixed — c a l l ( string $name , array $arguments ) 当调用一个不可访问的方法(如未定义,或者不可见)时,— c a l l ( )会被调用。其中 $ name参数是要调用的方法名称。Sarguments参数是一个数组,包含着要传递给方法的参数, 如下所示: public function 一 c a l l ($name, $arguments) { switch(count($arguments)){ case 2: echo $arguments[ 0 ] * $arguments[ 1 ], PHP一EOL; break; case 3: echo array_sum ($arguments) , PHP一EOL; break; d e fa u lt : echo •参數不对',?即_£01^; break; ) ) $a ->make (5); $a ->make (5,6); 以上代码模拟了类似其他语言中的根据参数类型进行重载。跟— c a ll方法配套的魔术方法 是— callStatic。当然,使用魔术方法“防止调用不存在的方法而报错‛,并不是魔术方法的本 意。实际上,魔术方法使方法的动态创建变为可能,这在MVC等框架设计中是很有用的语法。 假设一个控制器调用了不存在的方法,那么只要定义7 _ c a l丨魔术方法,就能友好地处理这种 情况。 试着理解代码清单1-3所示代码。这段代码通过使用— callStatic这一魔术方法进行方法的 动态创建和延迟绑定,实现一个简单的ORM模型。 代码清单 1-3 simpleOrm.php < ? p h p a b s t r a c t c l a s s A c t i v e R e c o r d { p r o t e c t e d s t a t i c $ table; p r o t e c t e d $ f i e l d v a l u e s ; p u b l i c $select; s t a t i c f u n c t i o n f i n d B y l d ($id) { $ query = "select * from " . s t a t i c : : $ t a b l e • " w h e r e i d * $id"; return self: :createDomain ($query); ) f u n c t i o n 一 g e t ($£ieldname) { r e t u r n $th i s - > f i e l d v a l u e s [ $ f i e l d n a m e ] ; } s t a t i c f u n c t i o n 一 c a l l S t a t i c ($method, $args) { $ f i e l d ■ p r e g一r e p l a c e (•/A f i n d B y ( W * >$/•, '$ { 1 }', $ m e t h o d ) ; $ q u e r y = " s e l e c t * f r o m " .static::$table w h e r e $ f i e l d = '$ arg s [ 0 ] return self:: createDomain ($query>; 10 ♦ > PHP核心技术与最佳实践 p r i v a t e s t a t i c f u n c t i o n c r e a t e D o m a i n ($query) { $ k l a s s = g e t _ c a l l e d一 c l a s s (); $ d o m a i n - n e w $ k l a s s (); $ d o m a i n - > f i e l d v a l u e s = a r r a y (); $ d o m a i n - > s e l e c t = $query; f o r e a c h ( $ k l a s s ::$fi e l d s as $ f i e l d - > $type) { $ d o m a i n - > f i e l d v a l u e s [ $field] = 'TODO: s et f r o m sql result*; ) r e t u r n $domain; c l a s s C u s t o m e r e x t e n d s A c t i v e R e c o r d { p r o t e c t e d s t a t i c $ t a b l e = ' c u s t d b ' ; p r o t e c t e d s t a t i c $ f i e l d s ■ a r r a y ( 第1 章面向对象思想的核心概念11 'id' - > -int., ' e m a i l 1 * > 'v a r c h a r 1, 'l a s t n a m e ' = > 'varchar' c l a s s S a l e s e x t e n d s A c t i v e R e c o r d { p r o t e c t e d s t a t i c S t a b l e ■ ' s a l e s d b ' ; p r o t e c t e d s t a t i c $ f i e l d s - a r r a y ( 'id' ■> ' in t *, 'item* ■> ‟varch a r * , iqtyi = > 'int* a s s e r t ( " s e l e c t * f r o m c u s t d b w h e r e i d - 1 2 3 " ■■ C u s t o m e r ::f i n d B y l d (123) - > s e l e c t ) ; a s s e r t ("TODO: s e t f r o m sql r e s u l t " = = C u s t o m e r ::f i n d B y l d (123) - > e m a i l ) ; a s s e r t ("select * f r o m s a l e s d b w h e r e i d ≪ 3 2 1 n ■■ S a l e s ::f i n d B y l d (321) - > s e l e c t ) ; a s s e r t ("select * f r o m c u s t d b w h e r e L a s t n a m e = 'D e n o n c o u r t ' " = = C u s t o m e r ::f i n d B y L a s t n a m e (•D e n o n c o u r t •) - > s e l e c t ) ; 再举个类似的例子。PHP里有很多字符串函数,假如要先过滤字符串首尾的空格,再求出 字符串的长度,一般会这么写: s t r l e n ( t r i m ( $ s t r ) ) ; 如果要实现j s 里的链式操作,比如像下面这样,应该怎么实现? $ s t r - > trim() - > s t r l e n () 很简单,先实现一个String类,对这个类的对象调用方法进行处理时,触发_ c a l l魔术方 法,接着执行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是可以定制的, 所提供的信息和样式更丰 12 < ♦ PHP核心技术与最佳实践 富,如代码淸单1-4所示。 ______________________________ 代码清单 1 -4 magic_2. php________ < ? p h p c l a s s A c c o u n t { p u b l i c $ u s e r * 1 ; p r i v a t e $ p w d - 2; / / 自定义的格式化输出方法 public function _ toString () { r e t u r n ““当前对象的用户名是{$this - > u s e r },密码是{$this - > p w d }"; ) } $a = n e w A c c o u n t (); e c h o $a; e c h o PHP_EOL; p r i n t一r ( $ a ) ; 运行这段代码发现,使用— toString方法后,输出的结果是可定制的,更易于理解。实际 上,PHP的— toString魔术方法的设计原型来源于Java。Java中也有这么一个方法,而且在Java 中,这个方法被大量使用,对于调试程序比较方便。实际上,— toString方法也是一种序列化, 我们知道PHP自带serialize/unserialize也是进行序列化的,但是这组函数序列化时会产生一些 无用信息,如属性字符串长度,造成存储空间的无谓浪费。因此,可以实现自己的序列化和反 序列化方法,或者json_encode/json_decode也是一^Is“不错的选择 为什么直接echo —个对象就会报语法错误,而如果这个对象实现— toString方法后就可以 直接输出呢?原因很简单,echo本来可以打印一个对象,而且也实现了这个接口,但是PHP对 其做了个限制,只有实现— toString后才允许使用。从下面的PHP源代码里可以得到验证: Z E N D _ V M _ H A N D L E R (40, E END_ECH0, C O N S T ITMP IVAR |CV, ANY) { z e n d _ o p *op l i n e = EX ( o p l i n e ) ; z e n d一f r e e _ o p f r e e _ o p l ; zval z一copy; zv a l *z - G E T _ 0 P 1 _ Z V A L _ P T R ( B P _ V A R _ R ) ; I I此处的代码蕷留了& 对象转換为字一符串^接口 i f (0P1-TYPE ! - I S _ C 0 N S T && Z _ TYPE_P(z) IS一O B J E C T && Z _ 0 B J _ H T _ P (z ) - > g e t m e t h o d ! = N U L L && z e n d _ s t d _ c a s t _ o b j e c t _ t o s t r i n g (z, &z_copy, I S j S T R I N G T SRMLS_CC) = = SUCCESS) { z e n d _ p r i n t _ v a r i a b l e ( & z _ c o p y ) ; z v a l一d t o r ( & z _ c o p y ) ; } e l s e { z e n d _ p r i n t一v a r i a b l e (z); ) F R E E _ 0 P 1 (); Z E N D V M N E X T O P C O D E (); 第1 章面向对象思想的核心概念13 由此可见,魔术方法并不神奇。 有比较才有认知。最后,针对本节代码给出一个Java版本的代码,供各位读者用来对比两 种语言中重载和魔术方法的异同,如代码清单1-5所示。 代码清单 1-5 Accountjava in^jort o r g .a p a c h e .c o m m o n s .I a n g 3 . b u i l d e r . T o S t r i n g B u i l d e r ; /** * 类的重我演示J a v a版本 * @ a u t h o r w f o x * Q d a t e @ v e r s o n V p u b l i c c l a s s A c c o u n t { p r i v a t e S t r i n g user;// 用户名 p r i v a t e S t r i n g pwd; // 密码 public Account () { S y s t e m . o u t .prin t l n (••构造函数; } p u b l i c A c c o u n t (String user. S t r i n g pwd) { S y s t e m .o u t .p r i n t l n {••重我构造函數; S y s t e m . o u t . p r i n t In (user + " ----" + p w d ) ; ) p u b l i c v o i d s a y (String user) { S y s t e m . o u t • p r i n t l n 用户是:" + u s e r >; ) p u b l i c v o i d s a y (String user. S t r i n g pwd) { S y s t e m • o u t • p r i n t l n ("用户:” + u s e r ); S y s t e m .o u t .p r i n t l n (••密码" + p w d ) ; p u b l i c ) p u b l i c } p u b l i c : S t r i n g g e t U s e r () { r e t u r n user; v o i d s e t U s e r (String user) t h i s . u s e r ■ user; :v o i d s e t P w d (String pwd) :S t r i n g g e t P w d () r e t u r n pwd; ) p u b l i c, } @ O v e r r i d e p u b l i c S t r i n g t o S t r i n g () { r e t u r n T o S t r i n g B u i l d e r .r e f l e c t i o n T o S t r i n g ( t h i s ) ; ) p u b l i c s t a t i c v o i d m a i n (String.") { 14 ♦ > PHP核心技术与最佳实践 A c c o u n t a c c o u n t = n e w A c c o u n t (); a c c o u n t .set U s e r 张三"); a c c o u n t . s e t P w d ("123456"); a c c o u n t . s a y < 李四" >; a c c o u n t . s a y <••王五","123"); S y s t e m . o u t . p r i n t l n ( a c c o u n t ) ; 可以看出,Java的构造方法比PHP好用,PHP由于有了— set/— get这一对魔术方法, 使得动态增加对象的属性字段变得很方便,而对Java来说,要实现类似的效果,就不得不 借助反射API或直接修改编译后字节码的方式来实现。这体现了动态语言的优势,简单、 灵活。 1 . 3 继承与多态 面向对象的优势在于类的复用。继承与多态都是对类进行复用,它们一个是类级别的复 用,一个是方法级别的复用。提到继承必提组合,二者有何异同? PHP到底有没有多态?若没 有,则为什么没有?有的话,和其他语言中的多态又有什么区别?这些都是本节所要讲述的 内容。 1.3.1 类的组合与继承 在1. 1节的代码中定义了两个类,一个是person, —个是family;在family类中创建person 类中的对象,把这个对象视为family类的一个屈性,并调用它的方法处理问题,这种复用方式 叫“组合”。还有一种复用方式,就是继承。 类与类之间有一种父与子的关系,子类继承父类的属性和方法,称为继承。在继承里,子 类拥有父类的方法和属性,同时子类也可以有自己的方法和属性。 运行上述代码,输出如图1-2所示。 构造函数 用户是 用户: 密码123 popular. Account@530daa [use「=张二,pwd=123456] 图1-2 Java里的构造方法和重载演示 第1 章面向对象思想的核心概念< ♦ 15 可以把1.1节的组合用继承实现,如代码淸单1-6所示。 代码清单 1 -6 family_extends. php < ? p h p c l a s s p e r s o n { p u b l i c $ n a m e = ' T o m ' ; p u b l i c $gender; s t a t i c $ m o n e y = 10000; p u b l i c f u n c t i o n _ c o n s t r u c t () { e c h o •这里是父类^ P H P J ^ O L ; ) p u b l i c f u n c t i o n s a y () { e c h o $ t h i s - > name, " \tis ", $ t h i s - > gend e r , "\r\n"; ) ) c l a s s f a m i l y e x t e n d s p e r s o n { p u b l i c $name; p u b l i c $gender; p u b l i c $age; s t a t i c $ m o n e y - 1 0 0 0 0 0 ; p u b l i c f u n c t i o n 一 c o n s t r u c t () { p a r e n t : : _ c o n s t r u c t (); / /调用父类构造方法 e c h o •这里是子类',PHP_EOL; ) p u b l i c f u n c t i o n s a y () { p a r e n t : : s a y (); echo $this -> name, " \tis\t", $this -> gender, ", and i s \ t " , $ t h i s - > a g e /PHP_EOL; ) p u b l i c f u n c t i o n c r y () { e c h o p a r e n t ::$ m o n e y , P H P _ E O L ; e c h o '% > _ < % * , PHP_E0L; e c h o s e l f ::$money, PHP_EOL; // 调用自身构造方法 e c h o ' •; $ p o o r = n e w f a m i l y (); $ p o o r - > n a m e ≫ ' L e e '; $ p o o r - > g e n d e r = ' f e m a l e “ / $ p o o r - > a g e = 2 5 ; $ p o o r - > s a y (); $ p o o r - > c r y <); 运行上面的代码,可以得到如下输出结果: 这里是父类 这里是子类 L e e is fema l e 16 < ♦ PHP核心技术与最佳实践 L ee is f e m a l e , a n d is 25 10000 % >_<% 100000 (*A 一 A” 从上面代码中可以了解继承的实现。在继承中,用parent指代父类,用self指代自身。使 用“ ::” 运算符(范围解析操作符)调用父类的方法。‚::‛操作符还用来作为类常量和静态 方法的调用,不要把这两种应用混淆。 既然提到静态,就再强调一点,如果声明类成员或方法为static,就可以不实例化类而直接 访问,同时也就不能通过一个对象访问其中的静态成员(静态方法除外),也不能用‚::‛访 问一个非静态方法。比如,把上例中的$poor-> cry();换成$poor: :c iy ( ) , 按照这个规则,应 该是要报错的。可能试验时,并没有报错,而且能够正确输出。这是因为用‚::‛方式调用一 个非静态方法会导致一个E_STRICT级别的错误,而这里的PHP设置默认没有开启这个级别的 报错提示。打开PHP安装目录下的PhP.ini文件,设置如下: e r r o r一r e p o r t i n g = E _ A L L 丨 E 一S T R I C T d i s p l a y _ e r r o r s - On 再次运行,就会看到错误提示。因此,用“ ::” 访问一个非静态方法不符合语法,但PHP 仍然能够TH确地执行代码,这只是PHP所做的一个“兼容” 或者说“让步‛。在开发时,设置 最严格的报错等级,在部署时可适当调低。 组合与继承都是提髙代码可重用性的手段。在设计对象模型时,可以按照语义识别类之间 的组合关系和继承关系。比如,通过一些总结,得出了继承是一种“是、像‛ 的关系,而组合 是一种“需要‟‟ 的关系。利用这条规律,就可以很简单地判断出父亲与儿子应该是继承关系, 父亲与家庭应该是组合关系。还可以从另外一个角度看,组合偏重整体与局部的关系, 而继承 偏重父与子的关系,如图1-3所示。 图1 - 3继承和组合的对照 从方法复用角度考虑,如果两个类具有很多相同的代码和方法,可以从这两个类中抽象出 一个父类,提供公共方法,然后两个类作为子类,提供个性方法。这时用继承语意更好。继承 的UML图如图1-4所示。 第1章面向对象思想的核心概念♦ > 17 ^methodl<) #m6tKXl2() ♦melhod3() ^melhodA<) ≪fme PHP核心技术与最佳实践 4 ) 不恰当地使用继承可能违反现实世界中的逻辑。 比如,人作为父类,雇员、经理、学生作为子类,可能存在这样的问题,经理一定是雇 员,学生也可能是雇员,而使用继承的话一个人就无法拥有多个角色。这种问题归结起来就是 “角色” 和“权限” 问题。在权限系统中很可能存在这样的问题,经理权利和职位大于主管, 但出于分工和安全的考虑,经理没有权限直接操作主管所负责的资源,技术部经理也没权限直 接命令市场部主管。这就要求角色和权限系统的设计要更灵活。不恰当的继承可能导致逻辑混 乱,而使用组合就可以较好地解决这个问题。 当然,组合并非没冇缺点。在创建组合对象时,组合需要一一创建局部对象,这一定程度 上增加了一些代码,而继承则不需要这一步,因为子类自动有了父类的方法,如代码清单1-7 所示。 代码清单 1-7 mobile, php < ? p h p / /继承拥有比组合更少的代码量 c l a s s c a r { p u b l i c f u n c t i o n a d d o i l () { e c h o " A d d oil\r\n"; } } c l a s s b m w e x t e n d s c a r { } c l a s s benz{ p u b l i c $car; p u b l i c f u n c t i o n 一 c o n s t r u c t () { $t h i s - > c a r = n e w car; ) p u b l i c f u n c t i o n a d d o i l () { $t h i s - > c a r - > a d d o i l (); } $ b m w = n e w b m w ; $ b m w - > a d d o i l (); $ b e n z = n e w b e n z (); $ b e n z - > a d d o i l (); 显然,组合比继承增加了代码量。组合还有其他的一些缺点,不过总体说来,是优点大于 缺点。 继承最大的优点就是扩展简单,但是其缺点大于优点, 所以在设计时, 需要慎重考虑。那 应该如何使用继承呢? □ 精心设计专门用于被继承的类,继承树的抽象层应该比较稳定,一般不要多于三层。 □ 对于不是专门用于被继承的类,禁止其被继承,也就是使用fmal修饰符。使用final修 饰符既可防止重要方法被非法覆写,又能给编辑器寻找优化的机会。 第1 章面向对象思想的核心概念19 □ 优先考虑用组合关系提高代码的可重用性。 □ 子类是一种特殊的类型,而不只是父类的一个角色。 □ 子类扩展,而不是覆盖或者使父类的功能失效。 □ 底层代码多用组合,顶层/业务层代码多用途承。底层用组合可以提高效率,避免对象 臃肿。顶层代码用继承可以提高灵活性,让业务使用更方便。 思考题设计一个丨og类,需要用到MySQL中的CURD操作,是应该使用继承呢还是组合?请 给出理由。 继承并非一无是处,而组合也不是完美无缺的。如果既要组合的灵活,又要继承的代码简 洁,能做到吗? 这是可以做到的,臂如多重继承,就具有这个特性。多重继承里一个类可以同时继承多个 父类,组合两个父类的功能。C++里就是使用的这种模型来增强继承的灵活性的,但是多重继 承过于灵活,并且会带来“菱形问题” ,故为其使用带来了不少困难,模型变得复杂起来,因 此在大多数语言中,都放弃了多重继承这一模型。 多重继承太复杂,那么还有其他方式能比较好地解决这个问题吗? PHP5.4引人的新的语 法结构Traits就是一种很好的解决方案。Traits的思想来源于C++和Ruby里的Mixin以及Scala 里的Traits,可以方便我们实现对象的扩展,是除extend、implements外的另外一种扩展对象的 方式。Traits既可以使单继承模式的语言获得多重继承的灵活,又可以避免多重继承带来的种 种问题。 1 . 3 . 2 各种语言中的多态 多态确切的含义是:同一类的对象收到相同消息时,会得到不同的结果。而这个消息是不 可预测的。多态,顾名思义,就是多种状态,也就是多种结果。 以Java为例,由于Java是强类型语言,因此变量和函数返回值是有状态的。比如,实现一 个add函数的功能,其参数可能是两个in t型整数,也可能是两个float型浮点数,而返回值可 能是整型或者浮点型。在这种情况下,add函数是有状态的,它有多种可能的运行结果。在实 际使用时,编译器会自动匹配适合的那个函数。这属于函数重载的概念。需要说明的是,重载 并不是面向对象里的东西,和多态也不是一个概念,它属于多态的一种表现形式。 多态性是一种通过多种状态或阶段描述相同对象的编程方式。它的真正意义在于:实际开 发中,只要关心一个接口或基类的编程,而不必关心一个对象所属于的具体类。 很多地方会看到“PHP没有多态” 这种说法。事实上,不是它没有,而是它本来就是多态 的。PHP作为一门脚本语言,自身就是多态的,所以在语言这个级别上,不谈PHP的多态。在 PHP官方手册也找不到对多态的详细描述。 既然说PHP没有多态这个概念(实际上是不需要多态这个概念),那为什么又要讲多态呢? 可以看下面的例子,如代码清单1-8所示。 20 < * PHP核心技术与最佳实践 代码清单 1 -8 Polymorphism, php < ? p h p c l a s s e m p l o y e e { p r o t e c t e d f u n c t i o n w o r k i n g () { e c h o •本方法需重栽才能运行“; } c l a s s t e a c h e r e x t e n d s e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g () { e c h o “教书 *; c l a s s c o d e r e x t e n d s e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g () { e c h o •敲代码1; f u n c t i o n d o p r i n t ($o b j ) { i f ( g e t j c l a s s ( $ o b j ) = = 'e m p l o y e e '){ e c h o 'E r r o r '; }else{ $obj - > w o r k i n g (); } } d o p r i n t (new t e a c h e r ()); d o p r i n t (new c o d e r ()); d o p r i n t (new e m p l o y e e ()); 通过判断传人的对象所属的类不同来调用其同名方法,得出不同结果,这是多态吗?如果 站在C++角度,这不是多态,这只是不同类对象的不同表现而已。C+ + 里的多态指运行时对象 的具体化,指__________同一类对象调用相同的方法而返冋不同的结果。看个C++的例子,如代码淸单1-9 所示。 代码清单1-9 C++多态的例子 ♦ i n c l u d e < c s t d l i b > t i n c l u d c < i o o t r c a m > /★* C + + 中用虚函数实现多态 * / u s i n g n a m e s p a c e std; c l a s s f a t h e r { publ i c : father () :age (30) {cout << "父类构造法,年龄•• 〜f a t h e r () {cout < < ••父类析构•• < < " \ n n ; } v o i d e a t () {cout < < "父类吃饭吃三斤" < < "\n"; v i r t u a l v o i d ru n () {cout < < "父类跑 100 0 0 米' < < a g e < < " \ n " ; } 第1 章面向对象思想的核心概念21 p r o t e c t e d : int age; Jrc l a s s s o n : p u b l i c f a t h e r { p u b l i c : son() {cout ≪ "子类构造法" < < ”\ n " ; } - s o n () {cout < < "子类析构" < < "\ n”;} v o i d e a t () {cout < < "儿子吃饭吃一斤" < < " \ n‛; v o i d ru n (> {cout < < "儿子砲 100 米" ≪ w \n"; } v o i d c r y <> {cout < < "哭泣" < < " \ n " ; ) in t m a i n (int argc, c h a r * a r g v [ ]) f a t h e r *pf ■ n e w son; p f - > e a t (); p f - > ru n (); d e l e t e p f ; s y s t e m ( " P A U S E " ) ; r e t u r n E X I T一SUCCESS; ) 上面的代码首先定义一个父类,然后定义一个子类,这个子类继承父类的一些方法并且有 自己的方法。通过father * pf = new son;语句创建一个派生类(子类)对象,并且把该派生类对 象陚给基类(父类)指针,然后用该指针访问父类中的eat和run方法。图1-6所示是运行 结果。 由于父类中的r u n方法加了 virtual关键字,表示该函数 有多种形态,可能被多个对象所拥有。也就是说,多个对象 在调用同一名字的函数时会产生不同的效果。 这个例子和PHP的例子有什么不同呢? C + + 的这个例 子所创建的对象是一个指向父类的子对象,还可以创建更多 派生类对象,然后上转型为父类对象。这些对象,都是同一 A 图1-6 行结果 类对象,但是在运行时,却都能调用到派生类同名函数。而 PHP中的例子则是不同类的对象调用。 由于PHP是弱类型的,并且也没有对象转型机制,所以不能像C + + 或者Java那样实现 father ≪pf = new son;把派生类对象陚给基类对象,然后在调用函数时动态改变其指向。在 PHP的例子中,对象都是确定的,是不同类的对象。所以‟ 从这个角度讲,这还不是真正的 多态。 代码淸单1-8所示代码通过判断对象的类域性实现“多态” ,此外,还可以通过接丨:!实现 多态,如代码淸单1-10所示。 f 斤後 - - 三长继 i 吃f 键 f s H K按 >:h k x )Ly 、li ___________________________代码清单1 - 1 0 通过接口实现多态 < ? p h p i n t e r f a c e e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g (); ) c l a s s t e a c h e r i m p l e m e n t s e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g () { e c h o •教书•; 22 ♦ > PHP核心技术与最佳实践 c l a s s c o d e r i m p l e m e n t s e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g () { e c h o •敲代码•; ) ) f u n c t i o n d o p r i n t ( employee $i) { $i $i - > w o r k i n g (); $a ≪ n e w teacher; $b ■ n e w coder; d o p r i n t ( $ a ) ; d o p r i n t ( $ b ) ; 这是多态吗?这段代码和代码淸单1-8相比没有多少区别,不过这段代码中doprim函数的 参数是一个接口类型的变量,符合“同一类型,不同结果” 这一条件,具有多态性的一般特 征。因此,这是多态。 如果把代码淸单1-8中doprim函数的obj参数看做一种类型(把所有弱类型看做一种类 型),那就可以认为代码清单1-8中的代码也是一种多态。 再次把三段代码放在一起品味,可以看出:区别是否是多态的关键在于看对象是否厲于同 —类型。如果把它们看做同一种类型,调用相同的函数,返回了不同的结果,那么它就是多 态;否则,不能称其为多态。由此可见,弱类型的PHP里多态和传统强类型语言里的多态在实 现和概念上是有一些区别的,而且弱类型语言实现起多态来会更简单,更灵活。 本节解决了什么是多态,什么不是多态的问题。至于多态是怎么实现的,各种语言的策略 是不一样的。但是,最终的实现无非就是查表和判断。总结如下: □ 多态指同一类对象在运行时的具体化。 □ PHP语言是弱类型的,实现多态更简单、更灵活。 □ 类型转换不是多态。 □ PHP中父类和子类看做“继父” 和“继子” 关系,它们存在继承关系,但不存在血缘 关系。因此子类无法向上转型为父类,从而失去多态最典型的特征。 □ 多态的本质就是if- e ls e ,只不过实现的层级不同。 第1 章面向对象思想的核心概念♦ > 23 1 . 4 面向接口编程 这里,首先强调一个概念,面向接口编程并不是一种新的编程范式。本章开头提到的三大 范式中并没有提到面向接口。其次,这里是狭义的接口,即interface关键字。广义的接口可以 是任何一个对外提供服务的出口,比如提供数据传输的USB接口、淘宝网对其他网站开放的支 付宝接口。 1 . 4 . 1 接□的作用 接口定义一套规范,描述一个“物” 的功能,要求如果现实中的“物” 想成为可用,就必 须实现这些基本功能。接口这样描述自己: "对于实现我的所有类,看起来都应该像我现在这个样子‛。 采用一个特定接口的所有代码都知道对于那个接口会调用什么方法。这便是接口的全部含 义。接口常用来作为类与类之间的一个“协议”。接口是抽象类的变体,接口中所有方法都是 抽象的,没有一个有程序体。接口除了可以包含方法外,还能包含常量。 比如用接口描述发动机,要求机动车必须要有“ run” 功能,至于怎么实现(摩托还是宝 马),应该是什么样(前驱还是后驱),不是接口关心的。因为接口为抽象而生。作为质检总 局,要判断这辆车是否合格,只要按“接口” 的定义一条一条验证,这辆车不能“ run‛,那它 就是废品,不能通过验收。但是,如果汽车实现了接口中本来不存在的方法music, 并不认为 有什么问题。接口就是一种契约。因此,在程序里,接口的方法必须被全部实现,否则将报 fetal错误,如代码清单1 -11所示。 代码清单 1-11 interface, php < ? p h p i n t e r f a c e m o b i l e p u b l i c f u n c t i o n r u n (); // 联动方法 ) c l a s s p l a i n i m p l e m e n t s m o b i l e p u b l i c f u n c t i o n r u n () { e c h o "我是飞机‛; ) p u b l i c f u n c t i o n f l y () { e c h o ”飞行"; } } class car i m p l e m e n t s m o b i l e { 24 ♦ > PHP核心技术与最佳实践 p u b l i c f u n c t i o n r un () { e c h o "我是汽车\r\n"; } ) cl a s s m a c h i n e { f u n c t i o n d e m o (mobile $a) { $a - > f l y ( ) ; II m o b i l e接口是没有这个方法的 } ) $obj = n e w m a c h i n e (); $obj - > d e m o (new p l a i n ()); // 运行成功 $obj - > d e m o (new c a r ()); // 运行失敗 在这段代码里,定义一个机动车接口,其中含有一个发动机功能。然后用一个飞机类实现 这个接口,并增加了飞行方法。最后,在一个机械检测类中对机动车进行测试(用类型约束指 定要测试的是机动车这个接口)。但是,此检测线测试的却是机动车接口中不存在的f ly方法, 直到遇到car的实例因不存在fly方法而报错。 这段代码实际上是错误的,不符合接口语义。但是在PHP里,对plain的实例进行检测时 是可以运行的。也就是说,在PHP里,只关心是否实现这个方法,而并不关心接口语义是否 正确。 按理说,接口应该是起一个强制规范和契约的作用,但是这里对接口的约束并没有起效, 也打破了契约,对检测站这个类的行为失去控制。可以看看在Java里是怎么处理的,如图1-7 所示。 炉 5 6 7 8 9 1妒 11 12 public class Machine { public void show(mobile mobile){ > 、The method ftyO is undefined for the type mobile 2 quick fixes available: 《„ Add t i l ta mobilt public st< Mdchinif mdehine=new Mdchine(); machine.shoM(new Plain()); 图丨-7 Java中接口是一种类型 Java认为,接口就是一种ty p e ,即类型。如果你打破了我们之间的契约,你的行为变得无 法控制,那就是非法的。这符合逻辑,也符合现实世界。这就真正起到接口作为规范的作 用了。 接口不仅规范接口的实现者,还规范接口的执行者,不允许调用接口中本不存在的方法。 当然这并不是说一个类如果实现了接口,就只能实现接口中才有的方法,而是说,如果针对的 第1 章面向对象思想的核心概念♦ > 25 是接口,而不是具体的类,则只能按接口的约定办事。这样的语法规定对接口的使用是有利 的,让程序更健壮。根据这个角度讲,为了保证接口的语义,通常一个接口的实现类仅实现该 接口所具有的方法,做到专一,当然这也不是一成不变的。 由上面的例子可以看出,PHP里接口作为规范和契约的作用打了折扣。上面例子实际就是 一个典型面向接口编程的例子。根据这个例子,可以很自然地想到使用接口的场合,比如数据 库操作、缓存实现等。不用关心我们所面对的数据库是MySQL还是Oracle, 只需要关心面向 Database接口进行具体业务的逻辑相关的代码,这就是面向接t l编程的来历。 在这里,Database就如同employee—样,针对这个接口实现就好了。缓存功能也一样,我 们不关注缓存是内存缓存还是文件缓存,或者是数据库缓存,只关注它是否实现了 Cache接口, 并且它只要实现了 Cache接口,就实现了写人缓存和读取某条缓存中的数据及淸除缓存这几个 关键的功能点。 通常在大型项目里,会把代码进行分层和分工。核心开发人员和技术经理编写核心的流程 和代码,往往是以接口的形式给出,而基础开发人员则针对这些接口,填充代码,如数据库操 作等。这样,核心技术人员把更多精力投人到了技术攻关和业务逻辑中。前端针对接口编程, 只管在Action层调用Service,不管实现细节;而后端则要负责Dao和Service层接口实现。这 样,就实现了代码分工与合作。 1 . 4 . 2 对PHP接□的思考 p h p的接口自始至终一直在被争议,有人说接口很好,有人说接口像鸡肋。首先要明白, 好和不好的判断标准是什么。无疑,这是和Java/C++相比。在上面的例子中,已经讨论了 PHP 的接口在“面向契约编程” 中是不足的,并没有起到应有的作用。 其实,在上面的interface.php代码中,machine类的声明应该在plain类前面。接口提供了一 套规范,这是系统提供的,然后machine类提供一组针对接口的API并实现,最后才是自定义 的类。在Java里,接口之所以盛行(多线程的runable接口、容器的collection接口等)就是因 为系统为我们做了前面两部分的工作,而程序员,只需要去写具体的实现类,就能保证接口可 用可控。 为什么要用接口?接口到底有什么好处?接口本身并不提供实现,只是提供一个规范。如 果我们知道一个类实现了某个接口,那么就知道了可以调用该接口的哪些方法,我们只需要知 道这些就够了。 PHP中,接口的语义是有限的,使用接口的地方并不多,PHP中接口可以淡化为设计文 档,起到一个团队基本契约的作用,代码淸单1-12所示。 代码清单 1-12 cachejmp.php < ?php interface cache { " * @ d e s c r i b e :缓存管理,項目经理定义接口,技术人员负责实现 26 ♦ > PHP核心技术与最佳实践 c o n s t m a x K e y = 10000; // 最大緩存量 p u b l i c f u n c t i o n g e t c ( $ k e y ) ; // 获取缓存 p u b l i c f u n c t i o n s e t c ($key, $ v a l u e >; // 设里缓存 p u b l i c f u n c t i o n f l u s h (); / /清空缓存 由于PHP是弱类型,且强调灵活,所以并不推荐大规模使用接口,而是仅在部分“ 内核” 代码中使用接口,因为PHP中的接口已经失去很多接口应该具有的语义。从语义上考虑,可以 更多地使用抽象类。至于抽象类和接口的比较,不再赘述。 另外,PHP5对面向对象的特性做了许多增强,其中就有一个SPL (标准PHP库)的尝试。 SPL中实现一些接口,其中最主要的就是Iterator迭代器接口,通过实现这个接口,就能使对 象能够用于foreach结构,从而在使用形式上比较统一。比如SPL中有一个Directorylteratoi•类, 这个类在继承SplFilelnfo类的同时,实现Iterator、Traversable、Seekablelterator这三个接口,那 么这个类的实例可以获得父类SplFilelnfo的全部功能外,还能够实现Iterator接口所展示的那些 操作。 Iterator接口的原型如下: * current() T h i s m e t h o d r e t u r n s th e c u r r e n t index* s v a l u e . Y o u a r e s o l e l y r e s p o n s i b l e for t r a c k i n g w h a t th e c u r r e n t i n d e x is as the i n t e r f a c e d o e s not d o t h i s f o r you. * key() T h i s m e t h o d r e t u r n s t h e v a l u e o f th e c u r r e n t index* s k e y . F o r f o r e a c h l oo p s t hi s is e x t r e m e l y i m p o r t a n t so th a t th e key v a l u e c a n b e p o p u l a t e d . * n e x t () T h i s m e t h o d m o v e s th e i n t e r n a l i n d e x f o r w a r d on e entry. * r e w i n d () T h i s m e t h o d s h o u l d res e t th e i n t e r n a l i n d e x to th e f i r s t elem e n t . * v a l i d () T h i s m e t h o d s h o u l d r e t u r n t r u e o r f a l s e if t h e r e is a c u r r e n t e l e m e n t . It is c a l l e d a f t e r r e w i n d () o r n e x t (). 如果一个类声明了实现Iterator接口,就必须实现这五个方法,如果实现了这五个方法,那 么就可以很容易对这个类的实例进行迭代。这里,Directorylteratoi•类之所以拿来就能用,是 因为系统已经实现了 Iterator接口,所以可以像下面这样使用: < ? p h p $ d i r = n e w D i r e c t o r y l t e r a t o r (dirnaroe (一 F I L E 一)); f o r e a c h ($dir as $ file i n f o ) { i f ( ! $ f i l e i n f o - > i s D i r ( ) ) { e c h o $ f i l e i n f o - > g e t F i l e n a m e <), \ t $f i l e i n f o - > g e t S i z e (), PHP_EOL; 第1 章面向对象思想的核心概念♦ > 27 可以想象,如果不用Directorylterator类,而是自己实现,不但代码量增加了,而且循环 时候的风格也不统一了。如果自己写的类也实现了 Iterator接口,那么就可以像Iterator那样 工作。 为什么一个类只要实现了 Iteratm•迭代器,其对象就可以被用作foreach的对象呢?其实原 因很简单,在对PHP实例对象使用foreach语法时,会检査这个实例有没有实现Iterator接口, 如果实现了,就会通过内拧方法或使用实现类中的方法模拟foreach语句。这是不是和前面提到 的— toString方法的实现很像呢?事实上, — toString方法就是接口的一种变相实现。 接口就是这样,接口本身什么也不做,系统悄悄地在内部实现了接口的行为,所以只要实 现这个接口,就可以使用接口提供的方法。这就是接口 “即插即用” 思想。 我们都知道,接口是对多重继承的一种变相实现,而在讲继承时,我们提到了用来实现混 人(Mixin) 式的Traits,实际上,Traits可以被视为一种加强型的接口。 来看一段代码: < ? p h p t r a i t H e l l o { p u b l i c f u n c t i o n s a y H e l l o (> { e c h o 'Hello '; t r a i t W o r l d { p u b l i c f u n c t i o n s a y W o r l d () e c h o •W o r l d ,; c l a s s M y H e l l o W o r l d { u s e Hello, World; p u b l i c f u n c t i o n s a y E x c l a m a t . i o n M a r k () { e c h o '!'; $o = n e w M y H e l l o W o r l d (); $o - > s a y H e l l o (); $o - > s a y W o r l d (); $o - > s a y E x c l a m a t i o n M a r k (); 上面的代码运行结果如下: 28 PHP核心技术与最佳实践 H e l l o World! 这里的MyHelloWorld同时实现了两个Traits,从而使其可以分别调用两个Traits里的代码 段。从代码中就可以看出,Traits和接n 很像,不同的是Traits是可以导人包含代码的接口。从 某种意义上来说,Traits和接口都是对“多重继承” 的一种变相实现。 总结关于接口的儿个概念: □ 接口作为一种规范和契约存在。作为规范,接口应该保证可用性;作为契约,接口应该 保证可控性。 □ 接口只是一个声明,一旦使用interface关键字,就应该实现它。可以由程序员实现(外 部接口),也可以由系统实现(内部接口)。接口本身什么都不做,但是它可以告诉我 们它能做什么。 □ PHP中的接口存在两个不足,一是没有契约限制,二是缺少足够多的内部接口。 接口其实很简单,但是接口的各种应用很灵活,设计模式中也有很大一部分是围绕接口展 开的。 1 . 5 反射 面向对象编程中对象被赋予了自省的能力,而这个自省的过程就是反射。 反射,直观理解就是根据到达地找到出发地和来源。比方说,我给你一个光秃秃的对象, 我可以仅仅通过这个对象就能知道它所属的类、拥有哪些方法。 反射指在PHP运行状态中,扩展分析PHP程序,导出或提取出关于类、方法、属性、参数 等的详细信息, 包括注释。这种动态获取信息以及动态调用对象方法的功能称为反射API。 1 . 5 . 1 如何使用反射API 以1. 1节的代码为模板,直观地认识反射的使用,如代码清单1-13所示。 代码清单 1-13 reflection, php < ? p h p c l a s s p e r s o n { p u b l i c $name; p u b l i c $gender; p u b l i c f u n c t i o n sa y () { e c h o $th i s - > name, " \tis ", $th i s - > gender, M\ r \ n w ; } p u b l i c f u n c t i o n 一 set ($name, $value) { e c h o " S e t t i n g $ n a m e to $ v a l u e \r\n"; $ t h i s - > $ n a m e = $value; p u b l i c f u n c t i o n — g e t ($name) { i f (!i s s e t ( $ t h i s - > $ n a m e ) ){ e c h o '未设直'; 第1 章面向对象思想的核心概念29 $ t h i s - > $ n a m e = ••正在为你设3L默认值"/ r e t u r n $ t h i s - > $name; $ s t u d e n t *= n e w p e r s o n (); $ s t u d e n t - > n a m e = * T o m •; $ s t u d e n t - > g e n d e r = ' m a l e ' ; $ s t u d e n t - > a g e ≪ 2 4 ; 现在,要获取这个student对象的方法和域性列表该怎么做呢?如以下代码所示: I I 获取对象爲性列表 $ r e f l e c t = n e w R e f l e c t i o n O b j e c t ( $ s t u d e n t ) ; $props ■ $ reflect ->getProperties (); f o r e a c h ($props as $prop) { p r i n t $ p r o p - > g e t N a m e () ) / /获取对象方法列表 $ m * $ r e f l e c t - > g e t M e t h o d s (); f o r e a c h ($m as $ prop) { p r i n t $ p r o p - > g e t N a m e () ) 也可以不用反射A P I ,使用class函数,返回对象屈性的关联数组以及更多的信息: / /返田对象属性的关联數组 v a r _ d u n p ( g e t _ o b j e c t _ v a r s ( $ s t u d e n t ) ) ; // ▢属性 v a r _ d u m p ( g e t一c l a s s一v a r s ( g e t一c l a s s ( $ s t u d e n t ) )); / / 回由类的;f 法名组i 的數组_ v a r _ d u m p ( g e t _ c l a s s一m e t h o d s ( g e t一c l a s s ( $ s t u d e n t ) )); 假如这个对象是从其他页面传过来的,怎么知道它属于哪个类呢? 一句代码就可以搞定: / /获取对象属性列表所属的类 e c h o g e t一c l a s s ( $ s t u d e n t ) ; 反射API的功能& 然更强大,甚至能还原这个类的原型,包括方法的访问权限,如代码淸 单1-14所示。 _____________________________ 代码清单1 - 1 4 使用反射API______________________________ / /反射获取类的原型 $obj ■* n e w R e f l e c t i o n C l a s s (• p e r s o n •); $ c l a s s N a m e = $obj - > g e t N a m e (); $ M e t h o d s = $ P r o p e r t i e s ≫ a r r a y (); f o r e a c h ( $ o b j - > g e t P r o p e r t i e s () a s $v) { $ P r o p e r t i e s [ $ v - > g e t N a m e ()] = $v; } f o r e a c h ( $ o b j - > g e t M e t h o d s () as $v) 30 < ♦ PHP核心技术与最佳实践 $ M e t h o d s [ $ v - > g e t N a m e ()] = $v; ) e c h o "cl a s s { $ c l a s s N a m e } \ n { \ n w ; i s _ a r r a y ( $ P r o p e r t i e s ) & & k s o r t ( $ P r o p e r t i e s ) ; f o r e a c h ($Prop e r t i e s as $k ■> $v) { e c h o "\t"; e c h o $ v - > i s P u b l i c () ? • p u b l i c ' : '',$v - > i s P r i v a t e () ? • p r i v a t e ': $ v - > i s P r o t e c t e d 0 ? ' p r o t e c t e d ' : $ v - > i s S t a t i c () ? • s t a t i c 1 : e c h o e c h o "\n"; i f ( i s一 a r r a y ( $ M e t h o d s ) ) k s o r t ($ M e t h o d s ); f o r e a c h ($Methods as $ k - > $v) echo "\tfunction {$k}( ){ }\n"; ) e c h o n }\nn ; 输出如下: c l a s s p e r s o n { p u b l i c g e n d e r p u b l i c n a m e f u n c t i o n 一 g e t (){} f u n c t i o n _ s e t (){} f u n c t i o n s a y 0 {) } 不仅如此,PHP手册中关于反射API更是有几丨个,可以说,反射完整地描述了一个类或 者对象的原型。反射不仅可以用于类和对象,还可以用于函数、扩展模块、异常等。 1 . 5 . 2 反射有什么作用 反射可以用于文档生成。因此可以用它对文件里的类进行扫描,逐个生成描述文档。 既然反射可以探知类的内部结构,那么是不是可以用它做hook实现插件功能呢?或者是做 动态代理呢?抛砖引玉,代码清单丨-15是个简单的举例。 ___________ __________________ 代码清单 1-15 proxy, php__________________________________ < ? p h p c l a s s m y s q l { f u n c t i o n c o n n e c t ($db) { e c h o "连接到數据库${也[0] }\r\n"; } 第1 章面向对象思想的核心概念< ♦ 31 cl a s s s q l p r o x y { p r i v a t e $ target; f u n c t i o n _ c o n s t r u c t ( $ t a r ) { $ t h i s - > t a r g e t [ ] = n e w $ t a r (); f u n c t i o n — c a l l ( $ n a m e , $args) { f o r e a c h <$t h i s - > t a r g e t as $ o b j ) { $r = n e w R e f l e c t i o n C l a s s ($o b j ); if ($method = $ r - > g e t M e t h o d ($ n a m e )) { if ($method -> isPublic () && ! $method-> isAbstract 0 ) { e c h o ••方法前拦我记录LOG\r\n"; $m e t h o d - > i n v o k e ($ o b j • $ a r g s ); e c h o ••方法后拦截\r\n"; } } $obj = n e w s q l p r o x y ('mysql *); $obj - > c o n n e c t (•m e m b e r •); 这里简单说明一下,真正的操作类是mysql类,但是sqlproxy类实现了根据动态传人参数, 代替实际的类运行,并且在方法运行前后进行拦截,并且动态地改变类中的方法和属性。这就 是简单的动态代理。 在平常开发中,用到反射的地方不多:一个是对对象进行调试,另一个是获取类的信息。 在MVC和插件开发中,使用反射很常见,但是反射的消耗也很大,在可以找到替代方案的情 况下,就不要滥用。 PHP有Token函数,可以通过这个机制实现一些反射功能。从简单灵活的角度讲,使用已 经提供的反射API是可取的。 很多时候,善用反射能保持代码的优雅和简洁,但反射也会破坏类的封装性,因为反射可 以使本不应该暴露的方法或属性被强制暴露了出来,这既是优点也是缺点。 思考题为什么要使用反射,反射存在的必要性是什么?或者说,反射为什么会存在? ( 已知一些情况:C语言是面向过程的编程语言,PHP、C+ + 、Java是具有面向对象风 格的编程语言。C 语言和C + +中没有对反射的原生支持,而PHP和Java具有反射 API。可以思考一下,为什么C/C + +语言里没有反射,以及C/C + +语言里是否需要 反射?) 32 ♦♦♦ PHP核心技术与最佳实践 1 . 6 异常和错误处理 在语言级别上,通常具有许多错误处理模式,但这些模式往往建立在约定俗成的基础上, 也就是说这些错误都是预知的。但是在大型程序中,如果每次调用都去逐一检杏错误,会使代 码变得冗长复杂,到处充斥着if- e ls e ,并且严重降低代码的可读性。而且人的因素也是不可信 赖的,程序员可能并不会把这些问题当一回事,从而导致业务异常。在这种背景下,就逐渐形 成了异常处理机制,或者强迫消除这些问题,或者把问题提交给能解决它的环境。这就把‘‘描 述在正常过程中做什么事的代码” 和“出了问题怎么办的代码” 进行分离。 1 . 6 . 1 如何使用异常处理机制 异常的思想鉍早可以追溯到20世纪60年代,其在C + +、Java中发扬光大,PHP则部分借 鉴了这两种语言的异常处理机制。 PHP里的异常,是程序运行中不符合预期的情况及与正常流程不同的状况。一种不正常的 情况,就是按照正常逻辑不该出错,但仍然出错的情况,这属于逻辑和业务流程的一种中断, 而不是语法错误。PHP里的错误则域于自身问题,是一种非法语法或者环境问题导致的、让编 译器无法通过检査甚至无法运行的情况。 在各种语言里,异常(exception)和错误(error)的概念是不一样的。在PHP里,遇到任 何自身错误都会触发一个错误,而不是抛出异常(对于一些情况,会同时抛出异常和错误)。 PHP—旦遇到非正常代码,通常都会触发错误,而不是抛出异常。在这个意义上,如果想使用 异常处理不可预料的问题,是办不到的。比如,想在文件不存在且数据库连接打不开时触发异 常,是不可行的。这在PHP里把它作为错误抛出,而不会作为异常自动捕获。 以经典的除零问题为例,如代码清单1-16所示。 代码清单 1 -16 exception, php // e x c e p t i o n . p h p < ? p h p $a - null; try{ $ a - 5 / 0 ; e c h o $a, PHP_E0L; }c a t c h ( e x c e p t i o n $e) { $e - > g e t M e s s a g e (); $a - - 1 ; e c h o $a; 第1 章面向对象思想的核心概念33 运行结果如图1-8所示。 2 Warning: Division by zero in 6:\bak\tenp\te≪pcode\exception2.php on line „ B 4 Call Stack: 5 0.0062 323216 1. {nain}(> G:\bak\tenp\tenpcode\exception2.php:0 图1-8 PHP里的除零错误 代码清单1-17所示是Java代码。 代码清单 1-17 ExceptionTry.java // E x c e p t i o n T r y . j a v a p u b l i c c l a s s E x c e p e t i o n T r y { p u b l i c s t a t i c v o i d tp () t h r o w s A r i t h m e t i c E x c e p t i o n { in t a; a =* 5 / 0 ; S y s t e m .o u t .p r i n t l n ("运算结果:•• + a ) ; } p u b l i c s t a t i c v o i d m a i n (String[ ] args) { in t a; t r y { a = 5 / 0 ; S y s t e m .o u t .p r i n t l n ("运算结果:" + a ) ; } c a t c h ( A r i t h m e t i c E x c e p t i o n e) { e.printStackTrace(); }f i n a l l y { a - -1; S y s t e m .o u t .p r i n t l n ("运算结果+ a ) ; ) t r y { E x c e p e t i o n T r y .t p (); } c a t c h ( Exception e) { S y s t e m .o u t .p r i n t l n ("异常被捕获"); } 运行结果如图1-9所示。 iava.lana.ArithagticfKccotion: / by ; at popular.FxcepelionTry.≪ain(^gpetiOTTry,jgVfl;l?) _ _ 果:-1 图1-9 Java里的除零异常 把tp方法中的第二条语句改为如下形式: 34 < ♦ PHP核心技术与最佳实践 a = 5 / 1 ; 修改后的结果如图1-10所示。 lava . lang,. Arithmetictxcept ion ; / by zero 运雜梁:-1 运縴结果:5 at popular.txcepetionTrv.imin(ExcgpetionTrv.iava:13) 图1-10 Java里的异常 由以上运行结果可以看到,对于除零这种“异常” 情况,PHP认为这是一个错误,直接 触发错误(warning也是错误,只是错误等级不一样),而不会自动抛出异常使程序进人异常 流程,故最终$ a值并不是预想中的- 丨,也就是说,并没有进人异常分支,也没有处理异 常。PHP只有你主动thnm后,才能捕获异常(一般情况是这样,也有一些异常PHP可以自 动捕获)。 而对于Ja v a ,则认为除零属于ArithmeticException,会对其进行捕获,并对异常进行处理。 也就是说,PHP通常是无法自动捕获有意义的异常的,它把所有不正常的情况都视作了错 误,你要想捕获这个异常,就得使用if-eUe结构,保证代码是正常的,然后判断如果除数为 0 , 则手动抛出异常,再捕获。Java有一套完整的异常机制,内置很多异常类会自动捕获各种各 样的异常。但PHP这个机制不完善。PHP内建的常见异常类主要有pdoexception、reflection exception 0 注意其实PHP和Java之间之所以有这个差距,根本原因就在于,在Java里,异常是唯一的错 误报告方式,而在PHP中却不是这样。通俗一点讲,就是这两种语言对异常和错误的界 定存在分歧。什么是异常,什么是错误,两种语言的设计者存在不同的观点。 也就是说,PHP只有手动抛出异常后才能捕获异常,或者是有内建的异常机制时,会先触 发错误,再捕获异常。那么PHP里的异常用法应该是什么样的呢?看下面的例子。 先定义两个异常类,它们需要继承自系统的exception, 如代码清单1-18所示。 _______________________________ 代码清单1 - 1 8定义两个异常类________________________________ c l a s s e m a i l E x c e p t i o n e x t e n d s e x c e p t i o n { ) c l a s s p w d E x c e p t i o n e x t e n d s e x c e p t i o n { f u n c t i o n _ t o S t r i n g () { r e t u r n •• < d i v c l a s s = \ " e r r o r \ " > E x c e p t i o n {$th i s - > g e t C o d e () }: {$ t h i s - > g e t M e s s a g e ()} in File: {$this - > g e t F i l e () Jon line: {$this - > g e t L i n e () } < / d i v > / /改写抛出异常结果 第1章面向对象思想的核心概念35 然后就是实际的业务,根据业务需求抛出不同异常,如代码清单1-丨9 所示。 ____________________ 代码清单1 - 1 9 根据业务需求抛出不同异常____ f u n c t i o n r e g ( $ r e g i n f o = null) { i f ( e m p t y ( $ r e g i n f o ) 11!i s s e t ( $ r e g i n f o ) ){ t h r o w n e w E x c e p t i o n {"参数非法"}; } i f ( e m p t y ( $ r e g i n f o [ ' e m a i l '])){ t h r o w n e w e m a i l E x c e p t i o n ("邮件为空"}; } i f ( $ r e g i n f o [ ' p w d ' ] ! = $ r e g i n f o [ 'r e p w d ']){ t h r o w n e w p w d E x c e p t i o n (••两次密码不一致"); ) e c h o *注新成功*; 上面的代码判断传人的参数,根据业务进行异常分发。首先,如果没有传人任何参数(这 个参数可以是POST进来,也可以是别的地方賦值得到),就把异常分发给exception超类,跳出 注册流程;如果Email地址不存在,那么把异常分发给自定义的emailException异常,跳出注册 流程;如果两次密码不一致,则将异常分发给自定义的pwdException,跳出注册流程。 现在异常分发了,但还不算完,还需要对异常进行分拣并做处理。代码如下所示: try{ re g (array ('email • = > • w a i t f o x @ q q . c o m *, 'pwd' = > 1 2 3 4 5 6 , • r e p w d ' ■> 1 2 3 4 5 6 7 8 ) ) ; // r e g (); }c a t c h ( e m a i l E x c e p t i o n $ee) { e c h o $ee - > g e t M e s s a g e (); }c a t c h (p w d E x c e p t i o n $ep) { e c h o $ep; e c h o P H P _ E O L , '特殊处理 *; }c a t c h ( E x c e p t i o n $e) { e c h o $e - > g e t T r a c e A s S t r i n g (); e c h o PHP_EOL, •其他情况,统一处理“; } 这一段代码用于捕获所抛出的各种异常,进行分门别类的处理。 提示可以尝试不同注册条件,看看异常分搞的流程。需要注意,exception作为超类应该放在 最后捕获。不然,捕获这个异常超类后,后面的捕获就终止了,而这个超类不能提供针 对性的信息和处理。 在这里,对表单进行异常处理,通过重写异常类、手动抛出错误的方式进行异常处理。这 是一种业务异常,可以人为地把所有不符合要求的情况都视作业务异常,和通常意义上的代码 异常相区别。 那PHP里的异常应该怎么用?在什么时候抛出异常,什么时候捕获呢?什么场景下能应用 36 ♦ > PHP核心技术与最佳实践 异常?在下面三种场景下会用到异常处理机制。 1. 对程序的悲观预测 如果一个程序员对自己的代码有“悲观情绪”,这里并不是指该程序员代码质量不高,而 是他认为自己的代码无法一一处理各种可预见、不可预见的情况,那该程序员就会进行异常处 理。假设一个场景,程序员悲观地认为自己的这段代码在高并发条件下可能产生死锁,那么他 就会悲观地抛出异常,然后在死锁时进行捕获,对异常进行细致的处理。 2. 程序的需要和对业务的关注 如果程序员希望业务代码中不会充斥大堆的打印、调试等处理,通常他们会使用异常机 制;或者业务上需要定义一些自己的异常,这个时候就需要自定义一个异常,对现实世界中各 种各样的业务进行补充。比如上班迟到,这种情况认为是一个异常,要收集起来,到月底集中 处理,扣你工资;如果程序员希望有预见性地处理可能发生的、会影响正常业务的代码,那么 它需要异常。在这里,强调了异常是业务处理中必不可少的环节,不能对异常视而不见。异常 机制认为,数据一致很重要,在数据一致性可能被破坏时,就需要异常机制进行事后补救。 举个例子,比如有个上传文件的业务需求,要把上传的文件保存在一个目录里,并在数据 库里插人这个文件的记录,那么这两步就是互相关联、密不可分的一个集成的业务,缺一不 可。文件保存失败,而插人记录成功就会导致无法下载文件;而文件保存成功数据库写人失 败,则会导致没有记录的文件成为死文件,永远得不到下载。 那么假设文件保存成功后没有提示,但是保存失败会自动抛出异常,访问数据库也一样, 插人成功没有提示,失败则自动抛出异常,就可以把这两个有可能抛出异常的代码段包在一个 try语句里,然后用catch捕捉错误,在catch代码段里删除没有被记录到数据库的文件或者删除 没有文件的记录,以保证业务数据的一致性。因此,从业务这个角度讲,异常偏重于保护业务 数据一致性,并且强调对异常业务的处理。 如果代码中只是象征性地try…catch,然后打印一个报错,S 后over。这样的异常不如不 用,因为其没有体现异常思想。所以,合理的代码应该如下: < ? p h p try{ / /可能出错的代码段 if (文件上传不成功} t h r o w (上传异常); if (插入数据库不成功> t h r o w (教据库搮作井常); }c a t c h (异常){ 必须的补救措施,如删除文件、刪除數据库插入记录,这个处理很细致 也可以如下: < ? p h p 上传{ if (文件上传不成功)th r o w (上传异常} ; 第1章面向对象思想的核心概念♦ > 37 if (插入數据库不成功)t h r o w (數据库操作异常>/ } / /其他代码... try{ 上传,• 其他; } c a t c h (上传异常> { 必须的补救措施,如刪除文件,刪除數据库插入记录 }catch (其他异常} { 记束l og 上面两种捕获异常的方式中,前一种是在异常发生时立刻捕获;后一种是分散抛异常集中 捕获。那到底应该是哪一种呢? 如果业务很重要,那么异常越早处理越好,以保证程序在意外情况下能保持业务处理的一致 性。比如一个操作有多个前提步骤,突然最后一个步骤异常了,那么其他前提操作都要消除掉才 行,以保证数据的一致性。并且在这种核心业务下,有大量的代码来做善后工作,进行数据补救, 这是一种比较悲观的、而又重要的异常。我们应把异常消灭在局部,避免异常的扩散。 如果异常不是那么重要,并且在单一人口、MVC风格的应用中,为了保持代码流程的统 一,则常常采用后一种异常处理方式。这种异常处理方式更多强调业务流程的走向,对善后工 作并不是很关心。这是一种次要异常,其将异常集中处理从而使流程更专一。 异常处理机制可以把每一件事当做事务考虑,还可以把异常看成一种内建的恢复系统。如 果程序某部分失败,异常将恢复到某个已知稳定的点上,而这个点就是程序的上下文环境,而 try块里面的代码就保存catch所要知道的程序上下文信息。因此,如果很看重异常,就应该分 散进行try-catch处理。 3. 语言级别的健壮性要求 在健壮性这点上,PHP是不足的。以Java为例,Java是一种面向企业级开发的语言,强调 健壮性。Java中支持多线程,Java认为,多线程被中断这种情况是彻彻底底的无法预料和避免 的。所以Java规定,凡是用了多线程,就必须正视这种情况。你要么抛出,不管它;要么捕 获,进行处理。总之,你必须面对丨nterruptedException异常,不准回避。也就是异常发生后应 对重要数据业务进行补救,当然你可以不做,但是你必须意识到,异常有可能发生。 这类异常是强制的。更多异常是非强制的,由程序员决定。Java对异常的分类和约束,保 证了 Java程序的健壮性。 异常就是无法控制的运行时错误,会导致出错时中断正常逻辑运行,该异常代码后面的逻 辑都不能继续运行。那么ty c a t c h处理的好处就是:可以把异常造成的逻辑中断破坏降到最 小范围内,并且经过补救处理后不彩响业务逻辑的完整性;乱抛异常和只抛不捕获,或捕获而 不补救,会导致数据混乱。这就是异常处理的一个重要作用,就是通过精确控制运行时的流 程,在程序中断时,有预见地用try缩小可能出错的影响范围,及时捕获异常发生并做出相应 补救,以使逻辑流程仍然能回到正常轨道上。 1 . 6 . 2 怎样看PHP的异常 PHP中的异常机制是不足的,绝大多数情况下无法自动抛出异常,必须用if-else先进行 判断,再手动抛出异常。手动抛异常的意义不是很大,因为这意味着在代码里已经充分预期到 错误的出现,也就算不上真正的“异常” ,而是意料之中。同时,这种方式还会使你陷人纷繁 复杂的业务逻辑判断和处理中。 Java语言做得比较好的就是定义了一堆内置的常见异常,不需要程序员判断各种异常情况 后再手动抛出,编译器会代我们进行判断业务是否发生错误,若发生了,则自动抛出异常。作 为程序员,只需要关心异常的捕获和随后补救,而不是像PHP那样关注到底会发生哪些异常, 用ir__else逐一判断,逐一抛出异常。 有没有什么机制使得PHP可以自动抛出异常呢?有’那就是结合PHP中的错误处理主动抛 出异常。 使用异常能一定程度上会降低耦合性,但是也不能滥用。滥用异常的后果就是很可能导致 代码被多处挂起,流程变得更复杂,难于理解。但是可以肯定,异常在PHP里有很大的价值, 越复杂的应用,越需要合理考虑使用异常。 38 ♦ > PHP核心技术与最佳实践 提示需要提R 读者关注,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。这种错误一般告诉你语法中存在不当的地方。如使用变量但是未定义就 第1 章面向对象思想的核心概念< ♦ 39 会报此错。最常见的,数组索引是字符时没有加引号,PHP就视为一个常量,先查找常 最表,找不到再视为变量。虽然PHP是脚本语言,语法要求不严,但是仍然建议对变量 进行初始化。这种错误不影响PHP正常流程。 □ warning是级别比较高的错误,在语法中出现很不恰当的情况时才会报此错误,比如函 数参数不匹配。这种级别的错误会导致得不到预期结果,故需要修改代码。 □ 更高级别的错误是fetal error。这是致命错误,直接导致PHP流程终结,后面的代码不再 执行。这种问题非改不可。 □ 最高级别的错误是语法解析错误prase error。上面提到的错误都属于PHP代码运行期间 错误,而语法解析错误属于语法检查阶段错误,这将导致PHP代码无法通过语法检查。 错误级别不止这几个,最主要的都在前面提到了。PHP手册中一共定义了 16个级别的错 误,最常用的就这几个。代码淸单1-20演示了常见级别的错误。 代码清单 1 -20 error, php_______________________________ // E r r o r .php < ? p h p $ d a t e = '2012 - 1 2 - 2 0 * ; if (ereg (" ([ 0 - 9] {4 } > - ([0 - 9 ] {1,2}) - <[0 - 9] {1,2 } > ", $date, $regs)) { e c h o ”$regs[3]. $ r e g s [ 2 ] .$regs[l]n; } e l s e { e c h o " I n v a l i d d a t e f o r m a t : $ d a t e n ; } i f ( $ i > 5 ) { e c h o • $ i 没有初始化啊• • PHP_EOL; $a = a r r a y ( 'o' - > 2 , 4, 6, 8); e c h o $a[ o ] ; $ r e s u l t = a r r a y一 s u m ( $ a f3); e c h o fun (); e c h o •致命错误后呢?还会执行吗? “; / / e c h o “最高级别的钳误“,$55; 这段代码演示至少四个级别的错误,如果看不全,应确保你的php.ini文件做了如下设定: e r r o r _ r e p o r t i n g = E一A L L | E _ S T R I C T d i s p l a y j e r r o r s = On erro^reporting指定错误级别,上面的设置是最严格的错误级别,具体设置可以参php.ini。 提示有一个技巧我想你会用到,那就是在代码质量或者环境不可控时(比如数据库连接失 敗),使用em>r_reporting(0), 这样就能屏蔽错误了, 正式部署时可以采取这样的策略, 防止错误消息泄露敏感信息。另外一个技巧就是在函数前加@符号,抑制错误信息输 出,如@ mysql_connect ( )。 40 ♦ > PHP核心技术与最佳实践 1.6.4 PHP中的错误处理机制 PHP里有一套错误处理机制,_______可以使用seLeirorJiandler接管PHP错误处理,也可以使用 trigger_error函数主动抛出一个错误。 set_em,r_handler()函数设H 用户自定义的错误处理函数。函数用于创建运行期间的用户自 己的错误处理方法。它需要先创建一个错误处理函数,然后设置错误级别。语法如下: se t 一e r r o r _ h a n d l e r ( e r r o r一f u n c t i o n , e r r o r _ t y p e s > 参数描述如下: □ error_function:规定发生错误时运行的函数。必需。 □ erTOr.types:规定在哪个错误报告级别会显示用户定义的错误。可选。默认为“E_ALL‛。 提示如果使用该函数,会完全绕过标准PHP错误处理函数,如果有必要,用户定义的错误处 理程序必须终止(d ie ( ) )脚本。 如果在脚本执行前发生错误,由于在那时自定义程序还没有注册,因此就不会用到这个自 定义错误处理程序。这先实现一个自定义的异常处理函数,如代码淸单1-21所示。 __________________________代码清单1 - 2 1 自定义的异常处理函数____________________ < ? p h p f u n c t i o n c u s t o m E r r o r ($errno, $errstr, $ errfile, $errline) { e c h o " < b > 将误代码:< /b > [ $ { e r r n o } ] $ { e r r s t r } \ r \ n n ; e c h o " 销■误所在的代码行:{ $ e r r l i n e }文件{ $ e r r file}\r\n"; e c h o M P H P 版本••,P H P一V E R S ION, / P H P 一OS, ") \ r \ n M ; // d i e (); ) s e t _ e r r o r一h a n d l e r ( " c u s t o m E r r o r " , E一A L L | E一S T R I C T ) ; $a = a r r a y ('o* m > 2 , 4, 6, 8); e c h o $ a [ o ] ; 在这个函数里,可以对错误的详情进行格式化输出,也可以做任何要做的事情,比如判断 当前环境和权限给出不同的错误提示,可使用errerjog函数将错误记人log文件,还可以细化 处理,针对$errno的不同进行对应的处理。 自定义的错误处理函数一定要有这四个输人变量$ermo、$errslr、$errfile、$errline。 emrn是一组常量,代表错误的等级,同时也有一组整数和其对应,但一般使用其字符串值 表示,这样语义更好一点。比如E_WARNING,其二进制掩码为4 . , 表示警告信息。 接下来,就是将这个函数作为回调参数传递给seLerrorJiandler。这样就能接管PHP原生的 错误处理函数了。要注意的是,这种托管方式并不能托管所有种类的错误,如£_£1111011、E_ PARSE、E_C0RE_ERR0R、E_C0RE_WARNING、E_COMPILE_ERROR、E_COMPILE_WARN第 1章面向对象思想的核心概念41 IN G ,以及E_STRICT中的部分。这些错误会以最原始的方式显示,或者不显示。 set_error_handler函数会接管PHP内置的错误处理,你可以在同一个页面使用reSt0re_error_ handlerO;取消接管。 注意如果使用自定义的selerrorjiandlei“接管PHP的错误处理,先前代码里的错误抑制@将 失效,这种错误也_______会被显示。 在PHP异常中,异常处理机制是有限的,无法自动抛出异常,必须手动进行,并且内K 异 常有限。PHP把许多异常看做错误,这样就可以把这些“异常” 像错误一样用seLerrorJiandler 接管,进而主动抛出异常。代码如下所示: f u n c t i o n c u s t o m E r r o r ($errno, $errstr, $err f i l e , $errline) / / 自定义锊误处理时,手动抛出异常 t h r o w n e w E x c e p t i o n ( $ l e v e l .' I * . $ { e r r s t r }); ) s e t _ e r r o r _ h a n d l e r ( " c u s t o m E r r o r " , E一A L L I E _ S T R I C T ) ; tr y { $a=5/0; ) c a t c h ( E x c e p t i o n $e) { e c h o “钱误信息:•,$e - > g e t M e s s a g e (); ) 这样就能捕获到异常和非致命的错误,就能按照1 .6 .1节里讲述的方法进行处理了,这样 可以弥补PHP异常处理机制的部分不足。 这种“曲折迂回” 的处理方式存在的问题就是:必须依靠程序员自己来掌控对异常的处 理,对于异常高发区、敏感区,如果程序员处理不好,就会导致前面所提到的业务数据不一致 的问题。其优点在于,可以获得程序运行时的上下文信息,‟以进行针对性的补救。 fetal error这样的错误虽然捕获不到,也无法在发生此错误后恢复流程处理,但是还是可以 使用一些特殊方法对这种错误进行处理的。这需要用到一-Is“函数--- register_shutdown_function, 此函数会在PHP程序终止或者die时触发一个函数,给PHP来一个短暂的“回光返照‛。在 PHP4的时代, 类不支持析构函数,常用这个函数模拟实现析构函数。实例代码如下: < ? p h p c l a s s S h u t d o w n p u b l i c f u n c t i o n s t o p () 42 ♦♦♦ PHP核心技术与最佳实践 r e g i s t e r一s h u t d o w n _ f u n c t i o n (array (new S h u t d o w n (), • s t o p ')); $a = n e w a (); / /将因为致命併误而失敗 e c h o •必须终止•; 可以运行看看效果。对于fetal error还能做点收尾工作,但是PHP流程的终止是必然的。 对于Parse error级别的错误,你只有傻眼了,除了可以修改配置文件p hp .ini,什么都做不了, 修改的内容如下: l o g一e r r o r s = O n e r r o r一l o g ■ u s r / l o g / p h p .log 这样一旦PHP发生了错误,就会被记人log文件,方便以后查询。 和exception类似,错误处理也有对应抛出错误的函数,那就是trigger_ error 函数,如下 所示: < ? p h p $ d i v i s o r = 0; if ($divisor = = 0) { t r i g g e r _ e r r o r ("Cannot d i v i d e b y zero", E _ U S E R _ E R R O R ); ) e c h o ' b r e a k ' ; 关于错误处理,主要就是这些内容,还有一些错误处理和调试相关,我们将会放到后面的 章节进行讲解。 提示在PHP中,错误和异常是两个不同的概念,这种设计从根本上导致了 PHP的异常和其他 语言相异。以Java为例,Java中,异常是镨___________误唯一的报告方式。说到底,两者的区别就 是对异常和错误的认识不同而产生的。PHP的异常绝大部分必须通过某种办法手动抛 出,才能被捕获到,是一种半自动化的异常处理机制。 无论是错误还是异常,都可以使用handler接管系统已有的处理机制。 1 . 7 本章小结 本章主要介绍面向对象思想的程序的组成元素— 类和对象。类是一个动作和属性的模 板,对象是数据的集合。结合PHP自身实际情况,着重讲述PHP里面向对象的一些比较模糊的 知识点,包括魔术方法、接口、多态、类的复用、反射、异常机制等。接口是一种类型,从接 口的实现讲述接口是怎么实现“即插即用” 的。 然后,对异常机制进行探讨。讲述异常应该是什么样的,应该怎么用,并且阐述了 PHP中 第1 章面向对象思想的核心概念< ♦ 43 的异常为什么会这样,应该在什么场合使用异常等。PHP起初没有异常机制,后期为了进军企 业级开发,才模仿Java加进去的,故有了错误处理和异常处理的并存,这种形式导致PHP异常 处理不伦不类,通过和Java对比,让我们了解到了异常的真实含义。错误处理是对异常处理的 一种补充。 到底面向过程和面向对象孰优孰劣呢?答案是:二者间并无高低优劣之别,它们各有 优劣。 其实在0 0 发展中,暴露出一些问题,如深人对象内部读写状态存在的困难,现实和开发 中不对应造成的建模困难,数据与逻辑绑定造成的类型臃肿。比如前面提到的反射,就是因为 面向对象的封装导致读写内部状态比较困难而产生的。 面向对象存在的问题是越来越多的语言引人函数式编程的特征,如闭包、回调等。PHP也 引入一些函数式编程的概念,有兴趣的读者可以自行研究。 第2 章面向对象的设计原则 第1章已经说过,面向对象是一种高度抽象的思维。在面向对象设计中,类是基本单位, 各种设计都是围绕着类来进行的。可以说,类与类之间的关系,构成了设计模式的大部分 内容。 在初学阶段,可以认为类就是屈性+函数组成的,实际上在底层存储上也确实是这样的, 但是,这些仅仅是确定一个独立的类。而类与类之间的关系是设计模式所要探讨的内容。 经典的设计模式有23种,每种都是对代码复用和设计的总结,就设计模式而言,除了熟读 GOF经典外,推荐《敏捷软件开发一原则、方法与实践》一书。本章并不就具体的设计模式 展开讨论,而是讨论一些基本的设计原则,并给出一些小的实例,最后,作为前两章的总结, 探讨一下PHP中的面向对象的一些问题。 2. 1 面向对象设计的五大原则 在面向对象的设计中,如何通过很小的设计改变就可以应对设计需求的变化,这是设计者 极为关注的问题。为此不少0 0 先驱提出了很多有关面向对象的设计原则用于指导0 0 的设计 和开发。下面是几条与类设计相关的设计原则。 面向对象设计的五大原则分别是单一职责原则、接口隔离原则、开放-封闭原则、替换原 则、依赖倒置原则。这五大原则也是23种设计模式的基础e 。 2 . 1 . 1 单一职责原则 亚当• 斯密曾就制针业做过一个分工产生效率的例子R。对于一个没有受过相应训练,又 不知道怎样使用这种职业机械的工人来讲,即使他竭尽全力地工作,也许一天连一根针也生产 不出来,当然更生产不出20根针了。但是,如果把这个行业分成各种专门的组织,再把这种组 织分成许多个部门,其中大部分部门也同样分为专门的组织。把制针分为18种不同工序,这 18种不同操作由18个不同工人来担任。那么,尽管他们的机器设备都很差,但他们尽力工作, 一天也能生产12磅针。每磅中等型号针有4000根,按这个数字计算,十多个人每天就可以制 造48000根针,而每个人每天能制造4800根针。如果他们各自独立地工作,谁也不专学做一种 专门的业务,那么他们之中无论是谁都绝不可能一天制造20根针,也许连1根针也制造不出 来。这就是企业管理中的分工,在面向对象的设计里,叫做单一职责原则(Single Pesponsibility ㊀这些原则主要是山R≫bertC.Martin 在 <敏捷软件开发一I);(则、方法与实践》一书中总结出来的。 c 见《国富论> 第1 审,分;T:理论是亚当•斯密的一个重要经济理论。 第2 章面向对象的设计原则♦ > 45 Principle, SRP)。 在{敏捷软件开发》中,把“职责” 定义为“变化的原因” ,也就是说,就一个类而言, 应该只有一个引起它变化的原因。这是一个最简单,最容易理解却最不容易做到的一个设计原 则。说得简单一点,就是怎样设计类以及类的方法界定的问题。这种问题是很普遍的,比如在 MVC的框架中,很多人会有这样的疑惑,对于表单插人数据库字段过滤与安全检查应该是放在 control层处理还是model层处理,这类问题都可以归到单一职责的范围。 再比如在职员类里,将工程师、销售人员、销售经理等都放在职员类里考虑,其结果将会 非常混乱。在这个假设下,职员类里的每个方法都要用if-_else判断是哪种情况,从类结构上 来说将会十分臃肿,并且上述三种职员类型,不论哪一种发生需求变化,都会改变职员类,这 是我们所不愿意看到的! 从上面的描述中应该能看出,单一职责有两个含义:一个是避免相同的职责分散到不同的 类中,另一个是避免一个类承担太多职责。 那为什么要遵守SRP呢? (1 ) 可以减少类之间的耦合 如果减少类之间的耦合,当需求变化时,只修改一个类,从而也就隔离了变化;如果一个 类有多个不同职责,它们耦合在一起,当一个职责发生变化时,可能会影响其他职责。 (2 ) 提高类的复用性 修理电脑比修理电视机简单多了。主要原因就在于电视机各个部件之间的耦合性太高,而 电脑则不同,电脑的内存、硬盘、声卡、网卡、键盘灯部件都可以很容易地单独拆卸和组装。 某个部件坏了,换上新的即可。 上面的例子就体现了单一职责的优势。由于使用了单一职责,使得“组件‛可以方便地 “拆卸” 和“组装‛。 不遵守SRP会影响对该类的复用性。当只需要复用该类的某一个职责时,由于它和其他的 职责耦合在一起,也就很难分离出。 遵守SRP在实际代码开发中有没有什么应用_______?有的。以数据持久层为例,所谓的数据持 久层主要指的是数据库操作,当然,还包括缓存管理等。以数据库操作为例,如果是一个复 杂的系统,那么就可能涉及多种数据库的相互读写等,这时就需要数据持久层支持多种数据 库。应该怎么做?定义多个数据库操作类?你的想法巳经很接近了,再进一步,就是使用工 厂模式。 工厂模式(Factory)允许你在代码执行时实例化对象。它之所以被称为工厂模式是因为它 负责“生产” 对象。以数据库为例,工厂需要的就是根据不同的参数,生成不同的实例化对 象。鉍简单的工厂就是根据传人的类型名实例化对象,如传人MySQL,就调用MySQL的类并实 例化,如果是SQLite,则调用SQLite的类并实例化,甚至可以处理TXT、Excel等“类数据库‛。 工厂类也就是这样的一个类,它只负责生产对象,而不负责对象的具体内容。 先定义一个接口,规定一些通用的方法,如代码清单2-1所示。 ______________________________代码清单2 - 1 定义一个适配器接口 < ?php i n t e r f a c e D b _ A d a p t e r { I h i t * 數据库连接 * @parain $ c o n f i g 数振库配直 * @ r e t u r n r e s o u r c e */ p u b l i c f u n c t i o n c o n n e c t ( $ c o n f i g ) ; 执行数据库查詢 @ pa r am s t r i n g $ q u e r y數採库查询S Q L 字符串 @ p a r a m m i x e d $ h a n d l e 连接对象 0 r e t u r n r e s o u r c e p u b l i c f u n c t i o n q u e r y ($query, $handle) / 46 < ♦ PHP核心技术与最佳实践 这是一个简化的接口,并没有提供所有方法,其定义了 MySQL数据库的操作类,这个类实 现了 Db_AdaPter接口,具体如代码清单2-2所示。 ________________________代码清单2 - 2 定义MySQL数据库的操作类________________________ < ? p h p c l a s s D b _ A d a p t e r _ M y s q l i m p l e m e n t s D b一A d a p t e r p r i v a t e $_dbLink; / /數据库连接字符串标示 * 數据库连接函數 * * @ p a r a m $ c o n f i g 數据库 K “罝 * @ t h r o w s Db一E x c e p t i o n * @ r e t u r n r e s o u r c e V p u b l i c f u n c t i o n c o n n e c t ($conf ig) i if ($this - > _ d b L i n k ■ @ m y s q l _ c o n n e c t ($config - > h o s t . (empty ($con fi g - > p o r t ) ? " : •:* . $ c o n f i g - > p o r t ) , $ c o n f i g - > user, $ c o n f i g - > p a s s w o r d , t r u e ) ) { if ( @ m y s q l _ s e l e c t _ d b ($config - > d a t a b a s e , $ t h i s - > _ d b L i n k ) ) { if ($config - > charset) { m y s q l _ q u e r y ("SET N A M E S ' { $ c o n f i g - > c h a r s e t } 'w , $ t h i s - > _ d b L i n k ) ; } r e t u r n $ t h i s - > dbLi n k ; 第2章面向对象的设计原则47 / * * 數据库异常*/ t h r o w n e w D b _ E x c e p t i o n (@m y s q l一e r r o r ($this - > _ d b L i n k ) ); * 执行數据库奎詢 * * @ p a r a m s t r i n g $ q u e r y数振库查询S Q L 字符串 * S p a r a m m i x e d $ h a n d l e 连接对象 * 0 r e t u r n r e s o u r c e */ p u b l i c f u n c t i o n q u e r y ($query, $handle) { if ($r e s o u r c e ≪ @ m y s q l _ q u e r y ( $ q u e r y , $h a n d l e ) ) { r e t u r n $ resou r c e ; 接下来是SQLhe数据库的操作类,同样实现了 Db_Adapter接口,如代码清单2-3所示。 _________________________ 代码清单2-3 SQLite数据库的搡作类__________________________ < ? p h p c l a s s Db一A d a p t e r一s q l i t e i m p l e m e n t s M > _ A d a p t e r { p r i v a t e $ _ d b L i n k ; / /數据库连接字符串标示 * 數据库连接函數 * @ p a r a m $ c o n f i g 教据库 f c l * @ t h r o w s 一E x c e p t i o n * 0 return resource */ p u b l i c f u n c t i o n c o n n e c t ($config) if ($t h i s - > _ d b l i n k ■ s q l i t e — o p e n ( $ c o n f i g - > file, 0666, $ e r r o r ) ) { r e t u r n $ t h i s - > jdbli n k ; } / * * 数据库异常*/ t h r o w n e w Db一E x c e p t ion ( $ e r r o r ) ; * 执行数据库查询 * 0 p a r a m s t r i n g $ q u e r y教据库查均S Q L 字符串 48 ♦ > PHP核心技术与最佳实践 * @ p a r a m m i x e d $ h a n d l e 连接对象 * @ r e t u r n r e s o u r c e */ p u b l i c f u n c t i o n q u e r y ($query, $handle) if ( $ resource = @ s q l i t e _ q u e r y ( $ q u e r y , $ h a n d l e ) ) { r e t u r n $ resou r c e ; 好了,如果现在需要一个数据库操作的方法的话怎么做?只需定义一个工厂类,根据传人 不同的参数生成需要的类即可,如代码淸单2-4所示。 _____________________________代码清单2 - 4 定义一个工厂类_____________________________ < ? p h p c l a s s s q l F a c t o r y p u b l i c s t a t i c f u n c t i o n f a c t o r y ($type) { if (inclu de_once ' D r i v e r s / ' .$ t ype . ' . p h p ' ) { $ c l a s s n a m e ≫ * Db_Adapter_' .$type; r e t u r n n e w $classname; } e l s e { t h r o w n e w E x c e p t i o n ('D r i v e r not f o u n d ' ); 要调用时,就可以这么写: $ d b = s q l F a c t o r y ::f a c t o r y (•M y S Q L •); $ d b = s q l F a c t o r y ::f a c t o r y (•S Q L i t e •); 我们把创建数据库连接这块程序单独拿出来,程序中的CURD就不用关心是什么数据库 了,只要按照规范使用对应的方法即可。 工厂方法让具体的对象解脱了出来,使其并不再依赖具体的类,而是抽象。除了数据库操 作这种显而易见的设计外,还有什么地方会用到工厂类呢?那就是SNS中的动态实现。 下面的图片来自国内某SNS网站,属于当前新鲜事页面,可以看到针对不同行为,其生成 了不同动态。比如,参加了某个小组,动态显示的就是“X X参加了 Y Y小组‛;收到某某的礼 物,别人看到的多台就是“X X收到了 Y Y的ZZ礼物‛,如图2-1所示。 以上这种动态应该怎么设计呢,敁容易想到的就是用工厂模式,根据传人的操作不同,结 合模板而生成不同的动态,如代码清单2-5所示。 第2 章面向对象的设计原则♦ > 49 若水收到了 一个TA送的2012来啦 礼供是私下eiti的f t ! 黌是想知暹他们说7禅马,线快去找昧若水 似一下, 2011-12-31 20:58收起回貫丨免竇送礼丨&TA—个 ;^CEg____________________________________“:: 若水砮加了 Team.Algonthmsl 本小组讨论c、c-m-v Java、algorthmsffi关◊致力子播 法技K ,共向典好生连,欢迎谷fatnx. 5611人• 加丨45t. 子 2011-10-17 22:22 我gfefcD I 分* 若水已与対亦菲交換名片• 2011-0SMH 00:00 查籌名片 图2 - 1某SNS网站的动态展示 ___________________________ 代码清单2 - 5 工厂模式 — < b e a d id ■ " f e e d S e r v i c e F a c t o r y " c l a s s = M F e e d S e r v i c e F a c t o r y " > < p r o p e r t y n a m e ≫ " f e e d M a p " > < e n t r y k ey = " f r i e n d " v a l u e - re f = " f r i e n d F e e d " / > < e n t r y k e y = "album*' v a l u e - re f = "albumFeed'* / > < e n t r y k e y - "rep l y " v a l u e - re f ≫ " r e p l y F e e d " / > < e n t r y k e y = " s h a r e " v a l u e - re f = " s h a r e F e e d " / > < e n t r y k e y = " v i d e o " v a l u e - re f = " v i d e o F e e d " / > < e n t r y k e y = " g r o u p " v a l u e - re f ■ " g r o u p F e e d‛ / > < /map > < / p r o p e r t y > < / b e a n > 以上代码是一个动态的生成配S , 通过FEED的类型匹配到k e y ,取到对应的b ean,然后创 建不同的动态,用的就是工厂模式。 设计模式里面的命令模式也是SRP的体现,命令模式分离“命令的请求者” 和‚命令的实 现者” 方面的职责。举一个很好理解的例子,就是你去餐馆吃饭,餐馆存在顾客、服务员、厨 师三个角色。作为顾客,你只要列出菜单,传给服务员,由服务员通知厨师去实现。作为服务 员,只需要调用准备饭菜这个方法(对厨师大喊“该炒菜了”),厨师听到要炒菜的请求,就立 即去做饭。在这里,命令的请求和实现就完成了解耦。 模拟这个过程,首先定义厨师角色,厨师进行实际的做饭、烧汤的工作。详细代码如代码 清单2-6所示。 50 PHP核心技术与最佳实践 代码清单2 - 6 餐馆的示例 對师类,命令接受者与执行者 j c l a s s cook{ p u b l i c f u n c t i o n m e a l () { e c h o “番茄炒鸡蛋',PHP_E0L; } p u b l i c f u n c t i o n d r i n k () { e c h o .紫菜蛋花汤•,P H P一EOL; } p u b l i c f u n c t i o n o k () { e c h o ,完毕' P H P —EOL; ) } / /然后是命令接口 i n t e r f a c e C o m m a n d { / /命令接口 p u b l i c f u n c t i o n e x e c u t e (); 现在轮到服务员出场,服务员是命令的传送者,通常你到饭馆吃饭都是叫服务员吧,不可 能直接叫厨师,一般都是叫“服务员,给我来盘番茄炒西红柿” ,而不会直接叫“厨师,给我 来盘番茄炒西红柿” 。所以,服务员是顾客和厨师之间的命令沟通者。模拟这个过程的代码如 代码清单2-7所示。 ____________________________ 代码清单2 - 7模拟服务员与厨师的过程____________________________ c l a s s M e a l C o m m a n d i m p l e m e n t s C o m m a n d { p r i v a t e $cook; / /绑定命令接受者 p u b l i c f u n c t i o n 一 c o n s t r u c t (cook $cook) { $ t h i s - > c o o k - $cook; ) p u b l i c f u n c t i o n e x e c u t e () { $ t h i s - > c o o k - > m e a l (); / /把消息传递给街师,让厨师做饭,下同 c l a s s D r i n k C o m m a n d i m p l e m e n t s C o m m a n d { p r i v a t e $cook; / /螂定命令接受者 p u b l i c f u n c t i o n _ c o n s t r u c t (cook $cook) { $this -> cook = $cook; p u b l i c f u n c t i o n e x e c u t e 0 { $ t h i s - > c o o k - > d r i n k (); 第2 章面向对象的设计原则♦ > 51 现在顾客可以按照菜单叫服务员了,如代码清单2-8所示。 _________________________代码清单2 - 8 模拟顾客与服务员的过程 c l a s s c o o k C o n t r o l { p r i v a t e $me a 1 c o m m a n d ; p r i v a t e $ drin kc o m m a n d ; / /将命令发送者绑定到命令接收器上面来 p u b l i c f u n c t i o n addConunand (Command $ m e a lcommand. C o m m a n d $drink c o m m a n d ) { $th i s - > m e a l c o m m a n d ■ $ m e a 1c o m m a n d ; $this - > drinkcommand = $drinkcommand; } p u b l i c f u n c t i o n c a l l m e a l () { $ t h i s - > m e a l c o m m a n d - > e x e c u t e (); ) p u b l i c f u n c t i o n c a l l d r i n k () { $ th i s - > d r i n k c o m m a n d - > e x e c u t e (); 好了,现在完成整个过程,如代码清单2-9所示。 _____________________________代码清单2 - 9 实现命令模式 $ c o n t r o l = n e w c o o k C o n t r o l ; $ c o o k = n e w cook; $ m e a l c o m m a n d = n e w M e a l C o m m a n d ( $ c o o k ) ; $ d r i n k c o m m a n d = n e w D r i n k c o m m a n d ( $ cook); $ c o n t r o l - > addCoiranand($mealcommand,$drinkcommand); $ c o n t r o l - > c a l l m e a l (); $ c o n t r o l - > c a l l d r i n k (); 从上面的例子可以看出,原来设计模式并非纯理论的东西,而是来源于实际生活,就连普 通的餐馆老板都懂设计模式这门看似高深的学问。其实,在经济和管理活动中,对流程的优化 就是对各种设计模式的摸索和实践。所以,设计模式并非计算机编程中的专利。事实上,设计 模式的起源不是计算机学科,而是源于建筑学。 在设计模式方面,不仅以上这两种体现了 S R P ,还有别的(比如代理模式)也体现了 SRP。 SRP不只是对类设计有意义,对以模块、子系统为单位的系统架构设计同样有意义。 模块、子系统也应该仅有一个引起它变化的原因,如MVC所倡导的各个层之间的相互分 离其实就是SRP在系统总体设计中的应用。图2-2是来自C1框架的流程图。 SRP是最简单的原则之一,也是最难做好的原则之一。我们会很自然地将职责连接在一 起。找到并且分离这些职责是软件设计需要达到的目的。 52 ♦♦♦ PHP核心技术与最佳实践 index.php 图2-2 MVC中的流程 一些简单的应该遵循的做法如下: □ 根据业务流程,把业务对象提炼出来。如果业务流层的链路太复杂,就把这个业务对象 分离为多个单一业务对象。当业务链标准化后,对业务对象的内部情况做进一步处理。 把第一次标准化视为最髙层抽象,第二次视为次高层抽象,以此类推,直到‚恰如其 分” 的设计层次。 □ 职责的分类需要注意。有业务职责,还要有脱离业务的抽象职责,从认识业务到抽象 算法是一个层层递进的过程。就好比命令模式中的顾客,服务员和厨师的职责,作为 老板(即设计师)的你需要规划好各自的职责范围,既要防止越俎代庖,也要防止 互相推倭。 2 . 1 . 2 接□隔离原则 设计应用程序的时候,如果一个模块包含多个子模块,那么我们应该小心对该模块做出抽 象。设想该模块由一个类实现,我们可以把系统抽象成一个接口。但是要添加一个新的模块扩 展程序时,如果要添加的模块只包含原系统中的一些子模块,那么系统就会强迫我们实现接口 中的所有方法,并且还要编写一些哑方法。这样的接口被称为胖接口或者被污染的接口,使用 这样的接口将会给系统引人一些不当的行为,这些不当的行为可能导致不正确的结果,也可能 导致资源浪费。 1. 接口隔离 接口隔离原则(Interface Segregation Principle, IS P )表明客户端不应该被强迫实现一些他 们不会使用的接口,应该把胖接口中的方法分组,然后用_______多个接口代替它,每个接口服务于一 个子模块。简单地说,就是使用多个专门的接口比使用单个接口要好得多。 ISP的主要观点如下: 1) 一个类对另外一个类的依赖性应当是建立在最小的接口上的。 □ ISP可以达到不强迫客户(接口的使用方)依赖于他们不用的方法,接「丨的实现类应该 只呈现为单一职责的角色(遵守SRP原则)。 □ ISP还可以降低客户之间的相互影响— 当某个客户程序要求提供新的职责(需求变 辅助函数 类库 控制器 m m — > < ~ — > — > < ~ 第2 章面向对象的设计原则< ♦ 53 化)而迫使接口发生改变时,影响到其他客户程序的可能性会最小。 2 ) 客户端程序不应该依赖它不需要的接口方法(功能)。 客户端程序不应该依赖它不需要的接口方法(功能),那依赖什么?依赖它所需要的接口。 客户端需要什么接口就提供什么接口,把不需要的接口剔除,这就要求对接口进行细化,保证 其纯洁性。 比如在应用继承时,由于子类将继承父类中的所有可用的方法;而父类中的某些方法,在 子类中可能并不需要。例如,普通员工和经理都继承自雇员这个接口,员工需要每天写工作曰 志,而经理则不需要。因此不能用工作日志来卡经理,也就是经理不应该依赖于提交工作曰志 这个方法。 可以看出,ISP和S R P在概念上是有一定交叉的。事实上,很多设计模式在概念上都有交 叉,甚至你很难判断一段代码属于哪一种设计模式。 ISP强调的是接口对客户端的承诺越少越好,并且要做到专一。当某个客户程序的要求发 生变化,而迫使接U 发生改变时,影响到其他客户程序的可能性小。这实际上就是接口污染的 问题。 2. 对接口的污染 过于臃肿的接口设计是对接口的污染。所谓接口污染就是为接口添加不必要的职责,如果 开发人员在接口中增加一个新功能的主要目的只是减少接口实现类的数目,则此设计将导致接 口被不断地“污染” 并“变胖‛。 接口污染会给系统带来维护困难和重用性差等方面的问题。为了能够重用被污染的接口, 接口的实现类就被迫要实现并维护不必要的功能方法。 “接口隔离” 其实就是定制化服务设计的原则。使用接口的多重继承实现对不同的接口的 组合,从而对外提供组合功能— 达到“按需提供服务‛。 看下面这个例子,如图2-3所示。 图2 - 3存在污染的接口设计 服务实现类 客户A 需要A 服务,只要针对客户A 的方法发生改变,客户B 和客户C 就会受到影响。故 这种设计需要对接口进行隔离,如图2-4所示。 54 < * PHP核心技术与最佳实践 〇 服务接nc ♦服务c() 图2 - 4 减少接口中的污染 由图2-4可知,如果针对客户A 的方法发生改变,客户B 和客户C 并不会受到任何影响。 你可能会想,这样做接口那岂不是会很多?这个问题问得很好,接口既要拆,但也不能拆得太 细,这就得有个标准,这就是高内聚。接口应该具备一些基本的功能,能独一完成一个基本的 任务。 图2-4所示只是个抽象的例子,在实际应用中,会遇到如下问题:比如,我需要一个能适 配多种类型数据库的DAO实现,那么首先应实现一个数据库操作的接口,其中规定一些数据库 操作的基本方法,如连接数据库、增刪査改、关闭数据库等。这是一个最少功能的接口。对于 —些MySQL中特有的而其他数据库不具有或性质不同的方法,如PHP里可能用到的MySQL的 pconnect方法,其他数据库里并不存在和这个方法相同的概念,这个方法也就不应该出现在这 个基本的接口里,那这个基本的接口应该有哪些基本的方法呢? PDO已经告诉你了。 PDO是一个抽象的数据接口层,它告诉我们一个基本的数据库操作接口应该实现哪些基本 的方法。接口是一个高层次的抽象,所以接口里的方法应该是通用的、基本的、不易变化的。 还有一个问题,那些特有的方法应该怎么实现?根据ISP原则,这些方法可以在另一个接 口中存在,让这个“异类” 同时实现这两个接口。 对于接口的污染,可以考虑下面这两条处理方式: □ 利用委托分离接口。 □ 利用多继承分离接口。 委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象 来处理,如策略模式、代理模式等中都应用到了委托的概念。至于其实现,在反射那一节其实 已经实现了,这里就不再细讲了。 利用多继承分离接口,在接口一节也做了相应的讲解,这里不再重复。 第2 章面向对象的设计原则♦> 55 2 . 1 . 3 幵放-封闭原则 1 .什么是"开放-封闭” 随着软件系统的规模不断增大,软件系统的维护和修改的复杂性不断提髙,这种困境促使 法国工程院院士 Bertrand Meyer 在 1998 年提出了 ‚开放 -封闭‛ (Open-Close Principle, OCP) 原则,这条原则的基本思想是: □ Open ( Open for extension) 模块的行为必须是开放的、支持扩展的,而不是僵化的。 □ Closed ( Closed for modification) 在对模块的功能进行扩展时, 不应该影响或大规模地影 响已有的程序模块。 换句话说,也就是要求开发人员在不修改系统中现有功能代码(源代码或者二进制代码) 的前提下,实现对应用系统的软件功能的扩展。用一句话概括就是:一个模块在扩展性方面应 该是开放的而在更改性方面应该是封闭的。 从生活中,最容易想到的例子就是电脑,我们可以轻松地对电脑进行功能的扩展,而只R 通过接口连人不同的设备。 开放-封闭能够提高系统的可扩展性和可维护性,但这也是相对的,对于一台电脑不可能 完全开放,有些设备和功能必须保持稳定才能减少维护上的困难。要实现一项新的功能,你就 必须升级硬件,或者换一台更髙性能的电脑。以电脑中的多媒体播放软件为例,作为一款播放 器,应该具有一些基本的、通用的功能,如打开多媒体文件,停止播放、快进、音景调节等功 能。但不论是什么播放器,不论是在什么平台下,遵循这个原则设计的播放器都应具有统一风 格和操作习惯,无论换用哪一款播放器,都应保证操作者能快速上手。 以播放器为例,先定义一个抽象的接口,代码如下所示。 i n t e r f a c e p r o c e s s { p u b l i c f u n c t i o n p r o c e s s (); } 然后,对此接口进行扩展,实现解码和输出的功能,如代码清单2-10所示。 _____________________________代码清单2 - 1 0实现播放器的编码功能_____________________________ c l a s s p l a y e r e n c o d e i m p l e m e n t s p r o c e s s { p u b l i c f u n c t i o n p r o c e s s () { e c h o " e n code\r\n"; } } c l a s s p l a y e r o u t p u t i m p l e m e n t s p r o c e s s { p u b l i c f u n c t i o n p r o c e s s () { e c h o " o u tput\r\n"; 对于播放器的各种功能,这里是开放的,只要你遵照约定,实现了 process接口,就能给播 56 PHP核心技术与最佳实践 放器添加新的功能模块。这里只实现解码和输出模块,还可以依据需求,加人更多新的模块。 接下来为定义播放器的线程调度管理器,播放器一旦接收到通知(可以是外部单击行为, 也可以是内部的notify行为),将回调实际的线程处理,如代码淸单2-11所示。 ____ ______________ 代码清单2 - 1 1 播放器的"调度管理器” c l a s s p l a y P r o c e s s { p r i v a t e $ m e s s a g e = null; p u b l i c f u n c t i o n _ c o n s t r u c t () { ) p u b l i c f u n c t i o n c a l l b a c k (event $event) { $ t h i s - > m e s s a g e ≫ $ e v e n t - > c l i c k (); i f ($this - > m e s s a g e i n s t a n c e o f p r o c e s s ) { $ t h i s - > m e s s a g e - > p r o c e s s (); ) } 具体的产品出来了,在这里定义一个MP4类,这个类是相对封闭的,其中定义事件的处理 逻辑,如代码清单2-12所示。 __________________________代码清单2 - 1 2 播放器的事件处理逻辑_______ c l a s s mp4 { p u b l i c f u n c t i o n w o r k () { $ p l a y P r o c e s s = n e w p l a y P r o c e s s (); $ p l a y P r o c e s s - > c a l l b a c k (new e v e n t (* e n c o d e ')); $ p l a y P r o c e s s - > c a l l b a c k (new e v e n t (' o u t p u t ')); ) 最后为事件分拣的处理类,此类负责对事件进行分拣,判断用户或内部行为,以产生正确 的“线程”,供播放器内置的线程管理器调度,如代码淸单2-13所示。 ___________________________代码清单2 - 1 3 播放器的事件处理类___________________________ cl a s s e v e n t { p r i v a t e $m; public function 一 construct ($me) { $ t h i s - > m ≫ $me; } p u b l i c f u n c t i o n c l i c k () { s w i t c h ($this - > m ) { c a s e 'e n c o d e ': r e t u r n n e w p l a y e r e n c o d e (); bre a k ; c a s e 'o u t p u t ': 第2 章面向对象的设计原则57 r e t u r n n e w p l a y e r o u t p u t (); brea k ; 最后,运行下下面的代码: $mp4 = n e w m p 4 ; $mp4 - > w o r k (); 输出结果如下: e n c o d e o u t p u t 这就实现了一个基本的播放器,此播放器的功能模块是对外开放的,但是内部处理应该是 相对封闭和稳定的。但这个实现还存在一些问题,这就需要你来发现了。有时候为了降低系统 的复杂性,也会不完全遵守设计模式,而是对其进行增删改。 2 .如何遵守开放-封闭原则 实现开放-封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类 依赖于固定的抽象,这样的修改就是封闭的;而通过面向对象的继承和对多态机制,可以实现 对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开 放的。 1 ) 在设计方面充分应用“抽象” 和“封装” 的思想。 一方面也就是要在软件系统中找出各种可能的“可变因素”,并将之封装起来; 另一方面,一种可变性因素不应当散落在多个不同代码模块中,而应当被封装到一个对 象中。 2 ) 在系统功能编程实现方面应用面向接口的编程。 当需求发生变化时,可以提供该接口新的实现类,以求适应变化。 面向接口编程要求功能类实现接口,对象声明为接口类型。在设计模式中,装饰模式比较 明显地用到了 OCP。 2 . 1 . 4 替換原则 替换原则由M1T计算机科学实验室的Liskov女士在1987年的OOPSLA大会上的一篇文章 《Data Abstractimi and Hierarchy》中提出,主要阐述有关继承的一些原则,故又称里氏替换 原则。 2002 年,Robert C.Martin 出版了一本名为《Agile Software Development Principles Patterns and Practices》的书,在书中他把里氏代换原则最终简化为一句话: “Subtypes must be substitutable for their base types”。(子类必须能够替换成它们的基类。) 58 PHP核心技术与最佳实践 1. L S P的内容 里氏替换原则(Liskov Substitution Principle, L S P )的定义和主要的思想如下:由于面向对 象编程技术中的继承在具体的编程中过于简单,在许多系统的设计和编程实现中,我们并没有 认真地、理性地思考应用系统中各个类之间的继承关系是否合适,派生类是否能正确地对其基 类中的某些方法进行重写等问题。因此经常出现滥用继承或者错误地进行了继承等现象,给系 统的后期维护带来不少麻烦。这就需要我们有一个设计原则来遵循,它就是替换原则。 LSP指出:子类型必须能够替换掉它们的父类型、并出现在父类能够出现的任何地方。它 指导我们如何正确地进行继承与派生,并合理地重用代码。此原则认为,一个软件实体如果使 用一个基类的话,那么一定适用于其子类,而且这根本不能察觉出基类对象和子类对象的区 别。想一想,是不是和第一章提到的多态的概念比较像? 2. L S P主要是针对继承的设计原则 因为继承与派生是OOP的一个主要特性,能够减少代码的重复编程实现,从而实现系统中 的代码复用,但如何正确地进行继承设计和合理地应用继承机制呢? 这就是LSP所要解决的问题: □ 如何正确地进行继承方面的设计? □ 最佳的继承层次如何获得? □ 怎样避免所设计的类层次陷人不符合OCP原则的状况? 那如何遵守该设计原则呢? □ 父类的方法都要在子类中实现或者重写,并且派生类只实现其抽象类中声明的方法,而 不应当给出多余的方法定义或实现。 □ 在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期 绑定(动态多态)。 如果A、B 两个类违反了 LSP的设计,通常的做法是创建一个新的抽象类C , 作为两个具 体类的超类,将A 和B 的共同行为移动到C 中,从而解决A 和B 行为不完全一致的问题。 在前面的多态,继承这几节的内容里,已经涉及LSP, 包括使用多态实现隐藏基类和派生 类对象的区别,以及使用组合的方式解决继承中的基类与派生类(即子类)中的不符合语意的 情况。PHP对LSP的支持并不好,缺乏向上转型等概念,只能通过一些曲折的方法实现。对于 这个原则,这里就不再细讲了。 在接n 那节提到了一个缓存的实现接口,试试用抽象类做基类,遵循l ^ P 实现其设计。 这里给出其抽象类代码,如代码淸单2-14所示。 ________________________________代码清单2 - 1 4缓存实现抽象类_________________________________ < ? p h p a b s t r a c t cl a s s C a c h e { * 设置一个缓存变量 * @ pa r a m S t r i n g $ k e y 缓存 K e y 第2 章面向对象的设计原则< ♦ 59 * @ p a r a m m i x e d $ v a l u e 缓存内容 * @ p a r a m int $ e x p i r e 缓存时间 <秒> * @ r e t u r n b o o l e a n 是否緩存成功 */ p u b l i c a b s t r a c t f u n c t i o n s et ($key, $value, $ e x p i r e = 60); /** * 获取一个已经缓存的变量 * @ p a r a m S t r i n g $ k e y 緩存 K ey * @ r e t u r n m i x e d 缓存内容 * / p u b l i c a b s t r a c t f u n c t i o n g e t ( $ k e y ) ; * 刪除一个已经缓存的变量 * @ r e t u r n b o o l e a n 是否删除成功 */ p u b l i c a b s t r a c t f u n c t i o n de l ( $ k e y ) ; * 删除全部緩存变量 * * @ r e t u r n b o o l e a n 是否删除成功 */ p u b l i c a b s t r a c t f u n c t i o n de l A l l (); /** ★检测是否存在对应的缓存 p u b l i c a b s t r a c t f u n c t i o n h a s ( $ k e y ) ; 如果现在要求实现文件、memcache、accelerator等各种机制下的缓存,只需要继承这个抽 象类并实现其抽象方法即可。 现在,再来思考本书开头提到的白马非马的问题,试着用里氏替换原则阐释。 注意LSP中代换的不仅仅是功能,还包括语意。试思考:白马可以代换马,而牛同样作为劳 力,可代换马否?高跟鞋也是鞋子,男人穿高跟鞋又是否能接受? 2 . 1 . 5 依赖倒置原则 什么是依赖倒S 呢?简单地讲就是将依赖关系倒置为依赖接口,具体概念如下: □ 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象(父类不能依赖子类,它们 都要依赖抽象类)。 60 < * PHP核心技术与最佳实践 □ 抽象不能依赖于具体,具体应该要依赖于抽象。 注意,这里的接口不是狭义的接口。 为什么要依赖接口?因为接口体现对问题的抽象,同时由于抽象一般是相对稳定的或者是相 对变化不频繁的,而具体是易变的。因此,依赖抽象是实现代码扩展和运行期内绑定(多态)的 基础:只要实现了该抽象类的子类,都可以被类的使用者使用。这里,我想强调一下扩展性这个 概念。通常扩展性是指对已知行为的扩展,在讲述接口那一节,我也提到,接口应该是相对稳定 的。这就告诉我们,无论使用多么先进的设计模式,也无法做到不需要修改代码即可达到以不变 应万变的地步。在面向对象的这五大原则里,我认为依赖倒置是最难理解,也是最难实现的。 这个例子以前面提到的雇员类为蓝本,实现代码如代码清单2-15所示。 代码清单 2-15 employee, php < ? p h p i n t e r f a c e e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g (); } c l a s s t e a c h e r i m p l e m e n t s e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g () { e c h o 't e a c h i n g ...'; ) } c l a s s c o d e r i m p l e m e n t s e m p l o y e e { p u b l i c f u n c t i o n w o r k i n g () { e c h o 'c o d i n g ...'; } } c l a s s workA{ p u b l i c f u n c t i o n w o r k () { $ t e a c h e r = n e w teacher; $ t e a c h e r - > w o r k i n g (); ) ) c l a s s workB{ p r i v a t e $e; p u b l i c f u n c t i o n set (employee $e) { $t h i s - > e = $ e ; } p u b l i c f u n c t i o n w o r k () { $t h i s - > e - > w o r k i n g (); $ w o r k a = n e w workA; $ w o r k a - > w o r k (); $ w o r k b = n e w workB; $ w o r k b - > s et (new t e a c h e r ()); $ w o r k b - > w o r k (); 第2 章面向对象的设计原则61 在classA中,work方法依赖于teacher实现;在classB中,work转而依赖于抽象,这样可以 把需要的对象通过参数传人。上述代码通过接口,实现了一定程度的解耦,但仍然是有限的。 不仅是使用接口,使用工厂等也能实现一定程度的解耦和依赖倒置。 在workB中,teacher实例通过setter方法传人中,从而实现了工厂模式。由于这样的实现 仍然是硬编码的,为了实现代码的进一步扩展,把这个依赖关系写在配置文件里,指明classB 需要一个teacher对象,专门由一个程序检测配S 是否正确(如所依赖的类文件是否存在)以及 加载配置中所依赖的实现,这个检测程序,就称为IOC容器。 很多文章里看到IOC (Inversion of Control)概念,实际上,IOC是依赖倒置原则(Dependence Inversion Principle, D IP )的同义词。而在提IOC的时候,你可能还会看到有人提起D1等 概念。DI,即依赖注人,一般认为,依赖注入(D I )和依赖查找(D S )是IOC的两种实现。 不过随着某些概念的演化,这几个概念之间的关系也变得很模糊,也有人认为IOC就是DI。有 人认为,依赖注人的描述比起IOC来更贴切,这里不纠缠于这几个概念之间的关系。 在经典的J2EE设计里,通常把DAO层和Service层细分为接口层和实现层,然后在配置文 件里进行依赖关系的配置,这是最常见的DIP的应用。Spring框架就是一个很好的IOC容器, 把控制权从代码剥离到丨0 C容器,这里是通过XML配置文件实现的,Spring在执行时期根据配 置文件的设定,建立对象之间的依赖关系。 如下面代码所示: < b e a n s c o p e * " p r o t o t y p e " c l a s s = " c n .n o t e b o o k .a c t i o n .N o t e b o o k L i s t O t h e r A c t i o n " i d = "n o t e b o o k L i s t O t h e r A c t i o n " > < p r o p e r t y ref = " u s e r R e p l y S e r v i c e " n a m e = " u s e r R e p l y S e r v i c e " / > < p r o p e r t y r ef = " u s e r S e r v i c e " n a m e = " u s e r S e r v i c e " / > < p r o p e r t y ref = " p e r m i s s i o n S e r v i c e " n a m e = " p e r m i s s i o n S e r v i c e " / > < p r o p e r t y r ef = " f r i e n d S e r v i c e " n a m e = " f r i e n d S e r v i c e " / > < / b e a n > 但是这样设置一样存在问题,配置文件会变得越来越大,其间关系会越来越复杂。同样逃脱 不了随# 应用和业务的改变,不断修改代码的恶魇(这里认为配置文件是代码的一部分。并且在 实际开发中,很少存在单纯修改配a 文件的情况。一般配h 文件修改了,代码也会做相应修改)。 在PHP里,也有类似模仿Spring的实现,即把依赖关系写在了配S 文件里,通过配进文件 来产生需要的对象。我觉得这样的代码是还是为了实现而实现。在Spring里,配置文件里配置 的不仅仅是一个类运行时的依赖关系,还可以实现事务管理、AOP、延迟加载等。而PHP要实 现上面的种种特性,其消耗是巨大的。从语言层面讲, PHP这种动态脚本型语言在实现一些多 态特性上和编译型的语言不同。其次PHP作为敏捷性的开发语言,更强调快速开发、逻辑淸 晰、代码简单易懂,如果再附加了各种设计模式的框架,从技术实现和运行效率上来看,都是 不可取的。依赖倒咒的核心原则是解耦。如果脱离这个最原始的原则,那就是本末倒置。 事实上,很多的设计模式里已经隐含了依赖倒置原则,我们也在有意或无意地做着一些依 赖反转的工作。只是作为P H P ,目前还没有一个比较完善的IOC容器,或许是PHP根本不 62 ♦ > PHP核心技术与最佳实践 需要。 如何满足DIP: □ 每个较高层次类都为它所需要的服务提出一个接口声明,较低层次类实现这个接口。 □ 每个高层类都通过该抽象接口使用服务。 2. 2 一个面向对象留言本的实例 在这一节,用面向对象的思想完成一个简单的留言本模型,这个模型不涉及实际的数据库 操作以及界面显示,只是一个demo,用来演示面向对象的一些思维。 在面向过程的思维里,要设计一个留言本,一切都将以留言本为核心,抓到什么是什么, 按流程走下来,即按用户填写信息— 留言— 展示的流程进行。 现在用面向对象的思维思考这个问题,在面向对象的世界,会想尽办法把肉眼能看见的以 及看不见的,但是实际存在的物或者流程抽象出来。既然是留言本,那么就存在留言内容这个 实体,这个留言实体(domain)应该包括留言者的姓名、E-mail、留言内容等要素,如代码淸 单2-16所示。 _________________________代码清单2 - 1 6留言实体类message.php c l a s s m e s s a g e { p u b l i c $name; / /留言者姓名 p u b l i c $email; / /留言者联系方式 p u b l i c $con t e n t ; // 留言内容 p u b l i c f u n c t i o n 一 se t ($name, $value) { $ t h i s - > $ n a m e = $ value; } p u b l i c f u n c t i o n 一 g e t ($name) { i f (!i s s e t ( $ t h i s - > $ n a m e ) ){ $ t h i s - > $ n a m e = N U L L ; 上面的类也就是所说的domain,是个真实存在的、经过抽象的实体模型。然后,需要一 个留言本模型,这个留言本模型包括留言本的基本厲性和基本操作,如代码淸单2-17所示。 __________________ 代码清单2-17 留言本模型g b o o k M o d e l .p h p _____________________ I it-k * 留言本模型,负责管理留言本 * $ b o o k P a t h :留言本属性 V c l a s s g b o o k M o d e l { p r i v a t e $ bookP a t h ; / / 留言本文件 p r i v a t e $ d a t a ; // 留言数据 第2章面向对象的设计原则♦> 63 p u b l i c f u n c t i o n s e t B o o k P a t h ($bookPath) { $t h i s - > b o o k P a t h = $book P a t h ; p u b l i c f u n c t i o n g e t B o o k P a t h () { r e t u r n $ t h i s - > b o o k P a t h ; ) p u b l i c f u n c t i o n o p e n {) { ) p u b l i c f u n c t i o n c l o s e () { } p u b l i c f u n c t i o n re a d () { r e t u r n f i l e _ g e t _ c o n t e n t s ($this - > b o o k P a t h ) ; } / /写入留言 p u b l i c f u n c t i o n w r i t e ($data) { $ t h i s - > d a t a ≫ s e l f : :safe ($data) - > n a m e . " & " . s e l f :: s a f e ($data) - > e m a i l “ " \ r \ n s a i d : \r\ n " .s e l f ::s a f e ( $ d a t a ) - > conte n t ; r e t u r n f i l e _ p u t _ c o n t e n t s ( $ t h i s - > b o o k P a t h , $ t h i s - > d a t a , F I L E _ A P P E N D ) ; ) I I模拟數据的安全处理,先拆包再打包 p u b l i c s t a t i c f u n c t i o n s a f e ($data) { $ r e f l e c t ≫ n e w R e f l e c t i o n O b j e c t ( $ d a t a ) ; $ p r o p s ■ $ r e f l e c t - > g e t P r o p e r t i e s (); $ m e s s a g e b o x = n e w s t d C l a s s (); f o r e a c h ($props as $ prop) { $ i v a r = $ p r o p - > g e t N a m e (); $ m e s s a g e b o x - > $ i v a r ■ t r i m ($prop - > g e t V a l u e ( $ d a t a ) ); } r e t u r n $ m e s s a g e b o x ; ) p u b l i c f u n c t i o n d e l e t e () { f i l e _ p u t _ c o n t e n t s ($this - > b o o k P a t h , 'it *s e m p t y n o w * ); } 实际留言的过程可能会更复杂,可能还包括一系列准备操作以及Log处理,所以应定义一 个类负责数据的逻辑处理,如代码淸单2-18所示。 ____________________ 代码清单2 - 1 8留言本业务逻辑处理leaveModel.php___________________ c l a s s l e a v e M o d e l { p u b l i c f u n c t i o n w r i t e (gbookModel $gb, $data) { $ b o o k * $ g b - > g e t B o o k P a t h (); 64 ♦ > PHP核心技术与最佳实践 $ g b - > w r i t e ( $ d a t a ) ; / /记录曰志 最后,通过一个控制器,负责对各种操作的封装,这个控制器是直接面向用户的,所以 包括留言本査看、删除、留言等功能。可以形象理解为这个控制器就是留言本所提供的直接 面向使用者的功能,封装了操作细节,只需要调用控制器的相应方法即可,如代码清单2-19 所示。 ____________________________代码清单2 - 1 9 前螭控制部分代码____________________________ c l a s s a u t h o r C o n t r o l { p u b l i c f u n c t i o n m e s s a g e ( l eaveModel $ l , g b o o k M o d e l $ g , m e s s a g e $data) { / /在留言本上留言 $1 - > w r i t e ( $ g , $ d a t a ) ; } p u b l i c f u n c t i o n v i e w (gbookModel $g) { / /查看留言本内容 r e t u r n $g - > r e a d (); p u b l i c f u n c t i o n d e l e t e ( g bookModel $g) { $ g - > d e l e t e (); e c h o s e l f :: v i e w ($g); 测试代码如下所示: $ m e s s a g e = n e w m e s s a g e ; $ m e s s a g e 一 > n a m e - • p h p e r *; $ m e s s a g e - > e m a i l ■ ' p h p e r @ p h p . n e t '; $ m e s s a g e - > c o n t e n t ■ *a c r a z y p h p e r l o v e p h p so muc h . •; $ g b * n e w a u t h o r C o n t r o l (); / /新建一个留言相关的控制器 $ p e n ≫ n e w l e a v e M o d e l (); // 拿出笔 $ b o o k = n e w g b o o k M o d e l (); // 翻出笔记本 $ b o o k - > s e t B o o k P a t h (wg : W b a k W t e m p W t e m p c o d e W a . t x t " ) ; $ g b - > m e s s a g e ( $ p e n , $ b o o k , $ m e s s a g e ) ; e c h o $g b - > v i e w ( $ b o o k ) ; $g b - > d e l e t e ( $ b o o k ) ; 这样看起来是不是比面向过程要复杂多了?确实是& 杂了,代码量增多了,也难以理解 了。似乎也体现不出优点来。但是你思考过以下问题吗? □ 如果让很多人来负责完善这个留言本,一部分负责实体关系的建立,一部人负责数据操 作层的代码,这样是不是更容易分工了呢? 第2 章面向对象的设计原则< ♦ 65 □ 如果我要把这个留言本进一步开发,实现记录在数据库中,或者添加分页功能,又该如 何呢? 要实现上面第二个问题提出的功能,只需在gbookModel类中添加分页方法,代码如下 所示: p u b l i c f u n c t i o n r e a d B y P a g e () { $ h a n d l e = f i l e ( $ t h i s - > b o o k P a t h ) ; $ c o u n t ■ c o u n t ( $ h a n d l e ) ; $ p a g e ■ i s s e t < $ _ G E T [ ' p a g e *])? i n t v a l ($一G E T [ ' p a g e ']) : 1; i f ( $ p a g e < 1 丨I$page > $ count) $ p a g e - 1 ; $ p n u m = 9; $ b e g i n = ($page -1 ) * $pnum; $ e n d - ($begin + $pnum) > $ c o u n t ? $ c o u n t : $ b e g i n + $pnum; for ($i _ $ b e g i n ;$i < $ e n d ; $ i + + ) { e c h o , < s t r o n g > 、$i + 1 , • < / s t r o n g > ', $handle[ $ i ] , ' < b r / > '; ) f o r ( $ i _ l ; $ i < = c e i l ( $ c o u n t / $ p n u m ) ; $ i + + ) { e c h o " < a h r e f ■ ? p a g e ■${!•} > ${i) < / a > " ; 然后到前端控制器里添加对应的action,代码如下所示: p u b l i c f u n c t i o n v i e w B y P a g e (gbookModel $g) { r e t u r n $g - > r e a d B y P a g e (); } 运行结果如图2-5所示。 只需要这么简单的两步,就可实现所需要的 分页功能,而且已有的方法都不用修改,只需在 相关类中新增方法即可。当然,这个分页在实际 点击时是有问题的,因为我没有把Action分开, 而是通通放在一个页面里。对照着上面的思路, 还可以把留言本扩展为MySQL数据库的。 在这个程序里只体现了非常简单的设计模式, 这个程序还有许多要改进的地方,每个程序员心 中都有一个自己的0 0。项目越大越能体现模块划分、面向对象的好处。 思考试着找找这个小程序里体现了哪些设计原則,并且试着加上一些异常处理等。 2 . 3 面向对象的思考 ,≫>“> : . : 1phper|phper#php. net 2phperIphperfphp. net 3phper|phper#php. net 4phper |phper%>hp. net 5phper|phper#php. net 6phper|phper#php. net 7phper|phper#php. net 8phperIphper^php. net 9phper|phper#php. net 1234 a crazy phper love php so much, a crazy phper love php so much, a crazy p如er love php so much, a crazy phper love php so nuch. a crazy phper love php so nuch. a crazy phper love php so nuch. a crazy phper love php so nuch. a crazy pl^er love php so much, a crazy phper love php so nuch. 图2 - 5 程序运行结果 PHP的特色是简单、快速、适用。在PHP的世界里,一切以解决问题为主,所以很多设计 方面的东西往往被忽视或排斥。虽然PHP的面向对象提出很多年了,但一直被排斥,很多人提 倡原生态开发方式,甚至有人提倡彻底面向过程。伴随着对0 0 的质疑,PHP框架一方面如雨 后春笋般遍地开花,另一方面一直受到抵制和质疑。 有一点是肯定的,PHP不是一门很好的面向对象的语言,因为其无法做到完全面向对象, 也无法优雅实现面向对象。所以现在比较流行的还是以类为主的开放方式,即抛弃或梢简经典 的MVC理论,很少用和几乎不用设计模式,以类加代码模块的方式进行代码组织。这种开发 方式在PHP的开源项目里是最流行的,也是最适合二次开发的,而比较纯的面向对象的产品有 Zend Framework。这类产品人门的门檻比较高,代码看似“臃肿”,开发成本比较高,这类产品 —般比较少见,市场占有率也比较低。 所有产品最终都是为市场服务的,PHP面向的是Web开发市场,所以并不需要高端的、复 杂的设计和开发技巧。但是前面讲的那些并不是没有作用。 □ 一些基本理论,在任何一门语言里都有共性。语法和函数库只是学好一门语言的必要条 件,而不是充要条件。语法和函式只是表层的东西。只要掌握面向对象的思想,即使没 有一点】ava和.NET基础,也能看懂用它们写成的代码。 □ PHP只是一个脚本语言、一门工具而已。在Web开发中,PHP语言自身所占的分量越 来越低,但却涉及程序设计的方方面面,而面向对象只是其中之一,也是最主要的一个 方面。PHP是一种经典思想,能实现低耦合、易扩展的代码,其可用最经济的方式干一 件事。 □ 理论是重要的,但是理论也不是一成不变的。比如我们提到的一些设计模式,也没必要 完全遵守,可以做一些精简和变形。 基于以上思考,我们认为在PHP的开发中应该灵活使用面向对象的特性和设计原则。 对于流程明确、需求清晰、需求变更风险小的业务逻辑,过程化开发(传统软件开发模 式)鉍适合,这就像解一道数学题,总需要一步步去解,上一步的结果作为下一步的条件。这 个时候,面向过程的开发更符合人的思维。 但是对于流程复杂、需求不完善、存在很大需求变更风险的业务逻辑,此时用过程化开发 将使程序变得非常的繁琐臃肿,实现难度很大,并且后期的维护代价高得惊人。此时,抽象思 维将是最适合的,用面向对象的思维去抽象业务模型并随需__________求不断精化,最终交付使用,其扩 展度和可维护性都要比过程化方法更好。 由于面向对象是更高一层的抽象,它有一些优点较之面向过程是比较突出的: 其一,新成员的加人和融合不再困难,高度抽象有利于高度总结。 其二,代码即文档,团队中的任何人都可以轻松地获得产品各个模块的基本信息,而不再 需要通读大部分代码。 说到这里,可能就会有人有疑问了:本书一直在推崇面向对象的开发模式,说面向对象的 好,说0 0 适合复杂的项目,那Linux这种复杂的项目,使用面向过程的C 语言编写的,这又如 何解释? 这个问题问得好,现解释如下: 66 < ♦ PHP核心技术与最佳实践 第2 章面向对象的设计原则♦ > 67 其一,Linux虽然是用面向过程的C语言编写的,但是Linux的操作系统是使用内核+ 模块 的方式构建的,这种模块化的思想是所有编程范式中的普适原则。 其二,面向对象和各种设计模式就是已经提供好的模式,使用已有的模式本比像Linux那 样自己摸索出一个模式更方便快捷,开发成本更低,代码更易阅读。 其实,面向过程也好,面向对象也好,目的只有两个:一个是功能实现,一个是代码维护 和扩展。只要能做好这两点,那就是成功的。 PHP不是一门很好的OOPL,但却是一门很好的Web设计语言。我们有理由相信,在Web 开发领域,PHP还将继续发挥其作用,以其简单、快速吸引更多的开发者加人。 2. 4 本章小结 本章主要讲解面向对象设计的五大原则,穿插一些设计模式的例子。在第1章的最后提到 面向对象的设计思想存在一些问题,其本质在于面向对象强调对现实的建模,而现实和开发中 并没有一一对应,因此五大原则和设计模式就是对o o 的补充。 最后一节给出的留言本demo,只是一个很小的模型。一般来说,越是规模较大的项H , 越 能体现设计模式的前睹性和必要性。 可能很多读者对一些设计模式有不同的见解和困惑,这是正常的。一段代码往往很难明确 地归属于某一种设计模式,其可能有多种设计模式的影子。设计模式只是一种成熟的、可供借 鉴的思考模式,而不是公式。 我们既要深人了解面向对象的思想,又不能执着于面向对象。 第3 章正则表达式基础与应用 正则表达式起源于科学家对人类神经系统工作原理的早期研究。美国新泽西州的Warren McCulloch和出生在美国底特律的Walter Pitts这两位神经生理方面的科学家,研究出一种用数 学方式来描述神经网络的新方法,他们创新地将神经系统中的神经元描述成小而简单的自动控 制元,从而做出一项伟大的工作革新。后来,数学科学家Stephen Kleene在Warren McCulloch和 Walter Pitts早期工作的基础之上发表一篇论文,题目是《神经网事件的表示法》,书中利用正 则集合的数学符号描述此模型,引人正则表达式的概念。 3. 1 认识正则表达式 正则表达式就是用某种模式去匹配一类字符串的一种公式。通俗地讲,就是用一个‚字符 串” 描述一个特征,然后验证另一个“字符串” 是否符合这个特征的公式。 比如“ab + ” 描述的特征是:一个a 和任意个b。那么ab、abb、abbbbbbbbbb都符合这个 特征,而字符串ad显然是不符合的。 正则表达式可应用到各个方面。在常用的高级编辑器中,几乎都支持正则表达式,如 Wod、EditPlus、UltraEdit、Vim 等。 正则表达式在编程语言中更是得到大规模推广。现在的语言几乎都是原生的,都可从语法 上支持正则表达式,尤其在Perl的推动下,PHP、Java, .NET, JavaScript等语言都支持丰富 的正则语法;不支持的可以通过一些包实现扩展。每种语言中对正则表达式的支持有所不 同,其中Perl和.NET对正则表达式的支持最为强大,而JavaScript对正则表达式的支持则比 较“朴素‛。 注意本节所讲的一些特性,并不是在所有语言中都支持。 3.1.1 PHP中的正则函数 正则表达式看起来总是那么古怪,以至于许多人对其望而生畏。首先要澄清一些概念:虽 然不同语言间正则语法大同小异,但实际上正则表达式的实现有多种引擎(如非确定性有穷自 动机NFA、确定性有穷自动机DFA),其表现又有多种风格(如JavaScript有自己的朴素正则、 Perl有一套高级而强大的正则、.NET也有自己的一套正则风格)。另外,还有人可能容易混淆 PHP 中的 preg 和 ereg。 简单地说,PHP中有两套正则函数,两者功能差不多: 第3 章正则表达式基础与应用< ♦ 69 1 ) 由PCRE库提供的函数,以“preg_” 为前缀命名。 PCRE ( Perl Compatible Regular Expression, 兼容 Perl 的正则表达式)由 Philip Hazel 于 1997 年开发。现代的编程语言和软件中一般都使用PCRE库。 2 ) 由POSIX扩展提供的函数,以“ereg_” 为前缀命名。 POSIX ( Portable Operating System Interface of UNIX, UNIX 可移植操作系统接口)由一系列 规范构成,定义了 UNIX操作系统应支持的功能,所以“ POSIX风格的正则表达式‛也就是 ‚关于正则表达式的 POSIX规范” , 定义了 BRE ( Basic Regular Expression, 基本型正则表达式) 和ERE ( Extended Regular Express, 扩展型正则表达式)两大流派。通常UN丨X 的一些工具和较 老的软件中会使用POSIX风格的正则。另外,一些数据库中也提供了 POSIX风格的正则表 达式。 自PHP 5. 3 以后,就不再推荐使用POSIX正则函数库,若程序中使用了则会报Deprecated 级别的错误,这种情况通常在一些较老的代码中比较常见。其实使用或不使用POSIX正则函数 库二者本质上没多大差别,主要是一些表现形式、语法和扩展功能的差别。 3 . 1 . 2 正则表达式的组成 在Windows资源管理器中査找文件以及批处理文件时,可使用通配符‚?‛ 和表示 匹配一组字符,这和正则表达式类似,“?” 表示一个不确定的字符,而则表示任意多个 不确定字符。比如下面是删除本地垃圾文件批处理的部分代码: d e l /f /s / q % s y s t e m d r i v e % \* . t mp d e l /f /s / q % s y s t e m d r i v e % \ * ._mp d e l /f /s / q % s y s t e m d r i v e % \ * .log d e l /f /s / q % s y s t e m d r i v e % \* .gid d e l /f /s / q % s y s t e m d r i v e % \ * .chk d e l ft /s / q % s y s t e m d r i v e % \ * .old 需要注意的是,这里的“?” 和称为“通配符”,而不是正则表达式。 在PHP里,一个正则表达式分为三个部分:分隔符、表达式和修饰符。 □ 分隔符:可以是除了字母、数字、反斜线及空白宇符以外的任何字符(比如/ 、!、# 、% 、 I 、-等)。经常使用的分隔符是正斜线(/ ) 、hash符号(# ) 以及取反符号(~ ) 。考虑到可 读性,为了避免和反斜线混淆,一般不使用正斜线做分隔符。 □ 表达式:由一些特殊字符和非特殊的字符串组成,比如“ [a-z0-9_-] +@ [a-zO-9_-.] + ” 可以匹配一个简单的电子邮件字符串。 □ 修饰符:用于开启或者关闭某种功能/模式。 3 . 1 . 3 测试工具的使用 在学习过程中,建议下载RegexTester工具验证和测试正则表达式,也可使用Firefox的扩展 Regular Expression Tester进行测试,其界面如图3-1所示。 70 < ♦ PHP核心技术与最佳实践 雄.R≪gutar depressions Tester [-.^|i^^^BL3il j\d{3)| B 大小与注意R G l o b a l圉Multiline G 代费 fffff 555 5t6yyh7 L ≪ s a ^ l la _ ssl Duration: 0 ms Matches: 1 图 3-1 Firefox 的扩展 Regular Expression Tester 本书以后测试都将利用此工具进行,而不再写PHP代码测试。 注意这个工具测试的代码不一定能在PHP中通过,反之PHP中合法的正则表达式在此工具 里也不一定能测试通过。其中的道理前面已经讲过了,不同语言实现的正则表达式略有 区别。 下面,就来开始最简单的正则表达式人门的介绍。 3 . 2 正则表达式中的元字符 假设要在一篇文章里查找“he”,可以使用正则表达式“he‛。这几乎是最简单的正则表达 式,它可以精确匹配这样的字符串:由两个字符组成,前一个字符是“ h”,后一个是“ e‛。通 常,处理正则表达式的工具会提供一个忽略大小写的选项,如果选中这个选项,它可以匹配 “he”、“HE"、“He” ' “hE” 这四种情况中的任意一种。 但是很多单词里包含“he” 这两个连续的字符,比如“ her”、“ heet” 等。用“ he‛来查 找,这些单词中的“ he” 也会被找出来。如果要精确地查找“ he” 这个单词,应该使用以下 形式: \bhe\b “\b” 是正则表达式规定的一个特殊代码,代表单词的开头或结尾,也就是单词的分界处。 第3 章正则表达式基础与应用< ♦ 71 虽然通常英文单词是由空格、标点符号或者换行来分隔,但是“\b‛并不匹配这些单词分隔字 符中的任何一个,它只匹配一个位置。 “\b” 匹配位置的精确说法:前一个字符和后一个字符不全是(一个是,一个不是或不存 在)“w ” 0 假如要找“he” 后面不远处跟着一个“is”,应该表示如下: \ b h e \ b . * \ b i s \ b 这里,点号(.)是元字符,匹配除了换行符以外的任意字符。同样是元字符,不过 它代表的不是字符,也不是位置,而是数量— 它指定前边的内容可以连续重复使用任 意次以使整个表达式得到匹配。因此, 和连在一起就意味着任意数量的、不包含换 行的字符。现在,“\bhe\b. * \bis\b” 的意思很明显:先是一个单词he, 然后是任意个任意字 符(但不能是换行符),最后是is这个单词。 3. 2. 1 什么是元字符 元字符(Meta-Characters)是正则表达式中具有特殊意义的专用字符,用来规定其前导字 符(即位于元字符前面的字符)在目标对象中的出现模式。通过前面的例子,我们已经知道几 个很有用的元字符。正则表达式里有很多元字符,常用元字符如表3-1所示。 表3 - 1 常用元字符 元字符描 述 匹配除换行符以外的任意字符 \w 四配宇母或数宇或下划线或汉宇 \s 匹配仟意空白符 \d 匹配数字 \b 匹配单词的开始或结束 - 匹配字符串的开始 $ 匹配宇符串的结束 - 表示范闹 [] 匹配括号中的任意一个宇符 * 、+ 、? 设词 下面看一些例子。 1 ) 匹配以字母“a” 开头的单词: \ b a \ w * \b 以上表达式先是某个单词开始处(\ b ) ,然后是字母“ a‛,接着是任意数量的字母或数字 ( \w * ) , 最后是单词结束处(\b), 匹配的单词如adandon、action、a 等。 2 ) 匹配1个或更多连续的数字: \d + 72 PHP核心技术与最佳实践 以上表达式可以匹配0、1、555等。这里的元字符+和* 类似,不同的是, * 匹配重复任 意次(可能是0 次),而+则匹配重复1次或更多次。 3 ) 匹配刚好6 个字符的单词: \ b \ w { 6 }\b 以上表达式匹配action、123^6、ste_ph等。 注意正则表达式里“单词” 指不少于丨个的连续字母和数字。 如果同时使用其他元字符,则能构造出功能更强大的正则表达式。比如下面这个例子: 0 \ d \ d - \ d \ d \ d\d\d\d\d\d PC.配字符串:以0 开头,然后是2 个数字,1个连字符,最后是8 个数字,也就是中国部 分地区的电话号码,如010-12345678。 这里“\d” 是元字符,匹配1 位数字(0、丨、2……)。不是元字符,只匹配它本 身— 连字符(或者减号,或者中横线,或者随你怎么称呼它)。 为了避免那么多烦人的重复,也可以这样写这个表达式: 0\d{2) - Vd{8> 这里后面|2丨和1 8 1的意思是,前面\d必须连续重复匹配2 次和8 次。 思考题使用 “he”、“\bhe\b” 分别查找句子 uhe is a good student , the most proud of his mother. With him, she hold the hope.” 有多少种匹配结果? 下面重点介绍几个常用元字符。 3 . 2 . 2 起始和结束元字符 元字符中有两个用来匹配位置: 匹配字符串的开始。 □ $:匹配字符串的结束。 元宇符〜‛、“S” 与“\b” 有点类似。匹配字符申的丌头,“$ ” 匹配结尾。这两个 代码在验证输人内容时非常有用,比如某网站如果要求填写QQ号必须为5 ~ 11位数字时,可 以使用: A\d{5,H) 5 这里15,11丨表示重复次数不能少于5 次,不能多于11次,否则都不匹配。因为使用 和‚ $‛,所以输人的整个字符串都要和\(1|5,1丨1匹配。也就是说,整个输人必须是5~ 1 1个数 字,如果输人QQ号能匹配这个正则表达式,就符合要求。如果输人含有5~ 1 1个数字,但不 是完整数字串,而只是一串字符的一部分,也不能匹配成功,如图3-2所示。 第3 章正则表达式基础与应用♦ > 73 酬 -\d{5rU}$ □ 大小写注意圉Global R Multiline 0 代《 363575104 q c ^ 3 6 3 5 7 5 1 0 4 Cli m y qq is 33575104.| ▼j 结果 3635751041 q q S 3 6 3 5 7 5 1 0 4 f q q is 33575104. 图3 - 2 正则表达式匹配结果 从图中就能清晰地看出的确切含义。我想,你也能猜测到它和正则表达式 \d|5,ll|的区别。为了加深印象,分别使用下面4 个正则表达式看一下效果: A \ d { 5 , l l } $ / /匹妃起始和结束位置都是數字的,且连续5 ~ 1 1 位 \ d { 5 , l l } $ / / 匹配结束位置是數字的,且连续5 ~ 1 1 位 A \ d { 5 , l l丨 / / 匹釔起始位置是數字的,且连续5 ~ 1 1 位 \d{5,ll) / /匹配连续的5 ~ 1 1 位教字 很自然,在一行中,前三个正则表达式结果只可能有一个匹配结果,而最后一个正则表达 式则可以有多个匹配成功的结果。因为一行只可能有一个开始位置和一个结束位置。 注意我们在正則表达式处理工具处勾选Multiline选项,即多行选项,-和$的意义就变成匹配 行的开始处和结束处,否则将把整个输入视作一个字符串,忽视换行符。可以试着把多 行选项去除后再看看效果。如果用过Vim编辑器,就知道命令“ d*” 和“ d $ ” 的作 用了。 3 . 2 . 3 点号 点号(.)是使用频率最高的元字符。例如,在做采集时抓取页面,要匹配某DIV里的内 容,就需要用到点号匹配。下面代码是抓取本地HTML页面的一部分: < h t m l x m l n s = "h t t p : / / w w w . w 3 . o r g / 1 9 9 9 / x h t m l” 〈h e a d p r o f i l e = "h t t p : / / g m p g . o r g / x f n / l l " > < m e t a h t t p - e q u i v ■ " c o n t e n t - t y p e " c o n t e n t ■ < t i t l e > 我的博客 < / t i t l e > 'text/html; c h a r s e t - U T F - 8 " / : 要匹配这个网页的标题应该怎么办呢?很简单,使用点号匹配全部字符,如下: 74 ♦> PHP核心技术与最佳实践 < t i t l e > .* < \ / t i t l e > 这样就可以抓取你想要的任何内容了,包括DIV、SPAN等。 思考题延伸思路,是不是还可以抓取页面的字符集?要判断这个页面有多少张图片是不是也 很容易?只要找到特征字符就可以。试一下,看看和预想的结果是否一致。 \dj5,ll丨等都应用了量词。正则表达式中的 表3 - 2正则表达式中的量词 3. 2. 4 量词 前面实际上已经涉及量词的概念,比如\d + 量词如表3-2所示。 下面是一些例子: 1) 匹配Windows后面跟丨个或更多数字: W i n d o w s \ d + 2 ) 表示index后面紧跟0 个或1个数字,: i n d e x \ d ? 以上表达式匹配index、indexl、index9这 样的文件名,但不匹配indexlO、indexa这样的 文件名。 3 ) 匹配一行第一个单词(或整个字符串第一个单词,具体匹配哪种,得看选项设置) A\w + 限定符代码/语法描 述 * 重复0 次或更多次 + $ 复1 次或史多次 ? 重复0 次或1 次 |n| 重复n 次 |n,| 重复n 次或更多次 |n,m| 重复n 到m 次 提示在学习量词的过程中,要注意* 和?这两个量词。前面提到过通配符的概念,通配符里 也有这两个符号,要注意它们之间的区别。 3 . 3 正则表达式匹配规则 我们已经学习“ * ”、“?” 等元字符,它们都有各自的特殊含义。如果想匹配没有 预定义元字符的字符集合,或者表达式和已知定义相反,或者存在多种匹配情况,应该怎么 办?本节就介绍几种常用匹配规则。 3. 3. 1 字符组 查找数字、字母、空白很简单,因为已经有了对应这些字符集合的元字符,但是如果想匹 配没有预定义元字符的字符集合(比如元音字母a、e、i、o、u ) , 方法很简单,只需要在方括 号里列出它们。 第3 章正则表达式基础与应用75 例如[aeimi]匹配任何一个英文元音字母, [ . ? ! ] 匹配标点符号( “?” 或‚!‛), c[aOU]t匹配“cat”、“cot”、“cut” 这三个单词,而"caout"则不匹配。 注意[]匹配单个字符,尽管看起来[]里有好多字符。 也可以指定字符范围,例如[0 - 9 ]的含意与\0)完全一致:代表一位数字;同理[a-zO- 9 A - Z J完全等同于\w (如果只考虑英文)。 字符组很简单,但是一定要弄清楚字符组中什么时候需要转义。 3 . 3 . 2 转义 如果想查找或匹配元字符本身,比如查找* 、?等就出现问题:没办法指定,因为它们会 被解释成别的意思。这时就使用\来取消这些字符的特殊意义。因此,应该使用\.和\ *。当 然,查找\本身用这叫做转义。 通俗地讲,转义就是防止特殊字符被解析,或者说用某个符号表示另一个特殊符号。例 在JavaScript或者PHP中都接触过转义的概念。例如,JavaScript中要弹出一个对话框,对 话框中需要分成两行显示,用HTML的< ^ > 标签或者在源代码里手工换行都不行,应该用\r\n 表示换行并新起一行,如下所示: 在PHP里使用反斜杠(\)表示转义,\(?和\£也可以在模式中忽略正则表达式元字符, 比如: 以上表达式先匹配一个或多个数字,紧接着一个点号,然后一个$ , 再然后一个点号,最 后是字符串末尾。也就是说,\(?和化之间的元字符都会作为普通字符用来匹配。 正则表达式是不是遇到这些特殊字符就该转义呢?答案显然是否定的。转义只有在一定条 件下,比如可能引起歧义或者被误解析的情况下才需要。有些情况并不需要转义这些“特殊” 字符,并且在时转义也是无效的。这需要不断尝试并积累经验。看一个例子: < ? p h p $ r e g • "#[ a b y \ } ]#"; $ s t r = ' a \ b c [ ] {}'; p r e g j n a t c h一a l l ( $ r e g , $str, $ m ) ; v a r一d u m p ($m ) ; 如:unibetter\.com 匹配 unibetter.com,C : \\Windows 匹配 C : \Windows。 a l e r t ("警告: 操作无效 a l e r t ("警告< b r > 操作无效 a l e r t ("警告\ r \ n操作无效">; / /措误 / /钳误 / /正确写法 \d + \Q.$.\E $ 在字符组中匹配“a”、“b” 、“ y” 和“ I ” 中任意一个,由于“ I ” 是元字符,具有特殊 意义,所以这里进行转义,使用“ \丨” 表示“ I‛。 但是实际上,这个转义是多余的。虽然“ I ” 是元字符,具有特殊意义,但是在字符组中, “ I ” 却无法发挥意义,不会引起歧义,所以不需要转义。在这里“\丨” 和“ |” 是等价的。 既然转义符是多__________余的,那么会不会被当作普通字符呢?字符串$str里有.‚\‛,但是 可以从代码运行结果中看出, “\” 字符并没有被匹配,也就是说正则表达式“#[abc\!]#" 中,虽然转义符是多余的,但是也并没有被当作普通字符进行匹配。 如果确实要把当作普通字符匹配,正则表达式需要写成: #[ )ab\\Vy] # 前面提到,不是所有出现特殊字符的地方都要转义。例如,以下正则表达式可以匹配 “cat‛、“c?t‛、“c)t” 等字符: c [ a o u ? * ) ] t 其中“?” 和等特殊字符都不需要转义。原因很简单,字符组里匹配的是单个字符, 这些特殊字符不会引起歧义。 字符组里可以使用转义吗?可以,例如“C[\d]d” 可以匹配“ cld”、“ c2d” 等。下面是 复杂的表达式: \(? 0\d{2)[) -]? \d(8( “(” 和“ )” 也是元字符(后面在分组章节会提到),所以在这里需要使用转义。这个表 达式可以匹配几种格式的电话号码,例如(010)88886666、022-22334455或02912345678等。 首先是转义符“ U ‛,表示出现0 或丨次(?),然后是一个0 , 后面跟着两个数字(\d|2| ), 然后是“)”、或空格中的一个,出现1次或不出现(?),最后是八个数字(\d|8|)。 3 . 3 . 3 反义 有些时候,査找的字符不属于某个字符类,或者表达式和已知定义相反(比如除了数字以 外其他任意字符),这时需要用到反义。常用反义如表3-3所示。 76 ♦ > PHP核心技术与最佳实践 表3 - 3 常用反义 常用反义描 述 \w 匹配仟葸不是字母、数字、下闸线、汉字的字符 \s 匹配任意不是空白符的字符 \D 匹配任意非数字的字符 \B 匹配不是单词开头或结束的位孜 [、] 匹配除了x以外的任意字符 [Aaeiou ] 匹配除了 aeion这儿个字母以外的任意字符 反义有一个比较明显的特征,就是和一些已知元字符相反,并且为大写形式。比如“\d” 表示数字,而“ \D ” 就表示非数字。看一些实际的例子。 1 ) 不包含空白符的宇符串: 第3 章正则表达式基础与应用♦ > 77 2 ) 用尖括号括起来、以a 开头的字符串: ] + > 比如,要匹配字符串“ 百度< / a > ‛, 这个正则表达式匹配 的结果就是 “ < a Href = " http:Abaidu.com" > ‛。 提示在这里是“非” 的意思,不要和表示开头的混淆。那怎么区分呢?很简单, 表示开始位置的只能用在正則表达式最前端,而表示取反的只用在字符组 中,即只在中括号里出现。记住这一点,就不会搞混了。 日常工作中反义用得不多,因为扩大了范围。例如程序里的变景,第一个字符不允许是数 字,一般使用“-[a-ZA-Z_]” 表示,而不会使用“\D”,因为“\D” 扩大了范围,包括所有非 数字的字符,敁然,变量命名不仅仅要求第一个字符不是数宇,也不能是其他除了 26个大小写 字母和下画线以外的字符。因此,不要随意使用反义,以免无形中扩大范围,而使自己没有考 虑到。 3 . 3 . 4 分支 分支就是存在多种可能的匹配情况。例如,匹配“cat” 或者“hat”,可以写成[ch]at; 要 匹配“cat”、“ hat”、“fat”、“ toat” ,很显然不能用字符组匹配的方式。这里表明前面的匹配字 符可以是c、h、f 或者t o ,而[ ] 只能匹配单个字符,此时可用分支形式,BP: (c I h I f I t o ) a t 其中括号里的表达式将视作一个整体(后面会讲到分组的概念),“ I ” 表示分支,即可能 存在的多种情况,可以匹配多个字符。分支的功能更强大,字符组方式只能对单个字符‚分 支” ‟ 而分支可以是多个字符以及更复杂的表达式。但对于单字符的情况,字符组的效率更高。 也就是说,能使用字符组就不用分支。 看到这里,你可能会有疑问:表达式“ [ch]at” 括号里面是可能的匹配,分支也是表示可能 的匹配,那么“ [ch]at” 是否可以写成“(clh)at” 呢?答案显然是可以的,a [ch]at = (clh)at"0 注意括号匹配会捕获文本,如果不需要捕获文本,上面的例子可以使用‚(?:)‛,后面还会 讲到。 正则表达式分支条件指有几种规则,无论满足其中哪一种规则都能匹配,具体方法是用 “ I ” 把不同规则分隔开,例如: 0\d(2) - \ d ( 8 ) |0\d(3) - \ d { 7 ) 这个表达式能匹配两种以连字号分隔的电话号码:一种是3 位区号,8 位本地号(如(MO- 12345678), 一种是4 位区号,7 位本地号(如0376-2233445)。匹配3 位区号的电话号码表达 78 ♦♦♦ PHP核心技术与最佳实践 式_______如下: \(0\d{2}\)[ - ]? \d{8) | 0\d{2}[ - ]? \d{8} 其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔, 也可以没有间隔。可以试试用分支条件把这个表达式扩展成同时支持4 位区号。 例如,美国邮编规则是5 位数字,或者用连字号间隔的9 位数字。匹配表达式如下: \d{5) - \ d { 4 } I \d{5} 另外,使用分支条件时,要注意各个条件的顺序。如果改成以下形式,就只匹配5 位邮编 以及9 位邮编的前5 位: \d{5) | \cM51 注意匹配分支条件时,将从左到右测试每个条件,如果满足某个分支,就不会再考虑其他 条件。 3. 3. 5 分组 重复单个字符只需要直接在字符后面加上限定符,但如果想重复多个字符又该怎么办呢? 可以用小括号指定子表达式,然后规定这个子表达式的重复次数,也可以对子表达式进行其他 一些操作。这就是本节介绍的分组,常用分组语法如表3-4所示。 表3 - 4 常用分组语法 类别代码/语法描 述 (exp) 匹配exp,并捕获文本到自动命名的组里 捕获( ? < name > exp) 匹配e x p ,并捕获文本到名称为name的组里,也可以写成(?• name • exp) ( ? :exp) 匹配e x p ,不捕获K 配的文本,也不给此分组分配组号 (? = exp) 匹配exp前面的位置 零宽断言 (? <=exp) 匹配exp后面的位置 ( ? ! exp) 匹配后面跟的不是exp的位置 ( ? < ! exp) 匹配前面不是exp的位置 注释( ?#commenl) 提供注释辅助阅读,不对正则衣达式的处理产生任何影响 例如,简单的IP地址匹配表达式如下: ( \ d ( l , 3 ) \.){3)\d{l,3≫ 要理解以上表达式,应按下列顺序分析: 1 ) 匹配1~ 3位的数字: \d(l,3J 2 ) 匹配3位数字加上1个英文句号(分组),重复3 次(最后加上一个1 ~3位的数字): 第3 章正则表达式基础与应用♦ > 79 ( \ d ( l , 3 > \ . ) {3} IP地址中每个数字都不能大于255,所以严格来说这个正则表达式是有问题的。因为它将 匹配256. 300. 888. 999这种不可能存在的IP地址。如果能使用算术比较,或许能简单地解决这 个问题,但是正则表达式中没有提供关于数学的任何功能,所以只能使用冗长的分组、选择、 字符类来描述一个正确1P地址,如下所示: ((2[0 - 4 ] \ d | 2 5 [ 0 - 5 ] |[01]? \d\d?)\.){3) (2[0 - 4 ] \ d |25[0 - 5 ] |[01]? \d\d?) 思考题理解这个表达式的关键是理解“2[0-4]\dl25[0-5]l[01]?\d\d?‛,读者应该能分析出 它的意义。 默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标 志,第一个出现的分组,其组号为1 ,第二个为2 , 以此类推;分组0 对应整个正则表达式。 也可以自己指定子表达式的组名,语法如下: ? < W o r d > \w + 把尖括号换成单引号也行,如下所示: ? 'Word * \ w + 这样就把W + 组名指定为Word。 提示组号分配远没有这么简单。组号分配过程是要从左向右扫描两遍:第一遍只给未命名组 分配,第二遍只给命名组分8己。因此,所有命名组的组号都大于未命名的组号。可以使 用语法(?: e x p )剥夺一个分组对组号分配的参与权。 3 . 3 . 6 反向引用 反向引用用于重复搜索前面某个分组匹配的文本。首先看示例,“\1” 代表分组1匹配的 文本: \b (\w + ) \b\s + \1 \b 以上表达式可以匹配重复的单词,例如go go或者kitty kitty。首先这个表达式是一个单词, 也就是单词开始处和结束处之间大于一个的字母或数字,即“\b(\w + )\b‛,这个单词会被捕 获到编号为1的分组中,然后是1个或几个空白符(\s + ) ,最后是分组1 中捕获的内容(也就 是前面匹配的那个单词),BP\1,这样就相当于把所匹配的重复一次。 要反向引用分组捕获的内容,可以使用“ \k ”,所以上个例子也可以写成这样: \b (? < W o r d > \w + ) \b\s + \k < W o r d > \b 例如,要捕获字符串“V ‟T h i s i s a ' string•\≫,>引号内的字符,如果使用以下正则表达式: 80 *1* PHP核心技术与最佳实践 将返回“ "This is a ‟ ”。显然,这并不是我们想要的内容。这个表达式从第一个双引号开始 匹配,遇到单引号之后就错误地结束匹配。这是因为表达式里包含‟‟ I ‟ ,也就是双引号(")和 单引号(‟)均可。要修正这个问题,可以用到反向引用。 表达式“\1,\2,丨,\9” 是对前面已捕获子内容的编号,可以作为对这R 编组的“指针” 引用。在此例中,第一个匹配的引号就由1代表。可以这么写成: ( " |\') .* ? M 如果使用命名捕获组,可以写成: (? P < q u o t e > " I ') .*? ( ? P - q u o t e ) 看PHP使用反向引用的例子。 在很多论坛中都会看到UBB标签代码。UBB标签最早的设计是用来在论坛和留言本里代替 HTML,实现一些简单的HTML效果,同时防止滥用HTML出现安全问题。例如,HTML中粗体 的标签是: < b > 粗体 < /b > 或者: < s t r o n g > 粗体 < / s t r o n g > 而UBB标签则是: [1)]粗体[/13] UBB标签以其更好的安全性,目前已经成为论坛发帖的代码标准,只不过不同论坛产品的 叫法不一样而已。 最终,UBB标签还是要解析成HTML代码,才能ii:浏览器认识。这个过程是怎样实现的 呢?下面以URL标签为例解释。 例如,UBB标签“ [Url] l.g if[ /Url]” 用于插人表情。在解析时,需要把l.gif“换成实际路 径,并且需要用HTML的IMG标签进行替换,方法如下所示: < ? p h p $ s t r - ‟[ u r l ] l . g i f [ / u r l ] [ u r l ] 2 . g i f [ / u r l ] [u r l ] 3 . g i f [ / u r l ] •; $s * p r e g _ r e p l a c e ("# \ [ u r l \ ] (? < W O R D > \d\ . g i f ) \[ \/url\] #", ” < i m g s r c = h t t p ://i m a g e .a i .c om / u p l o a d / $1 > " , $ s t r ) ; v a r 一d u m p ( $ s ) ; 运行结果如下: s t r i n g (141) " < img s r c = h t t p ://i m a g e .a i .o m / u p l o a d / 1 .gif > < im g s r c = h t t p :/ / i m a g e .a i .c o m / u p l o a d / 2 .gif > < img s r c - h t t p : / / i m a g e . a i . c o m / u p l o a d / 3 .gif > " 第3 章正则表达式基础与应用81 是不是很简单? 一个简易表情标签就这样实现了。 这里再给出一个表达式实现同样的效果: < ? p h p $ s t r = ' [ u r l ] l . g i f [ / u r l ] [ u r l ] 2 . g i f [ / u r l ] [ u r l ] 3 . g i f [ / u r l ] *; $ s = p r e g _ r e p l a c e ( " # \ [ u r l \ ] ( • * ? >、[ \ / u r l \ ] # " , " < img sr c = h t t p :/ / i m a g e .a i .c o m / u p l o a d / $ l > ", $ s t r ) ; v a r一d u m p ( $ s ) ; 提示这个正则表达式涉及贪# /懒惰匹配知识,后面会进一步介紹。 3. 3. 7 环视 断言用来声明一个应该为真的事实。正则表达式中,只有当断言为真时才会继续进行匹 配。断言匹配的是一个事实,而不是内容。本节介绍四个断言,它们用于查找在某些内容(但 并不包括这些内容)之前或之后,也就是一个位置(如\b、\ $ ) 应该满足的一定条件(即断 言),因此也称为零宽断言。 1 .顺序肯定环视(?=exp> 零宽度正预测先行断言,又称顺序肯定环视,断言自身出现位置的后面能匹配表达式exp。 比如,匹配以“ ing” 结尾的单词前面部分(除了 “ ing” 以外的部分): \b\w + (? ■ ing\b) 以上表达式查找以下句子时,会匹配“sing‛和“danc‛: I ' m s i n g i n g w h i l e y o u 're danc i n g . 2 .逆序肯定环视(?<=exp) 零宽度正回顾后发断言,又称逆序肯定环视,断言自身出现位置的前面能匹配表达式exp。 比如,以re开头的单词的后半部分(除了 re以外的部分): (? < = \bre) \w + \b 以上表达式在查找以下句子时匹配“ading‛: r e a d i n g a b o o k 假如在很长的数字中,每3 位间加1个逗号(当然是从右边加起),可以在前面和里面添 加逗号的部分: ( ( ? < = \ d ) \d{3)) + \ b 用以上表达式对“ 1234567890” 进行查找,结果是“ ,234, 567, 890‛。这里的逗号只是 匹配需要添加逗号的位置,还没有实际添加逗号。 下面这个例子同时使用这两种断言,匹配以空白符间隔的数字(再次强调,不包括这些空 白符): (? < * \s) \d + (? ■ \s) 前面提到过反义,用来査找不是某个字符或不在某个字符类里的字符。如果只是想要确保 某个字符没有出现,但并不想去匹配它时怎么办?例如,如果想查找这样的单词一出现字母 q , 但是q 后面跟的不是字母u。可以尝试这样: \ b \ w * q [ Au ] \ w * \ b 以上表达式匹配包含后面不是字母U的字母q 的单词。但是如果多做几次测试就会发现, 如果q 出现在单词的结尾,例如Iraq、Benq,这个表达式就会出错。这是因为[ ^ ]总要匹配 一个字符,如果q 是单词的最后一个字符,后面的“ ["u]” 将会匹配q 后面的单词分隔符(可 能是空格、句号或其他),后面的“\w*\b” 将会匹配下一个单词,于是以上表达式就能匹配 整个 Iraq fighting。 逆序肯定环视能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,解 决这个问题如下所示: \b、w * q ( ? ! u ) \w* \b 3 .顺序否定环视(?!exp) 零宽度负预测先行断言,又称顺序否定环视,断言此位置的后面不能匹配表达式“ exp‛。 例如: 1 ) 匹配3 位数字,而且这3位数字的后面不能是数字: \d{3J(?!\d) 2 ) 匹配不包含连续字符串abc的单词: \ b ( (?!abc)\w) + \b 如果匹配的单词是c 开头、t结尾,中间有一个字符,但不能是u (也就是说,整个单词不 能是c u t ) ,直接用就可以了,若中间的字符不能是8或1> (也就是说,整个单词不 能是cat或cut),则表达式改为“c [ lU]t‛。 如果认真读过关于排除型字符组的章节的读者肯定会知道,这个表达式能匹配的只是cot之 类的单词,因为中间的排除型字符组“ [Aau]” 必须匹配一个字符。可是,如果还想匹配chart、 conduct和court怎么办?最简单的想法是:去掉排除型字符组的长度限制,改成“c[*au] +t‛。 不幸的是,这样行不通,因为这个表达式的意思是:c 和t之间由多于一个“除8或11之外 的字符” 构成,而chart、conduct和court都包含a 或u。 我们发现,其实要否定的是“单个出现的a 或u”,而不仅仅是“出现的a 或u‛,所以才 出现这样的问题。要解决这个问题,就应当把意思准确表达出来,变成“在结尾的t 之前,不 允许只出现一个a或u”。想到这一步,就可以用顺序否定环视(?!…)来解决。表示在这个位 置向右,不允许出现子表达式能够匹配的文本,把子表达式规定为“ [au]t\b” (最后的“\b” 82 ♦> PHP核心技术与最佳实践 第3 章正则表达式基础与应用<♦ 83 很重要,它出现在t之后,保证t是单词的结尾字母>。有了限制,匹配a 和t 之间文本的表达 式就随意很多,可以用匹配单词字符的简记法“\w” 表示,于是整个表达式变成: c (?! [ au] t \b) \w + 1 注意这里出现的并不是排除型字符组“ [*au]‛,而是普通的字符组[au],因为顺序否定环 视本身已经表示了否定。 进一步思考,整个匹配文本中都不能出现字符串“ cat”,要怎么办呢?这个正则表达式应 该是: A (?: (?!cat) .) +$ 即在文本中的任意位置,都不能出现该字符串。 4 .逆序否定环视(?) .* (?=<\/\1 >) 以上表达式最能表现零宽断言的真正用途。( )指定前缀为:被尖括号括起来的 单词(比如可能是然后是“• * ” (任意的字符串),最后是一个后缀(?=<\/\1> ) 。 注意后缀里的“\/”,用到了前面提过的字符转义;“\1” 则是反向引用,引用的正是捕获的第 - 组,即前面(W + ) 匹配的内容,如果前缀实际上是“ < b > ”,后缀就是“ ‛。整个 表达式匹配的是“ ” 和“ < / b > ” 之间的内容(再次提醒,不包括前缀和后缀本身)。 总体而言,环视相当于对“所在位置” 附加一个条件,难点就在于找到这个“位置‛。这 一点解决了,环___________视就没有什么秘密可言了。 3 . 3 . 8 贪楚/懒情匹配模式 当正则表达式屮包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配 的前提下)匹配尽可能多的字符。例如以下表达式将匹配以a 开始,以b结束的最长字符串: 如果用来搜索“aabab‛,它会匹配整个字符串“aabab‛。这就是贪婪匹配。 有时,需要匹配尽可能少的字符,也就是懒惰匹配。前面给出的限定符都可以转化为懒惰 匹配模式,只要在后面加上一个问号。例如就意味着匹配任意数量的重复,但是在能 使整个匹配成功的前提下使用最少的重复。例如,匹配以a 开始、以b 结束的最短字符串,正 则表达式如下: 84 < ♦ PHP核心技术与最佳实践 把上述表达式应用于aabab,如果只考虑这个表达式,最先会匹配到aab ( 1~ 3字 符)和ab (第2~3个字符)这两组字符。 为什么第一个匹配是aab (第1~3个字符)而不是ab (第2~ 3个字符)?简单地说,正则 表达式有另一条规则,比懒惰/ 贪婪规则的优先级更高:最先开始的匹配拥有最高优先权。 常用懒惰限定符如表3-5所示。 表3-5 _ 情限定符 懶悄限定符代码/语法描 述 * ? 重复任意次,但尽可能少重复 + ? 取复1次或更多次,但尽可能少重复 ?? 取复0次或1次,但尽可能少重复 |n,m|? 取复n到m次,但尽可能少重复 In,}? 取发n次以上,但尽可能少重复 懒惰模式匹配原理简单来说,是在匹配和不匹配都可以的悄况下,优先不匹配,记录备选 状态,并将匹配控制交给正则表达式的下一个匹配字符。当后面的匹配失败时, 回溯,进行匹 配。关于回溯以及正则表达式效率等高级内容,可以查阅《精通正则表达式》一书。 在3. 3. 6 节涉及懒惰匹配,把该节的例子稍作更改: < ? p h p $ s t r ■ • [ u r l ] l . g i f [ / u r l ] [u r l ] 2 . g i f [ / u r l ] [u r l ] 3 . g i f [ / u r l ] •; $s = p r e g _ r e p l a c e (n #\[ url \ ] (. * ) \[ \/url\] < im g s r c ≫ h t t p :/ / i m a g e .a i y o o y o o .o m / u p l o a d / $ l > ‛,$str> ; v a r一d u m p ( $ s ) ; 在贪婪模式下,由于匹配表达式是“• * ” ,即任意字符出现任意次,这个正则表达式会一 直匹配[urf]后的内容,直到遇到结束条件“ [\/”。匹配结果如图3-3所示。 图3 - 3运行结果 提示实际开发中,涉及贪___________婪模式与镞惰模式的地方是很多的。在一定情况下,使用獭惰模式 可以减少回溯,提高效率。 3 . 4 构造正则表达式 在构造和理解正则表达式的过程中,通常都是由简到繁的过程,如果理解正则表达式内部 间的关系,就可以把比较复杂的正则表达式拆分成几个小块来理解,从而帮助消化。 第3 章正则表达式基础与应用85 3. 4. 1 正则表达式的逻辑关系 正则表达式之间的逻辑关系可以简单地用与、或、非来描述,如表3-6所示。 表3 - 6 正则表达式间的逻辑关系 逻辑关系描 述 与在某个位置,某些元岽(宇符、宇符组或者子表达式)必须出现 或 在某个位置,某个元岽或许出现,或许不出现;或长度和出现次数不固定,或者是某几个元岽 中的一个 非在某个位置,某些元素不出现 通常来说,正则表达式可以看做这三种逻辑关系的组合。下面分析这三种逻辑。 1. 与 “与” 是正则表达式中最普遍的逻辑关系。一般来说,如果正则表达式中的元素没有任何 量词(比如* 、?、+ ) 修饰,就是“与” 关系。比如正则表达式: a b c 表示必须同时出现a、b、c 三个字符。 连续字符是“与” 关系的最佳代表。此外,有些环视结构也可以表达“与” 关系。比如顺 序肯定环视(? = e Xp ) 表示自身出现的位置后面能匹配表达式e x p,换而言之,就是在它后面 必须出现表达式exp。例如: \w + (? - ing) 表示单词的后面必须是ing结尾。 除了顺序肯定环视外,逆序肯定环视也能表达“与” 关系。 比如匹配DIV标签里的内容,例如<加>丨<^0 < /卅> 中的丨0职,就可用以下正则表达式来 匹配: (? < = < d i v > ) .* (? = < / d i v > ) “(?< =
)” 表示自身(即要匹配的部分)出现的位置前面匹配表达式“
‛, “(?=
)” 表示它的后面需要匹配表达式“
‛,中间的就是匹配到的 内容。 2. 或 “或” 是正则表达式中容易出现的逻辑关系。 如果“或” 代表元素可以出现,也可以不出现,或者出现的次数不确定,可以用量词来表 示“或” 关系。比如以下表达式表示在此处,字符a 可以出现,也可以不出现: r a?j 以下表达式表示在此处,字符串ab必然要出现1次,也可以出现无限多次: 86 ♦ > PHP核心技术与最佳实践 r (ab) +J 如果“或” 表示出现的是某个元素的一个,那么可以使用字符组。比如以下正则表达式表 示此处出现的字符是a、b、c 中的任何一个: [ abc] 如果要匹配多个字符,则使用分支结构(…丨…)。比如匹配单词foot及其复数形式,就可 以用正则表达式: f ( 0 0 丨 ee} t 或者使用以下形式: f [ o e ] { 2 } t 3 .非 提到“非”,最容易想到正则表达式中的反义和元字符。比如“\d” 表示数字,那么 其对应的\0就表示非数字;[ a ]表示a 字符,那么[ 1 ] 就表示这个字符不是a。 “非” 关系最常用来匹配成对的标签,例如双引号字符串的匹配,首尾两个双引号很容易 匹配,其中的内容肯定不是双引号(暂不考虑转义的情况),所以可以用[…] 表示,其长度 不确定,用* 来限定,所以整个表达式如下: 比如,需要匹配HTML里成对的A 标签,先匹配左尖括号,紧接着是a 字符,后面可以是 任意内容,最后是一个右尖括号。在这对括号之间可以出现空格、字母、数字、引号等字符, 但是不能出现字符,于是就可以用排除型宇符组“ [ '> ] ” 来表示。再加上后面的配对 标签,整个表达式如下: < 3 卜> ] * > .* < \/a > 运行下面这段代码验证这个表达式: < ? p h p $ r e g = n # < a [ A > ] * > (•” < \ / a > # w ; $ s t r ■ * < a h r e f ■ " h t t p : //baidu • com " > b a i d u < /a > some < a h r e f = " h t t p : / / s o h u . com" > so h u < /a > 1; p r e g _ m a t c h _ a l 1 ($reg, $str, $ m ) ; v a r 一d u m p ( $ m ) ; 运行结果如下: a r r a y (2) { [0]-> a r r a y (1) { [0] ≫> s t r i n g (74) " < a h r e f = "h t t p : / / b a i d u . c o m " > b a i d u < /a > s o m e < a h r e f = "http://soh u . com " > s o h u < /a > " 第3 章正则表达式基础与应用87 [1]=> array (1) { [0]-> s t r i n g (43) " b a i d u < /a > s o m e < a h r e f = "h t t p : / / s o h u . c o m " > s o h u " 发现结果不符合预期,出现了嵌套匹配。原因在于A标签之间的文本忘了做排除型匹配,于 是修改后的正则表达式就成了 “ <化> ] * > ( [ '< > ] * ) < V a > % 经过修改后就符合预期丫。 除了反义和排除型字符组外,否定环视也能表示“非” 这种关系。比如有一串文字:“ ab < p > one

cde < div > fgh
< img src = " " > " 0 现在需要匹配除P 标签夕卜的所有标 签。换而言之,就是先匹配所有HTML标签,可以使用如下表达式: ] + > 匹配闭合的“ < x x x > ” 或“ < /XXX> ” 标签,然后再排除“XXX” 或“/XXX” 部分 是P 的标签,于是使用顺序否定环视,用表达式: (?!/? p\b) 排除了 “ < ” 或“ < / ” 这个位置后是P字符的情况,这样就满足需求了。最终的表达式 则为: < (?!/? p\b)[A>] + > 通过上面的分析得到正则表达式中的“与或非” 关系及其代表语法,如表3-7所示。 表3-7 正则表达式中的“与或非‛关系及其代表语法 逻辑关系代表语法 与连续字符、肯定环视(顺序肯定,逆序肯定) 或置词、字符组 非排除甩字符、反义、否定环视(顺序否定,逆序杏定) 3. 4 . 2 运算符优先级 正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。表3-8说 明了各种正则表达式运算符的优先级顺序,其中优先级从上到下、由高到低排列。 表3 - 8 正则表达式运算符的优先级 运算符描 述 \ 转义符 0 , (?:), (? = ),[] 括号和中括号 • , + ,?, |n|, |n,|, |n, m| 限定符 $ y \anymetacharacter, anycharacter 定位点和序列 1 替换 88 PHP核心技术与最佳实践 字符的优先级比替换运算符高,替换运算符允许m I food与m 或food匹配。要匹配mood 或food,使用括号创建子表达式,从而产生如下表达式: (m | f )o o d 3 . 4 . 3 正则表达式的常用模式 模式(Pattern Modifiers)就是可以改变表达行为的字符,用来关闭或打开某些特殊功能, 习惯上又称为正则修饰符。每种语言提供的正则表达式所支持的修饰符都不一样,这节介绍一 些基本修饰符及常用模式。 1 .忽略大小写模式(i> 在此模式下,正则匹配将不区分待匹配内容的大小写,这在HTML里常用。由于HTML本 身的容错性很好,对大小写混用有很好的兼容处理能力,也就经常会出现无论是标签还是内容 的大小写混乱问题,这时采用这种模式就能很好地处理这种情况。示例代码如下: < d i v > g g < \/d i v >在忽略大小写模式下,可匹配:< d i v > g G < / D i v > < ? p h p $ s t r ■ • < d i v > g G < / D i v > •; if ( p reg_match (*% < d i v > g g < \/div >%!■•," < d i v > g G < / D i v > ", $ a r r ) ) { e c h o "匹配成功”.$arr[ 0 ]; }else{ e c h o ••匹fc不成功" 7 } 忽略大小写是针对整个表达式而言,而不仅仅是欲匹配的部分。所以,可以在代码里放心 地使用此修饰符。但是出于效率的考虑,尽量让正则表达___________式所指示的范围更精确。 注意,修饰符对整个表达式有效。如果只想修饰部分表达式,可以使用PCRE的内部选 项— “局部修饰符”。比如下面表达式仅匹配abc和a b C ,而不会匹配Abc: # a b ( ? i ) c # 也就是说,(?i ) 只对它后面的字符c起作用。 2 .多行模式(m) 在讲起始和终止符时提过:“勾选Multiline选项,即多行选项。如果选中了这个选项, 和“$” 的意义就变成匹配行的开始处和结束处,否则将把整个输入视作一个字符串,忽视换 行符。” 也就是说,正则表达式默认开始•和结束$只是对于正则字符串,如果在修饰符中加上 m ,开始和结束将会指字符串的每一行:即每一行的开头就是' 结尾就是$。 需要注意,m表示多行匹配,而非跨行匹配。仅当表达式中出现' $中至少一个元字符且 宇符串有换行符“\n” 时,m修饰符才起作用,否则被忽略,如代码淸单3-1所示。 第3 章正则表达式基础与应用< ♦ 89 _______________________代码清单3 - 1 多行模式 < ? p h p $ s t r = "th i s is reg R e g t h i s is r e g e x p t u r t o r , o h reg"; i f ( p r e g _ m a t c h _ a l l ('%.* r e g $ % m i ',$ s t r , $ a r r ) ){ e c h o •,匹紀成功"; v a r _ d u m p ( $ a r r ) ; }else{ e c h o ••匹配不成功"; 在预想中,“. * r e g $” 就是以r e g结尾的行,由于加了 m 修饰符,按理应该匹配第一行和 第四行,但实际结果呢?如下所示: 匹 fit成功 array (1) { [0]=> a r r a y (1) { [0 ]=> s t r i n g (20) " r e g e x p t u r t o r , o h reg*' } } 可以看出,只匹配最后一行的re g ,而第一行虽然也是以reg结尾,但是并没有被匹配。这 里用到m i ,也就是把修饰符m 和i 组合使用。事实上,本例中即使去掉m修饰符,最终结果也 是一样,这说明8只能表示最后一行。把正则表达式改为: % At . * % m i 匹配的结果将是: 匹 K “成功 array { [ 0 ]=> a r r a y (2) { [0 ]-> s t r i n q (12) "th i s is req" [1]=> s t r i n g (8) "th i s is" 匹配到两行,去掉m修饰符只匹配到第一行。可见,即使加了m修饰符,也不是将整个字 符串都匹配,这就是跨行与多行的区别。 另外,使用m模式匹配需要注意换行符是否真的有效。看代码清单3-2所示的例子。 90 PHP核心技术与最佳实践 ____________________________代码清单3 - 2 多行模式的例子 < ? p h p $ s o u r c e l ■ ' a b c N n a b c d ' ; $ s o u r c e 2 - " a b c \ n a b c d w ; if ( p r e g_match_all (• % ^ a b c % m ' , $ s o u r c e l , $ a r r ) ) { e c h o ““匹纪成功 v a r一d u m p ( $ a r r ) ; }else{ e c h o "匹fc不成功 ) if ( p r e g_match_all (• % Aa b c % m ' , $ source2, $ a r r ) ) { e c h o " E f c成功 v a r _ d u m p ( $ a r r ) ; )else{ e c h o "匹配不成功 运行可以看到,由于sourcel字符串使用单引号,\n作为普通字符而非换行符,因此匹配 到的结果和预期不符。 3 .点号通配模式(s) 点号通配模式的作用是使正则表达式里的点号元字符可以匹配换行符,如果没有这个修饰 符,点号不匹配换行符。沿用上面的例子,如代码清单3-3所示。 _________________________________代码清单3 - 3点号通配横式_________________________________ < ? p h p $ s t r = "th i s is re g R e g t h i s is r e g e x p t u r t o r , o h reg"; if (preg_m a t c h _ a l l ('%this. * ? r e g % i •, $str, $ a r r ) ) { e c h o "匹fc成功 v a r _ d u m p ( $ a r r ) ; }else{ e c h o ••匹SC•不成功"; 前面学习过, 元字符表示除换行符以外的任意字符。所以按照这个推论,上述正则表 达式应该能匹配到第一行,即“ this is reg”,而第三行和第四行由于之间存在一个换行符,所 以不能匹配。如果给上述正则表达式加上s修饰符,即reg%is‛,那么匹配结果就 不同了,如下所示: 匹纪成功a r r a y (1> { [0 ]-> a r r a y (2) { [0 ]-> 第3 章正则表达式基础与应用91 s t r i n g (11) "th i s is r eg" [1]≫> s t r i n g (13) "th i s is r eg" 可以看出,这个匹配跨行了。 我们只要牢记,S修饰符包括换行符。这个修饰符很有用,特别是在抓取一些文档时,由 于存在不可见换行(这是很常见的),如果使用匹配,就可能存在问题,这就需要表达式 能匹配换行符。看代码淸单3-4所示的例子。 _________________________ 代码清单3 - 4 匹配换行符_________________________ $ s t r = • < b o d y >
< d i v c l a s s = " h e a d " > < / d i v > < d i v c l a s s = "bo d y " > < / d i v > < div class = "foot" > < /div > < / d i v > < / b o d y > '; $ a r r a y = a r r a y (); $arr a y 2 = a r r a y (); p r e g _ m a t c h _ a l l (* I < b o d y > (•* ) < \/b o d y > I •, $str, $ a r r a y ) ; v a r _ d u m p ( $ a r r a y ) ; p r e g j n a t c h一al l (' I < b o d y > (.* ) < \ / b o d y > I s •, $str, $ a r r a y 2 ); v a r j d u m p ( $ a r r a y 2 ); 结果是第一个正则表达式没有匹配到任何内容,而第二个正则表达式匹配< body > 标签之 内的全部内容。原因在于,要匹配的文本在< body >标签后紧接着是一个不可见换行符,从而 导致第一个正则表达式匹配失败。从这个例子中更能深刻体会到s修饰符的作用。 4 .懒惰模式(U> “U” 相当于前面提到的“?”,表示懒惰匹配,因此3.3.8节的例子也可以改写为如下 代码: < ? p h p $ s t r = • [ u r l ] l .gif [ /url] [ u r l ] 2 .gif [ /url] [ u r l ] 3 .gif[ /url] •; $s = p r e g _ r e p l a c e ("#\[ url\] (.* ) \[ \/url\] #U", •• < im g s rc - h t t p : //image. a i y o o y o o . om/upload/$l > " , $ s t r > ; v a r j d u m p ($s ) ; 在这里:以下两个表达式作用是等价的。 #\[url\] (•* )\[\/u r l \ ] # U 和 92 < ♦ PHP核心技术与最佳实践 5 .结尾限制(D) 如果使用$限制结尾字符,则不允许结尾有换行。例如,以下正则表达式将匹配“ abc‛、 “abc\n” 这样的字符,即忽视结尾的换行: % a b c $% 如果使用此模式,限定其不可有换行,必须以abc结尾,如下所示: % a b c $ % D 6 .支持UTF-8转义表达 u 修饰符启用PCRE中与Perl不兼容的额外功能,模式字符串被当成UTF-8。u 修饰符在 UNIX下自PHP 4. 1.0起可用,在Win32下自PHP 4. 2. 3 起可用,自PHP 4. 3. 5 起开始检査模式 的UTF-8合法性。看一个例子: $ s t r = " p h p 编程 if (preg一m a t c h ( " / A [ \ x { 4 e 0 0 } - \ x { 9 f a 5 } ] + $ / u”,$ s t r } ) { e c h o ““该¥ 符串全部是中文"; } e l s e { e c h o ““该字符串不全部是中文"; } 正则表达式中使用了 U修饰符,就可以使用UTF-8的转义表达,这实际上是兼容问题。在 PHP中,使用此修饰符即可实现在表达式中支持UTF-8。 提示除了上述几个模式修饰符之外,PHP里还支持另外几个修饰符,如A、x 等,但不是很 常用,这里就不做讲解了。 3 . 5 正则在实际开发中的应用 前面几节主要讲解了正则表达式一些基础知识,本节几个例子将展示正则表达式在日常开 发中的应用。 3. 5. 1 移动手机校验 首先,我们练习最常用的正则表达式:移动手机校验。 通过查阅相关资料知道移动的号段有:134、135、136、137、138、139、150、151、157、 158、1 5 9 ,新增3G号182、183和188。手机号码一共11位,前3 位为运营商号段,中间4 位 为号码归屈地,后4 位为随机号。那么可以写出如下正则表达式: (13[4 -9] 115[0X789] 118[238])\d{8) 规则很简单,匹配前3 位是否在移动号码段里,后8 位为数字即可。这里用到分支、分组 和元字符这三个知识点,用PHP代码测试如代码清单3-5所示。 第3 章正则表达式基础与应用♦ > 93 ___________________ 代码清单3 - 5测试移动手机号匹配 < ? p h p $ m o b i l e = ' 1 3 5 0 0 0 0 0 0 0 0 ' ; $ r e g e x = M !A (13[4 - 9 ] |15[0189] |188) \d{8 }$!"; i f (! p r e g _ m a t c h ( $ r e g e x , $ m o b i l e ) ){ d i e r 错误的移动号码段! *>; }else{ d i e (“移动手机.•>; 运行结果符合预期。注意第三行代码,在前面已经说过,正则表达式的分隔符只要遵循规 则是可以随意的。所以这里用的是感叹号,而不是常见的斜杠,当然,用#、~也可以(为了 书写美观,一般不用斜杠)。 如果只判断手机号就更简单了,如下所示: l [ 3 5 8 ] \ d { 9 ) 3. 5.2 匹配E-mail地址 运用学过的知识,我们实现简单的E-mail识别。 E-mail最简单的形式为user@ domain.com。其中,user为用户名,domain为域名,com为后 缀;当然,后缀还可以是net、name、c n等。一般用户名由3 个以上的字母和数字组成,当然 也不能太长,允许出现下画线。中间一个@符号,后面的域名长度为1 ~64位,后缀长度一般 为2~ 5位,如下所示: \ w { 3 , 1 6 ) @ \ w { l >6 4 ) \ . \ w ( 2 (5} 以上表达式匹配用户名长度3 ~ 16位,紧跟一个@符号,然后是1~64位的域名,再然后 是dot (•号),最后是2 ~ 5位的后缀。这个正则表达式还存在一些问题,比如xxx_yyy@ yahoaconLcn 这样的地址,后面的.cn就无法匹配到。这就需要进一步学习。 Regular Expression Tester工具提供一些预定义的正则表达式,它提供的匹配E-mail的正则 表达式如下: A [ a - z0 - 9_\ - ] + (V . [ _ a - z0 - 9 V - ] + ) * @ ( [ _ _ a - z 0 - 9\ - ] + \ .) + < [ a - z ] { 2 } | a e r o I a r p a I b i z | c o m I c o o p | e d u | g o v I i n f o I in t I jo b s I mi l I m u s e u m I n a m e I n a t o I n e t I o r g I p r o I travel) $ 这个表达式很长,使用字符组和分支条件语法,很好地解决了 com.cn这样多个后缀域名的 问题。相信现在读者对于匹配电话号码、QQ号等应该能得心应手了。 3 . 5 . 3 转义在数据安全中的应用 数据安全是任何一款软件设计中都需要考虑的问题。从技术层面讲,数据安全就是保护存 储和使用的数据不被窃听、盗取和破坏。这可能是由外部因素造成的,比如由于过滤不严格造 成SQL注人漏洞、提升脚本执行权限等,也有可能是由代码内部设计造成的,如死循环、低效 94 ♦ > PHP核心技术与最佳实践 率的语句造成服务器性能下降以致影响访问。 社会学意义上的数据安全则更广泛。比如,在在线购物商城的设计中,由于设计者错误地 使用自增ID作为商品的单据流水号,竞争对手或有心人很容易分析出这个商城的每日销售量, 进而估算出销售额、利润等商业机密数据。 在程序中要保证数据的安全,除了要保证代码内部运行 的可靠外,最主要的就是严格处理外部数据,即秉持‚一切 输人输出都是不可靠的” 理论,这就要求我们做好数据过滤 和验证。PHP编程中摄简单的过滤机制就是转义,即对用户 的输入和输出进行转义和过滤。图3-4所示是简单的留言本。 简单留言本的演示代码如下: < f o r m a c t i o n < t e x t a r e a nai < i n p u t t y p e - < ? p h p e c h o $_POST[ ' c o n t e n t '] m e t h o d = "POS T " > * = '* c o n t e n t " > < / t e x t a r e a > s u b m i t " v a l u e = ” 留言" / > 简单留言本31 贫_________ 用户名:lert , 〈style〉 body {background:#00000