使用 PHP V5 的新语言特性,可以明显地提高代码的可维护性和可靠性。通过阅读本文,您将了解如何利用这些新特性将用 PHP V4 开发的代码迁移到 PHP V5。
PHP V5 在 PHP V4 基础上做了重大改进。新语言特性使构建可靠的类库和维护类库更加容易。另外,重写标准库帮助使 PHP 更符合其同一 Web 语系,例如 Java™ 编程语言。让我们来看一些 PHP 新的面向对象特性,并了解如何将现有 PHP V4 代码迁移到 PHP V5。
首先,先来了解新语言特性及 PHP 的创建程序怎样更改了用 PHP V4 创建对象的方法。用 V5 的想法是要创建一种工业级语言用于 Web 应用程序开发。那意味着要了解 PHP V4 的限制,然后从其他语言中 (例如 Java、<font face="NSimsun">C#</font>
、<font face="NSimsun">C++</font>
、Ruby 和 Perl 语言) 抽取已知优秀语言架构并将这些架构并入 PHP 中。
第一个也是最重要的新特性是针对类的方法和实例变量的访问保护 —— <font face="NSimsun">public</font>
、<font face="NSimsun">protected</font>
和 <font face="NSimsun">private</font>
关键字。 这个新特性使类设计人员可以保证对类的内在特性的控制,同时告诉类的使用者哪些类可以而哪些类不可以触及。
在 PHP V4 中,所有代码都是 <font face="NSimsun">public</font>
的。在 PHP V5 中,类设计人员可以声明哪些代码是对外部可见的 (<font face="NSimsun">public</font>
) 而哪些代码仅对类内部可见 (<font face="NSimsun">private</font>
) 或仅对类的子类可见 (<font face="NSimsun">protected</font>
)。如果没有这些访问控制,则在大型团队中开发代码或将代码分布为库的工作会受阻,因为那些类的使用者很可能使用错误的方法或访问应当为 private 成员变量的代码。
另一个较大的新功能是关键字 <font face="NSimsun">interface</font>
和 <font face="NSimsun">abstract</font>
,这两个关键字允许进行契约编程。契约编程 意味着一个类向另一个类提供一张契约 —— 换言之: “这是我要做的工作,你不需要知道它是怎样完成的”。 实现 interface 的所有类都遵循该契约。interface 的所有使用者都同意仅使用 interface 中指定的方法。abstract 关键字使得使用接口十分容易,我稍后将加以说明。
这两个主要特性 —— 访问控制和契约编程 —— 允许大型编码人员团队更顺畅地使用大型代码库。这些特性还使 IDE 可以提供更丰富的语言智能特性集。本文不但说明了若干个迁移问题,而且还花了一些时间说明如何使用这些新主要语言特性。
访问控制
为了演示新语言特性,我使用了一个名为 <font face="NSimsun">Configuration</font>
的类。这个简单的类中含有用于 Web 应用程序的配置项 —— 例如,指向图片目录的路径。在理想的情况下,此信息将驻存在一个文件或数据库里。清单 1 显示了一个简化的版本。
<?php class Configuration { var $_items = array(); function Configuration() { $this->_items[ 'imgpath' ] = 'images'; } function get( $key ) { return $this->_items[ $key ]; } } $c = new Configuration(); echo( $c->get( 'imgpath' )."\n" ); ?>
这是一个完全正统的 PHP V4 类。成员变量保存配置项的列表,构造程序装入项,然后名为 <font face="NSimsun">get()</font>
的访问方法返回项的值。
运行脚本后,以下代码将显示在命令行中:
% php access.php4 images %
很好!这个结果意味着代码运行正常并且正常设定和读取了 <font face="NSimsun">imgpath</font>
配置项的值。
将这个类转换为 PHP V5 的第一步是要将构造程序重命名。在 PHP V5 中,初始化对象 (构造程序) 的方法称为 <font face="NSimsun">__construct</font>
。这次小改动如下所示。
<?php class Configuration { var $_items = array(); function __construct() { $this->_items[ 'imgpath' ] = 'images'; } function get( $key ) { return $this->_items[ $key ]; } } $c = new Configuration(); echo( $c->get( 'imgpath' )."\n" ); ?>
这次改动并不大。只是移至 PHP V5 约定。下一步是添加对类的访问控制以确保类的使用者无法直接读写 <font face="NSimsun">$_items</font>
成员变量。这次改动如下所示。
<?php class Configuration { private $_items = array(); public function __construct() { $this->_items[ 'imgpath' ] = 'images'; } public function get( $key ) { return $this->_items[ $key ]; } } $c = new Configuration(); echo( $c->get( 'imgpath' )."\n" ); ?>
如果这个对象的使用者都要直接访问项阵列,访问将被拒绝,因为该阵列被标记为 <font face="NSimsun">private</font>
。幸运的是,使用者发现 <font face="NSimsun">get()</font>
方法可以提供广受欢迎的读取权限。
为了说明如何使用 <font face="NSimsun">protected</font>
权限,我需要另一个类,该类必须继承自 <font face="NSimsun">Configuration</font>
类。我把那个类称为 <font face="NSimsun">DBConfiguration</font>
,并假定该类将从数据库中读取配置值。此设置如下所示。
<?php class Configuration { protected $_items = array(); public function __construct() { $this->load(); } protected function load() { } public function get( $key ) { return $this->_items[ $key ]; } } class DBConfiguration extends Configuration { protected function load() { $this->_items[ 'imgpath' ] = 'images'; } } $c = new DBConfiguration(); echo( $c->get( 'imgpath' )."\n" ); ?>
这张清单显示了 <font face="NSimsun">protected</font>
关键字的正确用法。基类定义了名为 <font face="NSimsun">load()</font>
的方法。此类的子类将覆盖 <font face="NSimsun">load()</font>
方法把数据添加到 <font face="NSimsun">items</font>
表中。<font face="NSimsun">load()</font>
方法对类及其子类是内部方法,因此该方法对所有外部使用者都不可见。如果关键字都是 <font face="NSimsun">private</font>
的,则 load() 方法不能被覆盖。
我并不十分喜欢此设计,但是,由于必须让 <font face="NSimsun">DBConfiguration</font>
类能够访问项阵列而选用了此设计。我希望继续由 <font face="NSimsun">Configuration</font>
类来完全维护项阵列,以便在添加其他子类后,那些类将不需要知道如何维护项阵列。我做了以下更改。
<?php class Configuration { private $_items = array(); public function __construct() { $this->load(); } protected function load() { } protected function add( $key, $value ) { $this->_items[ $key ] = $value; } public function get( $key ) { return $this->_items[ $key ]; } } class DBConfiguration extends Configuration { protected function load() { $this->add( 'imgpath', 'images' ); } } $c = new DBConfiguration(); echo( $c->get( 'imgpath' )."\n" ); ?>
现在,项阵列可以是 private 的,因为子类使用受保护的 <font face="NSimsun">add()</font>
方法将配置项添加到列表中。<font face="NSimsun">Configuration</font>
类可以更改存储和读取配置项的方法而不需要考虑它的子类。只要 <font face="NSimsun">load()</font>
和 <font face="NSimsun">add()</font>
方法以同样的方法运行,子类就应当不会出问题。
对于我来说,增加了访问控制是考虑移至 PHP V5 的主要原因。难道就因为 Grady Booch 说 PHP V5 是四大面向对象的语言之一么?不,因为我曾经接受了一个任务来维护 100KLOC <font face="NSimsun">C++</font>
代码,在这些代码中所有方法和成员都被定义为 public 的。我花了三天时间来清除这些定义,并在清除过程中,明显地减少了错误数并提高了可维护性。为什么?因为没有访问控制,就不可能知道对象怎样使用其他对象,也就不可能在不知道要突破什么难关的情况下做任何更改。使用 <font face="NSimsun">C++</font>
,至少我还有编译程序可用。PHP 没有配备编译程序,因此这类访问控制变得愈加重要。
契约编程
从 PHP V4 迁移到 PHP V5 时要利用的下一个重要特性是支持通过接口、抽象类和方法进行契约编程。清单 6 显示了一个版本的 <font face="NSimsun">Configuration</font>
类,在该类中 PHP V4 编码人员尝试了构建基本接口而根本不使用 <font face="NSimsun">interface</font>
关键字。
<?php class IConfiguration { function get( $key ) { } } class Configuration extends IConfiguration { var $_items = array(); function Configuration() { $this->load(); } function load() { } function get( $key ) { return $this->_items[ $key ]; } } class DBConfiguration extends Configuration { function load() { $this->_items[ 'imgpath' ] = 'images'; } } $c = new DBConfiguration(); echo( $c->get( 'imgpath' )."\n" ); ?>
清单开始于一个小型 <font face="NSimsun">IConfiguration</font>
类,该类定义所有 <font face="NSimsun">Configuration</font>
类或派生类所提供的接口。此接口将在类与其所有使用者之间定义契约。契约声明了实现 <font face="NSimsun">IConfiguration</font>
的所有类必须配有 <font face="NSimsun">get()</font>
方法并且 <font face="NSimsun">IConfiguration</font>
的所有使用者都必须坚持仅使用 <font face="NSimsun">get()</font>
方法。
下面的这段代码是在 PHP V5 中运行的,但最好使用提供的接口系统,如下所示。
<?php interface IConfiguration { function get( $key ); } class Configuration implements IConfiguration { ... } class DBConfiguration extends Configuration { ... } $c = new DBConfiguration(); echo( $c->get( 'imgpath' )."\n" ); ?>
一方面,读者可以更清楚地了解运行状况;另一方面,单个类可以实现多个接口。清单 8 显示了如何扩展 <font face="NSimsun">Configuration</font>
类来实现 <font face="NSimsun">Iterator</font>
接口,对于 PHP 来说,该接口是内部接口。
<?php interface IConfiguration { ... } class Configuration implements IConfiguration, Iterator { private $_items = array(); public function __construct() { $this->load(); } protected function load() { } protected function add( $key, $value ) { $this->_items[ $key ] = $value; } public function get( $key ) { return $this->_items[ $key ]; } public function rewind() { reset($this->_items); } public function current() { return current($this->_items); } public function key() { return key($this->_items); } public function next() { return next($this->_items); } public function valid() { return ( $this->current() !== false ); } } class DBConfiguration extends Configuration { ... } $c = new DBConfiguration(); foreach( $c as $k => $v ) { echo( $k." = ".$v."\n" ); } ?>
<font face="NSimsun">Iterator</font>
接口使所有类都可以看似是其使用者的阵列。正如您在脚本末尾看到的那样,您可以使用 <font face="NSimsun">foreach</font>
运算符重申 <font face="NSimsun">Configuration</font>
对象中的所有配置项。PHP V4 没有这种功能,但您可以在应用程序中通过各种方式使用此功能。
接口机制的优点是可以将契约快速集中在一起而无须实现任何方法。最后阶段是实现接口,您必须实现所有指定的方法。PHP V5 中另一个有帮助的新功能是 抽象类,使用抽象类可以轻松地用一个基类实现接口的核心部分,然后用该接口创建实体类。
抽象类的另一个用途是为多个派生类创建一个基类,在这些派生类中,基类决不会被实例化。例如,当 <font face="NSimsun">DBConfiguration</font>
和 <font face="NSimsun">Configuration</font>
同时存在时,则只能使用 <font face="NSimsun">DBConfiguration</font>
。<font face="NSimsun">Configuration</font>
类只是一个基类 —— 一个抽象类。因此,您可以使用 <font face="NSimsun">abstract</font>
关键字强制该行为,如下所示。
<?php abstract class Configuration { protected $_items = array(); public function __construct() { $this->load(); } abstract protected function load(); public function get( $key ) { return $this->_items[ $key ]; } } class DBConfiguration extends Configuration { protected function load() { $this->_items[ 'imgpath' ] = 'images'; } } $c = new DBConfiguration(); echo( $c->get( 'imgpath' )."\n" ); ?>
现在,所有要将 <font face="NSimsun">Configuration</font>
类型的对象实例化的尝试都会出错,因为系统认为该类是抽象的并且不完整。
静态方法和成员
PHP V5 中的另一个重要的新功能是支持对类使用静态成员和方法。通过使用这种功能,您可以使用流行的单例模式。这种模式对于 <font face="NSimsun">Configuration</font>
类是十分理想的,因为应用程序应当仅有一个配置对象。
清单 10 显示了 PHP V5 版的 <font face="NSimsun">Configuration</font>
类作为一个单例。
<?php class Configuration { private $_items = array(); static private $_instance = null; static public function get() { if ( self::$_instance == null ) self::$_instance = new Configuration(); return self::$_instance; } private function __construct() { $this->_items[ 'imgpath' ] = 'images'; } public function __get( $key ) { return $this->_items[ $key ]; } } echo( Configuration::get()->{ 'imgpath' }."\n" ); ?>
<font face="NSimsun">static</font>
关键字有很多用法。当需要访问单个类型的所有对象的某些全局数据时,请考虑使用此关键字。
Magic Method
PHP V5 中的另一个很大的新功能是支持 magic method,使用这些方法使对象可以迅速更改对象的接口 —— 例如,为 <font face="NSimsun">Configuration</font>
对象中的每个配置项添加成员变量。无须使用 <font face="NSimsun">get()</font>
方法,只要寻找一个特殊项将它当作一个阵列,如下所示。
<?php class Configuration { private $_items = array(); function __construct() { $this->_items[ 'imgpath' ] = 'images'; } function __get( $key ) { return $this->_items[ $key ]; } } $c = new Configuration(); echo( $c->{ 'imgpath' }."\n" ); ?>
在本例中,我创建了新的 <font face="NSimsun">__get()</font>
方法,只要使用者寻找对象上的成员变量时即调用此方法。然后,方法中的代码将使用项阵列来查找值并返回该值,就像有一个专门用于该关键字的成员变量在那儿一样。假定对象就是一个阵列,在脚本的末尾,您可以看到使用 <font face="NSimsun">Configuration</font>
对象就像寻找 <font face="NSimsun">imgpath</font>
的值一样简单。
从 PHP V4 迁移到 PHP V5 时,必须要注意这些在 PHP V4 中完全不可用的语言特性,还必须重新验证类来查看可以怎样使用这些类。
异常
最后介绍 PHP V5 中的新异常机制来结束本文。异常为考虑错误处理提供了一种全新的方法。所有程序都不可避免地会生成错误 —— 找不到文件、内存不足等等。如果不使用异常,则必须返回错误代码。请看下面的 PHP V4 代码。
<?php function parseLine( $l ) { // ... return array( 'error' => 0, data => array() // data here ); } function readConfig( $path ) { if ( $path == null ) return -1; $fh = fopen( $path, 'r' ); if ( $fh == null ) return -2; while( !feof( $fh ) ) { $l = fgets( $fh ); $ec = parseLine( $l ); if ( $ec['error'] != 0 ) return $ec['error']; } fclose( $fh ); return 0; } $e = readConfig( 'myconfig.txt' ); if ( $e != 0 ) echo( "There was an error (".$e.")\n" ); ?>
这段标准的文件 I/O 代码将读取一个文件,检索一些数据,并在遇到任何错误时返回错误代码。对于这个脚本,我有两个问题。第一个是错误代码。这些错误代码的含义是什么?要找出这些错误代码的含义,则必须创建另一个系统将这些错误代码映射到有含义的字符串中。第二个问题是 <font face="NSimsun">parseLine</font>
的返回结果十分复杂。我只需要它返回数据,但它实际上必须返回错误代码 和 数据。大多数工程师 (包括我本人在内) 经常偷懒,仅返回数据,而忽略掉错误,因为错误很难管理。
清单 13 显示了使用异常时代码的清晰程度。
<?php function parseLine( $l ) { // Parses and throws and exception when invalid return array(); // data } function readConfig( $path ) { if ( $path == null ) throw new Exception( 'bad argument' ); $fh = fopen( $path, 'r' ); if ( $fh == null ) throw new Exception( 'could not open file' ); while( !feof( $fh ) ) { $l = fgets( $fh ); $ec = parseLine( $l ); } fclose( $fh ); } try { readConfig( 'myconfig.txt' ); } catch( Exception $e ) { echo( $e ); } ?>
我无需考虑错误代码问题,因为异常中包含了错误的说明性文字。我也无需考虑如何追踪从 <font face="NSimsun">parseLine</font>
返回的错误代码,因为如果出现错误,该函数将只抛出一个错误。堆栈将延伸至最近的 <font face="NSimsun">try/catch</font>
块,该块位于脚本的底部。
异常机制将彻底改变编写代码的方法。您不必管理让人头痛的错误代码和映射,可以将精力集中在要处理的错误上。这样的代码更易于阅读、维护,而且我要说,甚至要鼓励您添加错误处理机制,它通常都能带来好处。
结束语
新的面向对象特性和异常处理的增加为将代码从 PHP V4 迁移到 PHP V5 提供了强有力的理由。正如您所见,升级过程并不难。扩展到 PHP V5 的语法感觉就像 PHP 一样。是的,这些语法来自诸如 Ruby 之类的语言,但我认为它们配合得非常好。并且这些语言将 PHP 的范围从一种用于小型站点的脚本语言扩展为可用于完成企业级应用的语言。