使用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 顯示了一個簡化的版本。
清單1. access.php4
<?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。這次小改動如下圖所示。
清單2. access1.php5
<?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 成員變數。這次改動如下圖所示。
清單3. access2.php5
<?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,並假定該類別將從資料庫中讀取配置值。此設定如下所示。
清單4. access3.php
<?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 類別來完全維護項陣列,以便在添加其他子類別後,那些類別將不需要知道如何維護項陣列。我做了以下更改。
清單5. access4.php5
<?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 關鍵字。
清單6. interface.php4
<?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 中運行的,但最好使用提供的介面系統,如下所示。
清單7. interface1.php5
<?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 來說,該接口是內部接口。
清單8. interface2.php5
<?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 關鍵字強制該行為,如下所示。
清單9. abstract.php5
<?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 類別作為一個單例。
清單10. static.php5
<?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() 方法,只要尋找一個特殊項將它當作一個陣列,如下所示。
清單11. magic.php5
<?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 程式碼。
清單12. file.php4
<?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 顯示了使用異常時程式碼的清晰程度。
清單13. file.php5
<?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 的範圍從一種用於小型網站的腳本語言擴展為可用於完成企業級應用程式的語言。