正文开始 1.2.3 - 1.6.3
1.2.3 toString方法 再看另外一个魔术方法TOstring(在这里故意这么写,是要说明PHP中方法不区分大小写,但实际开发中还需要注意规范)。 当进行测试时,需要知道是否得出正确的数据。比如打印一个对象时,看看这个对象都有哪些属性,其值是什么,如果类定义了toString方法,就能在测试时,echo打印对象体,对象就会自动调用它所属类定义的toString方法,格式化输出这个对象所包含的数据。如果没有这个方法,那么echo一个对象将报错,例如“Catchable fatal error:Object of class Account could not be converted to string”语法错误,实际上这是一个类型匹配失败错误。不过仍然可以用print_r()和var_dump()函数输出一个对象。当然,toString是可以定制的,所提供的信息和样式更丰富,如代码清单1-4所示。 代码清单1-4 magic_2.php <?php class Account{ public $user=1; private $pwd=2; // 自定义的格式化输出方法 public function toString(){ return "当前对象的用户名是{$this->user},密码是{$this->pwd}"; } } $a=new Account(); echo $a; echo PHP_EOL; print_r($a); 运行这段代码发现,使用toString方法后,输出的结果是可定制的,更易于理解。实际上,PHP的toString魔术方法的设计原型来源于Java。Java中也有这么一个方法,而且在Java中,这个方法被大量使用,对于调试程序比较方便。实际上,toString方法也是一种序列化,我们知道PHP自带serialize/unserialize也是进行序列化的,但是这组函数序列化时会产生一些无用信息,如属性字符串长度,造成存储空间的无谓浪费。因此,可以实现自己的序列化和反序列化方法,或者json_encode/json_decode也是一个不错的选择。 为什么直接echo一个对象就会报语法错误,而如果这个对象实现toString方法后就可以直接输出呢?原因很简单,echo本来可以打印一个对象,而且也实现了这个接口,但是PHP对其做了个限制,只有实现toString后才允许使用。从下面的PHP源代码里可以得到验证: ZEND_VM_HANDLER(40, ZEND_ECHO, CONST|TMP|VAR|CV, ANY) { zend_op *opline = EX(opline); zend_free_op free_op1; zval z_copy; zval *z = GET_OP1_ZVAL_PTR(BP_VAR_R); // 此处的代码预留了把对象转换为字符串的接口 if (OP1_TYPE != IS_CONST && Z_TYPE_P(z) == IS_OBJECT && Z_OBJ_HT_P(z)->get_method != NULL && zend_std_cast_object_tostring(z, &z_copy, IS_STRING TSRMLS_CC) == SUCCESS) { zend_print_variable(&z_copy); zval_dtor(&z_copy); } else { zend_print_variable(z); } FREE_OP1(); ZEND_VM_NEXT_OPCODE(); } 由此可见,魔术方法并不神奇。 有比较才有认知。最后,针对本节代码给出一个Java版本的代码,供各位读者用来对比两种语言中重载和魔术方法的异同,如代码清单1-5所示。 代码清单1-5 Account.java import org.apache.commons.lang3.builder.ToStringBuilder; /** *类的重载演示Java版本 *@author wfox *@date @verson */ public class Account { private String user;// 用户名 private String pwd;// 密码 public Account() { System.out.println("构造函数"); } public Account(String user,String pwd){ System.out.println("重载构造函数"); System.out.println(user+"---"+pwd); } public void say(String user){ System.out.println("用户是:"+user); } public void say(String user,String pwd){ System.out.println("用户:"+user); System.out.println("密码"+pwd); } public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getPwd() { return pwd; } public void setPwd(String pwd) { } @Override public String toString() { return ToStringBuilder.reflectionToString(this); } public static void main(String…) { Account account=new Account(); account.setUser("张三"); account.setPwd("123456"); account.say("李四"); account.say("王五","123"); System.out.println(account); } } 运行上述代码,输出如图1-2所示。 可以看出,Java的构造方法比PHP好用,PHP由于有了set/get这一对魔术方法,使得动态增加对象的属性字段变得很方便,而对Java来说,要实现类似的效果,就不得不借助反射API或直接修改编译后字节码的方式来实现。这体现了动态语言的优势,简单、灵活。 1.3.1 类的组合与继承 1.3 继承与多态 面向对象的优势在于类的复用。继承与多态都是对类进行复用,它们一个是类级别的复用,一个是方法级别的复用。提到继承必提组合,二者有何异同?PHP到底有没有多态?若没有,则为什么没有?有的话,和其他语言中的多态又有什么区别?这些都是本节所要讲述的内容。 1.3.1 类的组合与继承 在1.1节的代码中定义了两个类,一个是person,一个是family;在family类中创建person类中的对象,把这个对象视为family类的一个属性,并调用它的方法处理问题,这种复用方式叫“组合”。还有一种复用方式,就是继承。 类与类之间有一种父与子的关系,子类继承父类的属性和方法,称为继承。在继承里,子类拥有父类的方法和属性,同时子类也可以有自己的方法和属性。 可以把1.1节的组合用继承实现,如代码清单1-6所示。 代码清单1-6 family_extends.php <?php class person{ public $name='Tom'; public $gender; static $money=10000; public function construct(){ echo '这里是父类',PHP_EOL; } public function say(){ echo $this->name," \tis ",$this->gender,"\r\n"; } } class family extends person{ public $name; public $gender; public $age; static $money=100000; public function construct(){ parent::construct(); // 调用父类构造方法 echo '这里是子类',PHP_EOL; } public function say(){ parent::say(); echo $this->name," \tis\t",$this->gender,",and is\t",$this->age,PHP_EOL; } public function cry(){ echo parent::$money,PHP_EOL; echo '%>_<%',PHP_EOL; echo self::$money,PHP_EOL;// 调用自身构造方法 echo '(*^^*)'; } } $poor=new family(); $poor->name='Lee'; $poor->gender='female'; $poor->age=25; $poor->say(); $poor->cry(); 运行上面的代码,可以得到如下输出结果: 这里是父类 这里是子类 Lee is female Lee is female,and is 25 10000 %>_<% 100000 (*^^*) 从上面代码中可以了解继承的实现。在继承中,用parent指代父类,用self指代自身。使用“::”运算符(范围解析操作符)调用父类的方法。“::”操作符还用来作为类常量和静态方法的调用,不要把这两种应用混淆。 既然提到静态,就再强调一点,如果声明类成员或方法为static,就可以不实例化类而直接访问,同时也就不能通过一个对象访问其中的静态成员(静态方法除外),也不能用“::”访问一个非静态方法。比如,把上例中的$poor->cry();换成$poor::cry(),按照这个规则,应该是要报错的。可能试验时,并没有报错,而且能够正确输出。这是因为用“::”方式调用一个非静态方法会导致一个E_STRICT级别的错误,而这里的PHP设置默认没有开启这个级别的报错提示。打开PHP安装目录下的php.ini文件,设置如下: error_reporting = E_ALL| E_STRICT display_errors = On 再次运行,就会看到错误提示。因此,用“::”访问一个非静态方法不符合语法,但PHP仍然能够正确地执行代码,这只是PHP所做的一个“兼容”或者说“让步”。在开发时,设置最严格的报错等级,在部署时可适当调低。 组合与继承都是提高代码可重用性的手段。在设计对象模型时,可以按照语义识别类之间的组合关系和继承关系。比如,通过一些总结,得出了继承是一种“是、像”的关系,而组合是一种“需要”的关系。利用这条规律,就可以很简单地判断出父亲与儿子应该是继承关系,父亲与家庭应该是组合关系。还可以从另外一个角度看,组合偏重整体与局部的关系,而继承偏重父与子的关系,如图1-3所示。 从方法复用角度考虑,如果两个类具有很多相同的代码和方法,可以从这两个类中抽象出一个父类,提供公共方法,然后两个类作为子类,提供个性方法。这时用继承语意更好。继承的UML图如图1-4所示。 而组合就没有这么多限制。组合之间的类可以关系(体现为复用代码)很小,甚至没有关系,如图1-5所示。 然而在编程中,继承与组合的取舍往往并不是这么直接明了,很难说出二者是“像”的关系还是“需要”的关系,甚至把它拿到现实世界中建模,还是无法决定应该是继承还是组合。那应该怎么办呢?有什么标准吗?有的。这个标准就是“低耦合”。 耦合是一个软件结构内不同模块之间互连程度的度量,也就是不同模块之间的依赖关系。 低耦合指模块与模块之间,尽可能地使模块间独立存在;模块与模块之间的接口尽量少而简单。 现代的面向对象的思想不强调为真实世界建模,变得更加理性化一些,把目标放在解耦上。 解耦是要解除模块与模块之间的依赖。 按照这个思想,继承与组合二者语义上难于区分,在二者均可使用的情况下,更倾向于使用组合。为什么呢?继承存在什么问题呢? 1)继承破坏封装性。 比如,定义鸟类为父类,具有羽毛属性和飞翔方法,其子类天鹅、鸭子、鸵鸟等继承鸟这个类。显然,鸭子和鸵鸟不需要飞翔这个方法,但作为子类,它们却可以无区别地使用飞翔这个方法,显然破坏了类的封装性。而组合,从语义上来说,要优于继承。 2)继承是紧耦合的。 继承使得子类和父类捆绑在一起。组合仅通过唯一接口和外部进行通信,耦合度低于继承。 3)继承扩展复杂。 随着继承层数的增加和子类的增加,将涉及大量方法重写。使用组合,可以根据类型约束,实现动态组合,减少代码。 4)不恰当地使用继承可能违反现实世界中的逻辑。 比如,人作为父类,雇员、经理、学生作为子类,可能存在这样的问题,经理一定是雇员,学生也可能是雇员,而使用继承的话一个人就无法拥有多个角色。这种问题归结起来就是“角色”和“权限”问题。在权限系统中很可能存在这样的问题,经理权利和职位大于主管,但出于分工和安全的考虑,经理没有权限直接操作主管所负责的资源,技术部经理也没权限直接命令市场部主管。这就要求角色和权限系统的设计要更灵活。不恰当的继承可能导致逻辑混乱,而使用组合就可以较好地解决这个问题。 当然,组合并非没有缺点。在创建组合对象时,组合需要一一创建局部对象,这一定程度上增加了一些代码,而继承则不需要这一步,因为子类自动有了父类的方法,如代码清单1-7所示。 代码清单1-7 mobile.php <?php // 继承拥有比组合更少的代码量 class car{ public function addoil(){ echo "Add oil\r\n"; } } class bmw extends car{ } class benz{ public $car; public function construct(){ $this->car=new car; } public function addoil(){ $this->car->addoil(); } } $bmw=new bmw; $bmw->addoil(); $benz=new benz(); $benz->addoil(); 显然,组合比继承增加了代码量。组合还有其他的一些缺点,不过总体说来,是优点大于缺点。 继承最大的优点就是扩展简单,但是其缺点大于优点,所以在设计时,需要慎重考虑。那应该如何使用继承呢? 精心设计专门用于被继承的类,继承树的抽象层应该比较稳定,一般不要多于三层。 对于不是专门用于被继承的类,禁止其被继承,也就是使用final修饰符。使用final修饰符既可防止重要方法被非法覆写,又能给编辑器寻找优化的机会。 优先考虑用组合关系提高代码的可重用性。 子类是一种特殊的类型,而不只是父类的一个角色。 子类扩展,而不是覆盖或者使父类的功能失效。 底层代码多用组合,顶层/业务层代码多用继承。底层用组合可以提高效率,避免对象臃肿。顶层代码用继承可以提高灵活性,让业务使用更方便。 思考题 设计一个log类,需要用到MySQL中的CURD操作,是应该使用继承呢还是组合?请给出理由。 继承并非一无是处,而组合也不是完美无缺的。如果既要组合的灵活,又要继承的代码简洁,能做到吗? 这是可以做到的,譬如多重继承,就具有这个特性。多重继承里一个类可以同时继承多个父类,组合两个父类的功能。C++里就是使用的这种模型来增强继承的灵活性的,但是多重继承过于灵活,并且会带来“菱形问题”,故为其使用带来了不少困难,模型变得复杂起来,因此在大多数语言中,都放弃了多重继承这一模型。 多重继承太复杂,那么还有其他方式能比较好地解决这个问题吗?PHP5.4引入的新的语法结构Traits就是一种很好的解决方案。Traits的思想来源于C++和Ruby里的Mixin以及Scala里的Traits,可以方便我们实现对象的扩展,是除extend、implements外的另外一种扩展对象的方式。Traits既可以使单继承模式的语言获得多重继承的灵活,又可以避免多重继承带来的种种问题。 1.3.2 各种语言中的多态 多态确切的含义是:同一类的对象收到相同消息时,会得到不同的结果。而这个消息是不可预测的。多态,顾名思义,就是多种状态,也就是多种结果。 以Java为例,由于Java是强类型语言,因此变量和函数返回值是有状态的。比如,实现一个add函数的功能,其参数可能是两个int型整数,也可能是两个float型浮点数,而返回值可能是整型或者浮点型。在这种情况下,add函数是有状态的,它有多种可能的运行结果。在实际使用时,编译器会自动匹配适合的那个函数。这属于函数重载的概念。需要说明的是,重载并不是面向对象里的东西,和多态也不是一个概念,它属于多态的一种表现形式。 多态性是一种通过多种状态或阶段描述相同对象的编程方式。它的真正意义在于:实际开发中,只要关心一个接口或基类的编程,而不必关心一个对象所属于的具体类。 很多地方会看到“PHP没有多态”这种说法。事实上,不是它没有,而是它本来就是多态的。PHP作为一门脚本语言,自身就是多态的,所以在语言这个级别上,不谈PHP的多态。在PHP官方手册也找不到对多态的详细描述。 既然说PHP没有多态这个概念(实际上是不需要多态这个概念),那为什么又要讲多态呢?可以看下面的例子,如代码清单1-8所示。 代码清单1-8 Polymorphism.php <?php class employee{ protected function working(){ echo '本方法需重载才能运行'; } } class teacher extends employee{ public function working(){ echo '教书'; } } class coder extends employee{ public function working(){ echo '敲代码'; } } function doprint($obj){ if(get_class($obj)=='employee'){ echo 'Error'; }else{ $obj->working(); } } doprint(new teacher()); doprint(new coder()); doprint(new employee()); 通过判断传入的对象所属的类不同来调用其同名方法,得出不同结果,这是多态吗?如果站在C++角度,这不是多态,这只是不同类对象的不同表现而已。C++里的多态指运行时对象的具体化,指同一类对象调用相同的方法而返回不同的结果。看个C++的例子,如代码清单1-9所示。 代码清单1-9 C++多态的例子 #include #include /** C++中用虚函数实现多态 */ using namespace std; class father{ public: father():age(30){cout<<"父类构造法,年龄"<<age<<"\n";} ~father(){cout<<"父类析构"<<"\n";} void eat(){cout<<"父类吃饭吃三斤"<<"\n";} virtual void run(){cout<<"父类跑10000米"<<"\n";}// 虚函数 protected: int age; }; class son:public father{ public: son(){cout<<"子类构造法"<<"\n";} ~son(){cout<<"子类析构"<<"\n";} void eat(){cout<<"儿子吃饭吃一斤"<<"\n";} void run(){cout<<"儿子跑100米"<<"\n";} void cry(){cout<<"哭泣"<<"\n";} }; int main(int argc, char *argv[]) { father *pf=new son; pf->eat(); pf->run(); delete pf; system("PAUSE"); return EXIT_SUCCESS; } 上面的代码首先定义一个父类,然后定义一个子类,这个子类继承父类的一些方法并且有自己的方法。通过father *pf=new son;语句创建一个派生类(子类)对象,并且把该派生类对象赋给基类(父类)指针,然后用该指针访问父类中的eat和run方法。图1-6所示是运行结果。 由于父类中的run方法加了virtual关键字,表示该函数有多种形态,可能被多个对象所拥有。也就是说,多个对象在调用同一名字的函数时会产生不同的效果。 这个例子和PHP的例子有什么不同呢?C++的这个例子所创建的对象是一个指向父类的子对象,还可以创建更多派生类对象,然后上转型为父类对象。这些对象,都是同一类对象,但是在运行时,却都能调用到派生类同名函数。而PHP中的例子则是不同类的对象调用。 由于PHP是弱类型的,并且也没有对象转型机制,所以不能像C++或者Java那样实现father $pf=new son;把派生类对象赋给基类对象,然后在调用函数时动态改变其指向。在PHP的例子中,对象都是确定的,是不同类的对象。所以,从这个角度讲,这还不是真正的多态。 代码清单1-8所示代码通过判断对象的类属性实现“多态”,此外,还可以通过接口实现多态,如代码清单1-10所示。 代码清单1-10 通过接口实现多态 <?php interface employee{ public function working(); } class teacher implements employee{ public function working(){ echo '教书'; } } class coder implements employee{ public function working(){ echo '敲代码'; } } function doprint(employee $i){ $i $i->working(); } $a=new teacher; $b=new coder; doprint($a); doprint($b); 这是多态吗?这段代码和代码清单1-8相比没有多少区别,不过这段代码中doprint函数的参数是一个接口类型的变量,符合“同一类型,不同结果”这一条件,具有多态性的一般特征。因此,这是多态。 如果把代码清单1-8中doprint函数的obj参数看做一种类型(把所有弱类型看做一种类型),那就可以认为代码清单1-8中的代码也是一种多态。 再次把三段代码放在一起品味,可以看出:区别是否是多态的关键在于看对象是否属于同一类型。如果把它们看做同一种类型,调用相同的函数,返回了不同的结果,那么它就是多态;否则,不能称其为多态。由此可见,弱类型的PHP里多态和传统强类型语言里的多态在实现和概念上是有一些区别的,而且弱类型语言实现起多态来会更简单,更灵活。 本节解决了什么是多态,什么不是多态的问题。至于多态是怎么实现的,各种语言的策略是不一样的。但是,最终的实现无非就是查表和判断。总结如下: 多态指同一类对象在运行时的具体化。 PHP语言是弱类型的,实现多态更简单、更灵活。 类型转换不是多态。 PHP中父类和子类看做“继父”和“继子”关系,它们存在继承关系,但不存在血缘关系。因此子类无法向上转型为父类,从而失去多态最典型的特征。 多态的本质就是if…else,只不过实现的层级不同。 1.4.1 接口的作用 1.4 面向接口编程 这里,首先强调一个概念,面向接口编程并不是一种新的编程范式。本章开头提到的三大范式中并没有提到面向接口。其次,这里是狭义的接口,即interface关键字。广义的接口可以是任何一个对外提供服务的出口,比如提供数据传输的USB接口、淘宝网对其他网站开放的支付宝接口。 1.4.1 接口的作用 接口定义一套规范,描述一个“物”的功能,要求如果现实中的“物”想成为可用,就必须实现这些基本功能。接口这样描述自己: “对于实现我的所有类,看起来都应该像我现在这个样子”。 采用一个特定接口的所有代码都知道对于那个接口会调用什么方法。这便是接口的全部含义。接口常用来作为类与类之间的一个“协议”。接口是抽象类的变体,接口中所有方法都是抽象的,没有一个有程序体。接口除了可以包含方法外,还能包含常量。 比如用接口描述发动机,要求机动车必须要有“run”功能,至于怎么实现(摩托还是宝马),应该是什么样(前驱还是后驱),不是接口关心的。因为接口为抽象而生。作为质检总局,要判断这辆车是否合格,只要按“接口”的定义一条一条验证,这辆车不能“run”,那它就是废品,不能通过验收。但是,如果汽车实现了接口中本来不存在的方法music,并不认为有什么问题。接口就是一种契约。因此,在程序里,接口的方法必须被全部实现,否则将报fetal错误,如代码清单1-11所示。 代码清单1-11 interface.php <?php interface mobile { public function run(); // 驱动方法 } class plain implements mobile { public function run() { echo "我是飞机"; } public function fly() { echo "飞行"; } } class car implements mobile{ public function run(){ echo "我是汽车\r\n"; } } class machine { function demo(mobile $a) { $a->fly(); // mobile接口是没有这个方法的 } } $obj = new machine(); $obj->demo(new plain());// 运行成功 $obj->demo(new car());// 运行失败 在这段代码里,定义一个机动车接口,其中含有一个发动机功能。然后用一个飞机类实现这个接口,并增加了飞行方法。最后,在一个机械检测类中对机动车进行测试(用类型约束指定要测试的是机动车这个接口)。但是,此检测线测试的却是机动车接口中不存在的fly方法,直到遇到car的实例因不存在fly方法而报错。 这段代码实际上是错误的,不符合接口语义。但是在PHP里,对plain的实例进行检测时是可以运行的。也就是说,在PHP里,只关心是否实现这个方法,而并不关心接口语义是否正确。 按理说,接口应该是起一个强制规范和契约的作用,但是这里对接口的约束并没有起效,也打破了契约,对检测站这个类的行为失去控制。可以看看在Java里是怎么处理的,如图1-7所示。 Java认为,接口就是一种type,即类型。如果你打破了我们之间的契约,你的行为变得无法控制,那就是非法的。这符合逻辑,也符合现实世界。这就真正起到接口作为规范的作用了。 接口不仅规范接口的实现者,还规范接口的执行者,不允许调用接口中本不存在的方法。当然这并不是说一个类如果实现了接口,就只能实现接口中才有的方法,而是说,如果针对的是接口,而不是具体的类,则只能按接口的约定办事。这样的语法规定对接口的使用是有利的,让程序更健壮。根据这个角度讲,为了保证接口的语义,通常一个接口的实现类仅实现该接口所具有的方法,做到专一,当然这也不是一成不变的。 由上面的例子可以看出,PHP里接口作为规范和契约的作用打了折扣。上面例子实际就是一个典型面向接口编程的例子。根据这个例子,可以很自然地想到使用接口的场合,比如数据库操作、缓存实现等。不用关心我们所面对的数据库是MySQL还是Oracle,只需要关心面向Database接口进行具体业务的逻辑相关的代码,这就是面向接口编程的来历。 在这里,Database就如同employee一样,针对这个接口实现就好了。缓存功能也一样,我们不关注缓存是内存缓存还是文件缓存,或者是数据库缓存,只关注它是否实现了Cache接口,并且它只要实现了Cache接口,就实现了写入缓存和读取某条缓存中的数据及清除缓存这几个关键的功能点。 通常在大型项目里,会把代码进行分层和分工。核心开发人员和技术经理编写核心的流程和代码,往往是以接口的形式给出,而基础开发人员则针对这些接口,填充代码,如数据库操作等。这样,核心技术人员把更多精力投入到了技术攻关和业务逻辑中。前端针对接口编程,只管在Action层调用Service,不管实现细节;而后端则要负责Dao和Service层接口实现。这样,就实现了代码分工与合作。 1.4.2 对PHP接口的思考 PHP的接口自始至终一直在被争议,有人说接口很好,有人说接口像鸡肋。首先要明白,好和不好的判断标准是什么。无疑,这是和Java/C++相比。在上面的例子中,已经讨论了PHP的接口在“面向契约编程”中是不足的,并没有起到应有的作用。 其实,在上面的interface.php代码中,machine类的声明应该在plain类前面。接口提供了一套规范,这是系统提供的,然后machine类提供一组针对接口的API并实现,最后才是自定义的类。在Java里,接口之所以盛行(多线程的runable接口、容器的collection接口等)就是因为系统为我们做了前面两部分的工作,而程序员,只需要去写具体的实现类,就能保证接口可用可控。 为什么要用接口?接口到底有什么好处?接口本身并不提供实现,只是提供一个规范。如果我们知道一个类实现了某个接口,那么就知道了可以调用该接口的哪些方法,我们只需要知道这些就够了。 PHP中,接口的语义是有限的,使用接口的地方并不多,PHP中接口可以淡化为设计文档,起到一个团队基本契约的作用,代码清单1-12所示。 代码清单1-12 cache_imp.php <?php interface cache{ /** @describe:缓存管理,项目经理定义接口,技术人员负责实现 **/ const maxKey = 10000; // 最大缓存量 public function getc($key);// 获取缓存 public function setc($key,$value);// 设置缓存 public function flush();// 清空缓存 } 由于PHP是弱类型,且强调灵活,所以并不推荐大规模使用接口,而是仅在部分“内核”代码中使用接口,因为PHP中的接口已经失去很多接口应该具有的语义。从语义上考虑,可以更多地使用抽象类。至于抽象类和接口的比较,不再赘述。 另外,PHP5对面向对象的特性做了许多增强,其中就有一个SPL(标准PHP库)的尝试。SPL中实现一些接口,其中最主要的就是Iterator迭代器接口,通过实现这个接口,就能使对象能够用于foreach结构,从而在使用形式上比较统一。比如SPL中有一个DirectoryIterator类,这个类在继承SplFileInfo类的同时,实现Iterator、Traversable、SeekableIterator这三个接口,那么这个类的实例可以获得父类SplFileInfo的全部功能外,还能够实现Iterator接口所展示的那些操作。 Iterator接口的原型如下: * current() This method returns the current index’s value.You are solely responsible for tracking what the current index is as the interface does not do this for you. * key() This method returns the value of the current index’s key.For foreach loops this is extremely important so that the key value can be populated. * next() This method moves the internal index forward one entry. * rewind() This method should reset the internal index to the first element. * valid() This method should return true or false if there is a current element.It is called after rewind() or next(). 如果一个类声明了实现Iterator接口,就必须实现这五个方法,如果实现了这五个方法,那么就可以很容易对这个类的实例进行迭代。这里,DirectoryIterator类之所以拿来就能用,是因为系统已经实现了Iterator接口,所以可以像下面这样使用: <?php $dir = new DirectoryIterator(dirname(FILE)); foreach ($dir as $fileinfo) { if (!$fileinfo->isDir()) { echo $fileinfo->getFilename(),"\t",$fileinfo->getSize(),PHP_EOL; } } 可以想象,如果不用DirectoryIterator类,而是自己实现,不但代码量增加了,而且循环时候的风格也不统一了。如果自己写的类也实现了Iterator接口,那么就可以像Iterator那样工作。 为什么一个类只要实现了Iterator迭代器,其对象就可以被用作foreach的对象呢?其实原因很简单,在对PHP实例对象使用foreach语法时,会检查这个实例有没有实现Iterator接口,如果实现了,就会通过内置方法或使用实现类中的方法模拟foreach语句。这是不是和前面提到的toString方法的实现很像呢?事实上,toString方法就是接口的一种变相实现。 接口就是这样,接口本身什么也不做,系统悄悄地在内部实现了接口的行为,所以只要实现这个接口,就可以使用接口提供的方法。这就是接口“即插即用”思想。 我们都知道,接口是对多重继承的一种变相实现,而在讲继承时,我们提到了用来实现混入(Mixin)式的Traits,实际上,Traits可以被视为一种加强型的接口。 来看一段代码: <?php trait Hello { public function sayHello() { echo 'Hello'; } } trait World { public function sayWorld() echo 'World'; } } class MyHelloWorld { use Hello, World; public function sayExclamationMark() { echo '!'; } } $o = new MyHelloWorld(); $o->sayHello(); $o->sayWorld(); $o->sayExclamationMark(); ?> 上面的代码运行结果如下: Hello World! 这里的MyHelloWorld同时实现了两个Traits,从而使其可以分别调用两个Traits里的代码段。从代码中就可以看出,Traits和接口很像,不同的是Traits是可以导入包含代码的接口。从某种意义上来说,Traits和接口都是对“多重继承”的一种变相实现。 总结关于接口的几个概念: 接口作为一种规范和契约存在。作为规范,接口应该保证可用性;作为契约,接口应该保证可控性。 接口只是一个声明,一旦使用interface关键字,就应该实现它。可以由程序员实现(外部接口),也可以由系统实现(内部接口)。接口本身什么都不做,但是它可以告诉我们它能做什么。 PHP中的接口存在两个不足,一是没有契约限制,二是缺少足够多的内部接口。 接口其实很简单,但是接口的各种应用很灵活,设计模式中也有很大一部分是围绕接口展开的。 1.5.1 如何使用反射API 1.5 反射 面向对象编程中对象被赋予了自省的能力,而这个自省的过程就是反射。 反射,直观理解就是根据到达地找到出发地和来源。比方说,我给你一个光秃秃的对象,我可以仅仅通过这个对象就能知道它所属的类、拥有哪些方法。 反射指在PHP运行状态中,扩展分析PHP程序,导出或提取出关于类、方法、属性、参数等的详细信息,包括注释。这种动态获取信息以及动态调用对象方法的功能称为反射API。 1.5.1 如何使用反射API 以1.1节的代码为模板,直观地认识反射的使用,如代码清单1-13所示。 代码清单1-13 reflection.php <?php class person{ public $name; public $gender; public function say(){ echo $this->name," \tis ",$this->gender,"\r\n"; } public function set($name, $value) { echo "Setting $name to $value \r\n"; $this->$name= $value; } public function get($name) { if(!isset($this->$name)){ echo '未设置'; $this->$name="正在为你设置默认值"; } return $this->$name; } } $student=new person(); $student->name='Tom'; $student->gender='male'; $student->age=24; 现在,要获取这个student对象的方法和属性列表该怎么做呢?如以下代码所示: // 获取对象属性列表 $reflect = new ReflectionObject($student); $props = $reflect->getProperties(); foreach ($props as $prop) { print $prop->getName() ."\n"; } // 获取对象方法列表 $m=$reflect->getMethods(); foreach ($m as $prop) { print $prop->getName() ."\n"; } 也可以不用反射API,使用class函数,返回对象属性的关联数组以及更多的信息: // 返回对象属性的关联数组 var_dump(get_object_vars($student)); // 类属性 var_dump(get_class_vars(get_class($student))); // 返回由类的方法名组成的数组 var_dump(get_class_methods(get_class($student))); 假如这个对象是从其他页面传过来的,怎么知道它属于哪个类呢?一句代码就可以搞定: // 获取对象属性列表所属的类 echo get_class($student); 反射API的功能显然更强大,甚至能还原这个类的原型,包括方法的访问权限,如代码清单1-14所示。 代码清单1-14 使用反射API // 反射获取类的原型 $obj = new ReflectionClass('person'); $className = $obj->getName(); $Methods = $Properties = array(); foreach($obj->getProperties() as $v) { $Properties[$v->getName()] = $v; } foreach($obj->getMethods() as $v) { $Methods[$v->getName()] = $v; } echo "class {$className}\n{\n"; is_array($Properties)&&ksort($Properties); foreach($Properties as $k => $v) { echo "\t"; echo $v->isPublic() ? ' public' : '',$v->isPrivate() ? ' private' : '', $v->isProtected() ? ' protected' : '', $v->isStatic() ? ' static' : ''; echo "\t{$k}\n"; } echo "\n"; if(is_array($Methods)) ksort($Methods); foreach($Methods as $k => $v) { echo "\tfunction {$k}(){}\n"; } echo "}\n"; 输出如下: class person { public gender public name function get(){} function set(){} function say(){} } 不仅如此,PHP手册中关于反射API更是有几十个,可以说,反射完整地描述了一个类或者对象的原型。反射不仅可以用于类和对象,还可以用于函数、扩展模块、异常等。 1.5.2 反射有什么作用 反射可以用于文档生成。因此可以用它对文件里的类进行扫描,逐个生成描述文档。 既然反射可以探知类的内部结构,那么是不是可以用它做hook实现插件功能呢?或者是做动态代理呢?抛砖引玉,代码清单1-15是个简单的举例。 代码清单1-15 proxy.php <?php class mysql { function connect($db) { echo "连接到数据库${db[0]}\r\n"; } } class sqlproxy { private $target; function construct($tar) { $this->target[] = new $tar(); } function call($name, $args) { foreach ($this->target as $obj) { $r = new ReflectionClass($obj); if ($method = $r->getMethod($name)) { if ($method->isPublic() && !$method->isAbstract()) { echo "方法前拦截记录LOG\r\n"; $method->invoke($obj, $args); echo "方法后拦截\r\n"; } } } } } $obj = new sqlproxy('mysql'); $obj->connect('member'); 这里简单说明一下,真正的操作类是mysql类,但是sqlproxy类实现了根据动态传入参数,代替实际的类运行,并且在方法运行前后进行拦截,并且动态地改变类中的方法和属性。这就是简单的动态代理。 在平常开发中,用到反射的地方不多:一个是对对象进行调试,另一个是获取类的信息。在MVC和插件开发中,使用反射很常见,但是反射的消耗也很大,在可以找到替代方案的情况下,就不要滥用。 PHP有Token函数,可以通过这个机制实现一些反射功能。从简单灵活的角度讲,使用已经提供的反射API是可取的。 很多时候,善用反射能保持代码的优雅和简洁,但反射也会破坏类的封装性,因为反射可以使本不应该暴露的方法或属性被强制暴露了出来,这既是优点也是缺点。 思考题 为什么要使用反射,反射存在的必要性是什么?或者说,反射为什么会存在? (已知一些情况:C语言是面向过程的编程语言,PHP、C++、Java是具有面向对象风格的编程语言。C语言和C++中没有对反射的原生支持,而PHP和Java具有反射API。可以思考一下,为什么C/C++语言里没有反射,以及C/C++语言里是否需要反射?) 1.6.1 如何使用异常处理机制 1.6 异常和错误处理 在语言级别上,通常具有许多错误处理模式,但这些模式往往建立在约定俗成的基础上,也就是说这些错误都是预知的。但是在大型程序中,如果每次调用都去逐一检查错误,会使代码变得冗长复杂,到处充斥着if…else,并且严重降低代码的可读性。而且人的因素也是不可信赖的,程序员可能并不会把这些问题当一回事,从而导致业务异常。在这种背景下,就逐渐形成了异常处理机制,或者强迫消除这些问题,或者把问题提交给能解决它的环境。这就把“描述在正常过程中做什么事的代码”和“出了问题怎么办的代码”进行分离。 1.6.1 如何使用异常处理机制 异常的思想最早可以追溯到20世纪60年代,其在C++、Java中发扬光大,PHP则部分借鉴了这两种语言的异常处理机制。 PHP里的异常,是程序运行中不符合预期的情况及与正常流程不同的状况。一种不正常的情况,就是按照正常逻辑不该出错,但仍然出错的情况,这属于逻辑和业务流程的一种中断,而不是语法错误。PHP里的错误则属于自身问题,是一种非法语法或者环境问题导致的、让编译器无法通过检查甚至无法运行的情况。 在各种语言里,异常(exception)和错误(error)的概念是不一样的。在PHP里,遇到任何自身错误都会触发一个错误,而不是抛出异常(对于一些情况,会同时抛出异常和错误)。PHP一旦遇到非正常代码,通常都会触发错误,而不是抛出异常。在这个意义上,如果想使用异常处理不可预料的问题,是办不到的。比如,想在文件不存在且数据库连接打不开时触发异常,是不可行的。这在PHP里把它作为错误抛出,而不会作为异常自动捕获。 以经典的除零问题为例,如代码清单1-16所示。 代码清单1-16 exception.php // exception.php <?php $a=null; try{ $a=5/0; echo $a,PHP_EOL; }catch(exception $e){ $e->getMessage(); $a=-1; } echo $a; 运行结果如图1-8所示。 代码清单1-17所示是Java代码。 代码清单1-17 ExceptionTry.java // ExceptionTry.java public class ExcepetionTry { public static void tp() throws ArithmeticException{ int a; a=5/0; System.out.println("运算结果:"+a); } public static void main(String[] args) { int a; try { a=5/0; System.out.println("运算结果:"+a); } catch (ArithmeticException e) { e.printStackTrace(); }finally{ a = -1; System.out.println("运算结果:"+a); } try { ExcepetionTry.tp(); } catch (Exception e) { System.out.println("异常被捕获"); } } } 运行结果如图1-9所示。 把tp方法中的第二条语句改为如下形式: a=5/1; 修改后的结果如图1-10所示。 由以上运行结果可以看到,对于除零这种“异常”情况,PHP认为这是一个错误,直接触发错误(warning也是错误,只是错误等级不一样),而不会自动抛出异常使程序进入异常流程,故最终$a值并不是预想中的-1,也就是说,并没有进入异常分支,也没有处理异常。PHP只有你主动throw后,才能捕获异常(一般情况是这样,也有一些异常PHP可以自动捕获)。 而对于Java,则认为除零属于ArithmeticException,会对其进行捕获,并对异常进行处理。 也就是说,PHP通常是无法自动捕获有意义的异常的,它把所有不正常的情况都视作了错误,你要想捕获这个异常,就得使用if…else结构,保证代码是正常的,然后判断如果除数为0,则手动抛出异常,再捕获。Java有一套完整的异常机制,内置很多异常类会自动捕获各种各样的异常。但PHP这个机制不完善。PHP内建的常见异常类主要有pdoexception、reflection exception。 注意 其实PHP和Java之间之所以有这个差距,根本原因就在于,在Java里,异常是唯一的错误报告方式,而在PHP中却不是这样。通俗一点讲,就是这两种语言对异常和错误的界定存在分歧。什么是异常,什么是错误,两种语言的设计者存在不同的观点。 也就是说,PHP只有手动抛出异常后才能捕获异常,或者是有内建的异常机制时,会先触发错误,再捕获异常。那么PHP里的异常用法应该是什么样的呢?看下面的例子。 先定义两个异常类,它们需要继承自系统的exception,如代码清单1-18所示。 代码清单1-18 定义两个异常类 class emailException extends exception{ } class pwdException extends exception{ function toString() { return " Exception{$this->getCode()}: {$this->getMessage()} in File:{$this->getFile()}on line:{$this->getLine()}"; // 改写抛出异常结果 } } 然后就是实际的业务,根据业务需求抛出不同异常,如代码清单1-19所示。 代码清单1-19 根据业务需求抛出不同异常 function reg($reginfo=null){ if(empty($reginfo)||!isset($reginfo)){ throw new Exception("参数非法"); } if(empty($reginfo['email'])){ throw new emailException("邮件为空"); } if($reginfo['pwd']!=$reginfo['repwd']){ throw new pwdException("两次密码不一致"); } echo '注册成功'; } 上面的代码判断传入的参数,根据业务进行异常分发。首先,如果没有传入任何参数(这个参数可以是POST进来,也可以是别的地方赋值得到),就把异常分发给exception超类,跳出注册流程;如果Email地址不存在,那么把异常分发给自定义的emailException异常,跳出注册流程;如果两次密码不一致,则将异常分发给自定义的pwdException,跳出注册流程。 现在异常分发了,但还不算完,还需要对异常进行分拣并做处理。代码如下所示: try{ reg(array('email'=>'waitfox@qq.com','pwd'=>123456,'repwd'=>12345678)); // reg(); }catch(emailException $ee){ echo $ee->getMessage(); }catch(pwdException $ep){ echo $ep; echo PHP_EOL,'特殊处理'; }catch(Exception $e){ echo $e->getTraceAsString(); echo PHP_EOL,'其他情况,统一处理'; } 这一段代码用于捕获所抛出的各种异常,进行分门别类的处理。 提示 可以尝试不同注册条件,看看异常分拣的流程。需要注意,exception作为超类应该放在最后捕获。不然,捕获这个异常超类后,后面的捕获就终止了,而这个超类不能提供针对性的信息和处理。 在这里,对表单进行异常处理,通过重写异常类、手动抛出错误的方式进行异常处理。这是一种业务异常,可以人为地把所有不符合要求的情况都视作业务异常,和通常意义上的代码异常相区别。 那PHP里的异常应该怎么用?在什么时候抛出异常,什么时候捕获呢?什么场景下能应用异常?在下面三种场景下会用到异常处理机制。 1.对程序的悲观预测 如果一个程序员对自己的代码有“悲观情绪”,这里并不是指该程序员代码质量不高,而是他认为自己的代码无法一一处理各种可预见、不可预见的情况,那该程序员就会进行异常处理。假设一个场景,程序员悲观地认为自己的这段代码在高并发条件下可能产生死锁,那么他就会悲观地抛出异常,然后在死锁时进行捕获,对异常进行细致的处理。 2.程序的需要和对业务的关注 如果程序员希望业务代码中不会充斥大堆的打印、调试等处理,通常他们会使用异常机制;或者业务上需要定义一些自己的异常,这个时候就需要自定义一个异常,对现实世界中各种各样的业务进行补充。比如上班迟到,这种情况认为是一个异常,要收集起来,到月底集中处理,扣你工资;如果程序员希望有预见性地处理可能发生的、会影响正常业务的代码,那么它需要异常。在这里,强调了异常是业务处理中必不可少的环节,不能对异常视而不见。异常机制认为,数据一致很重要,在数据一致性可能被破坏时,就需要异常机制进行事后补救。 举个例子,比如有个上传文件的业务需求,要把上传的文件保存在一个目录里,并在数据库里插入这个文件的记录,那么这两步就是互相关联、密不可分的一个集成的业务,缺一不可。文件保存失败,而插入记录成功就会导致无法下载文件;而文件保存成功数据库写入失败,则会导致没有记录的文件成为死文 正文结束 |
没有上一篇 | 1面向对象思想的核心概念3 PHP核心技术与最佳实践 |