使用 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、C#
、C++
、Ruby 和 Perl 語言) 抽取已知優秀語言架構並將這些架構並入 PHP 中。
第一個也是最重要的新特性是針對類的方法和實例變量的訪問保護 —— public
、protected
和 private
關鍵字。 這個新特性使類設計人員可以保證對類的內在特性的控制,同時告訴類的使用者哪些類可以而哪些類不可以觸及。
在 PHP V4 中,所有代碼都是 public
的。在 PHP V5 中,類設計人員可以聲明哪些代碼是對外部可見的 (public
) 而哪些代碼僅對類內部可見 (private
) 或僅對類的子類可見 (protected
)。如果沒有這些訪問控制,則在大型團隊中開發代碼或將代碼分布為庫的工作會受阻,因為那些類的使用者很可能使用錯誤的方法或訪問應當為 private 成員變量的代碼。
另一個較大的新功能是關鍵字 interface
和 abstract
,這兩個關鍵字允許進行契約編程。契約編程 意味著一個類向另一個類提供一張契約 —— 換言之: “這是我要做的工作,你不需要知道它是怎樣完成的”。 實現 interface 的所有類都遵循該契約。interface 的所有使用者都同意僅使用 interface 中指定的方法。abstract 關鍵字使得使用接口十分容易,我稍後將加以說明。
這兩個主要特性 —— 訪問控制和契約編程 —— 允許大型編碼人員團隊更順暢地使用大型代碼庫。這些特性還使 IDE 可以提供更豐富的語言智能特性集。本文不但說明了若干個遷移問題,而且還花了一些時間說明如何使用這些新主要語言特性。
訪問控制
為了演示新語言特性,我使用了一個名為 Configuration
的類。這個簡單的類中含有用於 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 類。成員變量保存配置項的列表,構造程序裝入項,然後名為 get()
的訪問方法返回項的值。
運行腳本後,以下代碼將顯示在命令行中:
% php access.php4 images %
很好!這個結果意味著代碼運行正常並且正常設定和讀取了 imgpath
配置項的值。
將這個類轉換為 PHP V5 的第一步是要將構造程序重命名。在 PHP V5 中,初始化對象 (構造程序) 的方法稱為 __construct
。這次小改動如下所示。
<?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 約定。下一步是添加對類的訪問控制以確保類的使用者無法直接讀寫 $_items
成員變量。這次改動如下所示。
<?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" ); ?>
如果這個對象的使用者都要直接訪問項陣列,訪問將被拒絕,因為該陣列被標記為 private
。幸運的是,使用者發現 get()
方法可以提供廣受歡迎的讀取權限。
為了說明如何使用 protected
權限,我需要另一個類,該類必須繼承自 Configuration
類。我把那個類稱為 DBConfiguration
,並假定該類將從數據庫中讀取配置值。此設置如下所示。
<?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" ); ?>
這張清單顯示了 protected
關鍵字的正確用法。基類定義了名為 load()
的方法。此類的子類將覆蓋 load()
方法把數據添加到 items
表中。load()
方法對類及其子類是內部方法,因此該方法對所有外部使用者都不可見。如果關鍵字都是 private
的,則 load() 方法不能被覆蓋。
我並不十分喜歡此設計,但是,由於必須讓 DBConfiguration
類能夠訪問項陣列而選用了此設計。我希望繼續由 Configuration
類來完全維護項陣列,以便在添加其他子類後,那些類將不需要知道如何維護項陣列。我做了以下更改。
<?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 的,因為子類使用受保護的 add()
方法將配置項添加到列表中。Configuration
類可以更改存儲和讀取配置項的方法而不需要考慮它的子類。只要 load()
和 add()
方法以同樣的方法運行,子類就應當不會出問題。
對於我來說,增加了訪問控制是考慮移至 PHP V5 的主要原因。難道就因為 Grady Booch 說 PHP V5 是四大面向對象的語言之一麼?不,因為我曾經接受了一個任務來維護 100KLOC C++
代碼,在這些代碼中所有方法和成員都被定義為 public 的。我花了三天時間來清除這些定義,並在清除過程中,明顯地減少了錯誤數並提高了可維護性。為什麼?因為沒有訪問控制,就不可能知道對象怎樣使用其他對象,也就不可能在不知道要突破什麼難關的情況下做任何更改。使用 C++
,至少我還有編譯程序可用。PHP 沒有配備編譯程序,因此這類訪問控制變得愈加重要。
契約編程
從 PHP V4 遷移到 PHP V5 時要利用的下一個重要特性是支持通過接口、抽象類和方法進行契約編程。清單 6 顯示了一個版本的 Configuration
類,在該類中 PHP V4 編碼人員嘗試了構建基本接口而根本不使用 interface
關鍵字。
<?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" ); ?>
清單開始於一個小型 IConfiguration
類,該類定義所有 Configuration
類或派生類所提供的接口。此接口將在類與其所有使用者之間定義契約。契約聲明了實現 IConfiguration
的所有類必須配有 get()
方法並且 IConfiguration
的所有使用者都必須堅持僅使用 get()
方法。
下面的這段代碼是在 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 顯示了如何擴展 Configuration
類來實現 Iterator
接口,對於 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" ); } ?>
Iterator
接口使所有類都可以看似是其使用者的陣列。正如您在腳本末尾看到的那樣,您可以使用 foreach
運算符重申 Configuration
對象中的所有配置項。PHP V4 沒有這種功能,但您可以在應用程序中通過各種方式使用此功能。
接口機制的優點是可以將契約快速集中在一起而無須實現任何方法。最後階段是實現接口,您必須實現所有指定的方法。PHP V5 中另一個有幫助的新功能是 抽象類,使用抽象類可以輕松地用一個基類實現接口的核心部分,然後用該接口創建實體類。
抽象類的另一個用途是為多個派生類創建一個基類,在這些派生類中,基類決不會被實例化。例如,當 DBConfiguration
和 Configuration
同時存在時,則只能使用 DBConfiguration
。Configuration
類只是一個基類 —— 一個抽象類。因此,您可以使用 abstract
關鍵字強制該行為,如下所示。
<?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" ); ?>
現在,所有要將 Configuration
類型的對象實例化的嘗試都會出錯,因為系統認為該類是抽象的並且不完整。
靜態方法和成員
PHP V5 中的另一個重要的新功能是支持對類使用靜態成員和方法。通過使用這種功能,您可以使用流行的單例模式。這種模式對於 Configuration
類是十分理想的,因為應用程序應當僅有一個配置對象。
清單 10 顯示了 PHP V5 版的 Configuration
類作為一個單例。
<?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" ); ?>
static
關鍵字有很多用法。當需要訪問單個類型的所有對象的某些全局數據時,請考慮使用此關鍵字。
Magic Method
PHP V5 中的另一個很大的新功能是支持 magic method,使用這些方法使對象可以迅速更改對象的接口 —— 例如,為 Configuration
對象中的每個配置項添加成員變量。無須使用 get()
方法,只要尋找一個特殊項將它當作一個陣列,如下所示。
<?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" ); ?>
在本例中,我創建了新的 __get()
方法,只要使用者尋找對象上的成員變量時即調用此方法。然後,方法中的代碼將使用項陣列來查找值並返回該值,就像有一個專門用於該關鍵字的成員變量在那兒一樣。假定對象就是一個陣列,在腳本的末尾,您可以看到使用 Configuration
對象就像尋找 imgpath
的值一樣簡單。
從 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 代碼將讀取一個文件,檢索一些數據,並在遇到任何錯誤時返回錯誤代碼。對於這個腳本,我有兩個問題。第一個是錯誤代碼。這些錯誤代碼的含義是什麼?要找出這些錯誤代碼的含義,則必須創建另一個系統將這些錯誤代碼映射到有含義的字符串中。第二個問題是 parseLine
的返回結果十分復雜。我只需要它返回數據,但它實際上必須返回錯誤代碼 和 數據。大多數工程師 (包括我本人在內) 經常偷懶,僅返回數據,而忽略掉錯誤,因為錯誤很難管理。
清單 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 ); } ?>
我無需考慮錯誤代碼問題,因為異常中包含了錯誤的說明性文字。我也無需考慮如何追蹤從 parseLine
返回的錯誤代碼,因為如果出現錯誤,該函數將只拋出一個錯誤。堆棧將延伸至最近的 try/catch
塊,該塊位於腳本的底部。
異常機制將徹底改變編寫代碼的方法。您不必管理讓人頭痛的錯誤代碼和映射,可以將精力集中在要處理的錯誤上。這樣的代碼更易於閱讀、維護,而且我要說,甚至要鼓勵您添加錯誤處理機制,它通常都能帶來好處。
結束語
新的面向對象特性和異常處理的增加為將代碼從 PHP V4 遷移到 PHP V5 提供了強有力的理由。正如您所見,升級過程並不難。擴展到 PHP V5 的語法感覺就像 PHP 一樣。是的,這些語法來自諸如 Ruby 之類的語言,但我認為它們配合得非常好。並且這些語言將 PHP 的范圍從一種用於小型站點的腳本語言擴展為可用於完成企業級應用的語言。