另外五個 PHP 設計模式

日期:2008-07-04  作者:喜騰小二  來源:PHPChina


PHP V5 的麵嚮物件特性使您能夠實現設計模式來改處理序式碼設計。透過這種方式改處理序式碼設計,程式碼在進行修改時將變得更加易讀、更易維護且更加健壯。

設計模式 一書介紹了很多此類概念。當時,我還在學習麵嚮物件 (OO),因此我發現那本書中有許多概念都很難領會。但是,隨着越來越熟悉 OO 概念 —— 尤其是介麵和繼承的使用 —— 我開始看到設計模式中的實際價值。作為一名應用程式開發人員,即使從不瞭解任何模式或者如何及何時使用這些模式,對您的職業生涯也沒有什麼大的影響。但是,我發現瞭解這些模式以及 developerWorks 文章 “五種常見 PHP 設計模式” 中介紹的那些模式的優秀知識後(請參閱 參考資料),您可以完成兩件事情:

啓用高頻寬工作階段
如果瞭解設計模式,您將能夠更快地建置可靠的 OO 應用程式。但當整個開發團隊知道各種模式時,您可以突然擁有非常高的頻寬工作階段。您不再需要討論將到處使用的所有類。相反,您可以與其他人談論模式。“我要在這裡引用一個單例(singleton),然後使用叠代器遍歷物件集合,然後……” 比遍歷構成這些模式的類、方法和介麵快很多。單是通信效率一項就值得花時間以團隊的形式透過工作階段來研究模式。
減少痛苦的教訓
每個設計模式都描述了一種經過驗證的解決常見問題的方法。因此,您無需擔心設計是不是正確的,只要您已經選擇了提供所需優點的模式。

缺陷

有句諺語說得好:“當您手中拿着一把錘子時,所有事物看上去都像釘子”。當您認為自己找到一個優秀模式時,您可能會嘗試到處使用它,即使在不應當使用它的位置。記住您必須考慮正在學習的模式的使用目的,不要為了使用模式而把這些模式強行應用到應用程式的各個部分中。

本文將介紹可用於改進 PHP 程式碼的五個模式。每個模式都將介紹一個特定場景。可以在 下載 部分中獲得這些模式的 PHP 程式碼。

要求

要發揮本文的最大功效並使用範例,需要在電腦中安裝以下軟體:

  • PHP V5 或更高版本(本文是使用 PHP V5.2.4 撰寫的)
  • 壓縮程式,例如 WinZIP(用於壓縮可下載的程式碼歸檔)

注:雖然您也可以使用純文字編輯器,但是我發現擁有語法高亮顯示和語法糾錯功能的編輯器真的很有說明。本文中的範例是使用 Eclipse PHP Development Tools (PDT) 編寫的。

介麵卡模式

在需要將一類物件轉換成另一類物件時,請使用介麵卡模式。通常,開發人員透過一係列賦值程式碼來處理此過程,如清單 1 所示。介麵卡模式是整理此類程式碼並在其他位置重用所有賦值程式碼的優秀方法。此外,它還將隱藏賦值程式碼,如果同時還要設定格式,這樣可以極大地簡化工作。


清單 1. 使用程式碼在物件之間賦值

class AddressDisplay
{
private $addressType;
private $addressText;

public function setAddressType($addressType)
{
$this->addressType = $addressType;
}

public function getAddressType()
{
return $this->addressType;
}

public function setAddressText($addressText)
{
$this->addressText = $addressText;
}

public function getAddressText()
{
return $this->addressText;
}
}

class EmailAddress
{
private $emailAddress;

public function getEmailAddress()
{
return $this->emailAddress;
}

public function setEmailAddress($address)
{
$this->emailAddress = $address;
}
}

$emailAddress = new EmailAddress();
/* Populate the EmailAddress object */
$address = new AddressDisplay();
/* Here's the assignment code, where I'm assigning values
from one object to another... */
$address->setAddressType("email");
$address->setAddressText($emailAddress->getEmailAddress());

此範例將使用 AddressDisplay 物件把地址顯示給使用者。AddressDisplay 物件有兩部分:地址類型和一個格式化的地址字串。

在實現模式(參見清單 2)後,PHP 指令檔將不再需要擔心如何把 EmailAddress 物件轉換成 AddressDisplay 物件。那是件好事,尤其是在 AddressDisplay 物件發生變更時或者控制如何把 EmailAddress 物件轉換成 AddressDisplay 物件的規則發生變更時。記住,以模組化風格設計程式碼的主要優點之一就是,在業務領域發生一些變更時或者需要嚮軟體中新增新功能時盡可能少的使用變更。即使在執行普通工作(例如把一個物件的內容值賦給另一個物件)時,也請考慮使用此模式。


清單 2. 使用介麵卡模式

class EmailAddressDisplayAdapter extends AddressDisplay
{
public function __construct($emailAddr)
{
$this->setAddressType("email");
$this->setAddressText($emailAddr->getEmailAddress());
}
}

$email = new EmailAddress();
$email->setEmailAddress("user@example.com");

$address = new EmailAddressDisplayAdapter($email);

echo($address->getAddressType() . "
") ;
echo($address->getAddressText());

圖 1 顯示了介麵卡模式的類圖。


圖 1. 介麵卡模式的類圖
介麵卡模式的類圖

替代方法

編寫介麵卡的替代方法 —— 並且是推薦方法 —— 是實現一個介麵來修改行為,而不是延伸物件。這是一種非常乾淨的、建立介麵卡的方法並且沒有延伸物件的缺點。使用介麵的缺點之一是需要把實現新增到介麵卡類中,如圖 2 所示:


圖 2. 介麵卡模式(使用介麵)
介麵卡模式(使用介麵) 

叠代器模式

叠代器模式將提供一種透過物件集合或物件陣列封裝叠代的方法。如果需要遍歷集合中不同類型的物件,則使用這種模式尤為便利。

檢視上麵清單 1 中的電子郵件和實體地址範例。在新增叠代器模式之前,如果要遍歷個人地址,則可能要遍歷實體地址並顯示這些地址,然後遍歷個人電子郵件地址並顯示這些地址,然後遍歷個人 IM 地址並顯示這些地址。非常復雜的遍歷!

相反,透過實現叠代器,您隻需要調用 while($itr->hasNext()) 並處理下一個條目 $itr->next() 返回。清單 3 中顯示了一個叠代器範例。叠代器功能強大,因為您可以新增要遍歷的新類型條目,並且無需變更遍歷條目的程式碼。例如,在 Person 範例中,可以新增 IM 地址陣列;隻需更新叠代器,無需變更遍歷地址的任何程式碼。


清單 3. 使用叠代器模式遍歷物件

class PersonAddressIterator implements AddressIterator
{
private $emailAddresses;
private $physicalAddresses;
private $position;

public function __construct($emailAddresses)
{
$this->emailAddresses = $emailAddresses;
$this->position = 0;
}

public function hasNext()
{
if ($this->position >= count($this->emailAddresses) ||
$this->emailAddresses[$this->position] == null) {
return false;
} else {
return true;
}
}

public function next()
{
$item = $this->emailAddresses[$this->position];
$this->position = $this->position + 1;
return $item;
}

}

如果把 Person 物件修改為返回 AddressIterator 介麵的實現,則在將實現延伸為遍歷附加物件時無需修改使用叠代器的應用程式程式碼。您可以使用一個混合叠代器,它封裝了遍歷清單 3 中列出的每種地址的叠代器。本文提供了此類應用範例(請參閱 下載)。

圖 3 顯示了叠代器模式的類圖。


圖 3. 叠代器模式的類圖
叠代器模式的類圖 

裝飾器 (decorator) 模式

考慮清單 4 中的程式碼樣例。這段程式碼的目的是要把許多功能新增到 Build Your Own Car 站點的汽機車中。每個汽機車模型都有更多功能及相關價格。如果隻針對兩個模型,使用 if then 陳述式新增這些功能十分平常。但是,如果出現了新模型,則必須返回檢視程式碼並確保陳述式對新模型工作正常。


清單 4. 使用裝飾器模式新增功能

require('classes.php');

$auto = new Automobile();

$model = new BaseAutomobileModel();

$model = new SportAutomobileModel($model);

$model = new TouringAutomobileModel($model);

$auto->setModel($model);

$auto->printDescription();

進入裝飾器模式,該模式允許您透過一個優秀整潔的類將此功能新增到 AutomobileModel。每個類僅僅關注其價格、選項以及新增到基本模型的方式。

圖 4 顯示了裝飾器模式的類圖。


圖 4. 裝飾器模式的類圖
裝飾器模式的類圖

裝飾器模式的優點是可以輕鬆地同時跟蹤庫的多個裝飾器。

如果您擁有流物件的使用經驗,則一定使用過裝飾器。大多數流結構(例如輸出流)都是接受基本輸入流的裝飾器,然後透過新增附加功能來裝飾它 —— 例如從檔案輸入流、從緩衝區輸入流,等等。

委託模式

委託模式將提供一種基於各種條件委託行為的方法。考慮清單 5 中的程式碼。這段程式碼包含幾個條件。根據條件,程式碼將選擇相應類型的物件來處理請求。


清單 5. 使用條件陳述式來發送送貨請求

pkg = new Package("Heavy Package");
$pkg->setWeight(100);

if ($pkg->getWeight() > 99)
{
echo( "Shipping " . $pkg->getDescription() . " by rail.");
} else {
echo("Shipping " . $pkg->getDescription() . " by truck");
}

使用委託模式,物件將內在化(internalize)此發送過程,方法為在調用如清單 6 中的 useRail() 之類的方法時設定對相應物件的內部引用。如果處理各個包的條件發生變更或者使用新的送貨類型時,則使用此模式尤為便利。


清單 6. 使用委託模式來發送送貨請求

require_once('classes.php');

$pkg = new Package("Heavy Package");
$pkg->setWeight(100);

$shipper = new ShippingDelegate();

if ($pkg->getWeight() > 99)
{
$shipper->useRail();
}

$shipper->deliver($pkg);

委託將透過調用 useRail()useTruck() 方法來切換處理工作的類,從而提供動態變更行為的優點。

圖 5 顯示了委託模式的類圖。


圖 5. 委託模式的類圖
委託模式的類圖 

狀態模式

狀態模式類似於指令模式,但是意圖截然不同。考慮下麵的程式碼。


清單 7. 使用程式碼來建置機器人

class Robot 
{

private $state;

public function powerUp()
{
if (strcmp($state, "poweredUp") == 0)
{
echo("Already powered up...
");
/* Implementation... */
} else if ( strcmp($state, "powereddown") == 0) {
echo("Powering up now...
");
/* Implementation... */
}
}

public function powerDown()
{
if (strcmp($state, "poweredUp") == 0)
{
echo("Powering down now...
");
/* Implementation... */
} else if ( strcmp($state, "powereddown") == 0) {
echo("Already powered down...
");
/* Implementation... */
}
}

/* etc... */

}

在此清單中,PHP 程式碼表示變成一輛汽機車的強大機器人的操作係統。機器人可以啓動、關閉、由汽機車變成機器人以及由機器人變成汽機車。程式碼現已就緒,但是您會看到如果任何規則發生變更或者新增另一個狀態則會變得十分復雜。

現在檢視清單 8,其中提供了相同的邏輯處理機器人的狀態,但是這一次把邏輯放入狀態模式。清單 8 中的程式碼完成的工作與初始程式碼相同,但是用於處理狀態的邏輯已經被放入每個狀態的一個物件中。為了演示使用設計模式的優點,假定不久以後,這些機器人發現它們不應在處於機器人模式時關閉。實際上,如果它們關閉,它們必須先切換到汽機車模式。如果它們已經處於汽機車模式下,則機器人將關閉。使用狀態模式,對程式碼的變更十分微小。


清單 8. 使用狀態模式處理機器人的狀態

$robot = new Robot();
echo("
");
$robot->powerUp();
echo("
");
$robot->turnIntoRobot();
echo("
");
$robot->turnIntoRobot(); /* This one will just give me a message */
echo("
");
$robot->turnIntoVehicle();
echo("
");


清單 9. 對一個狀態物件的微小變更
class NormalRobotState implements RobotState
{
private $robot;

public function __construct($robot)
{
$this->robot = $robot;
}

public function powerUp()
{
/* implementation... */
}
public function powerDown()
{
/* First, turn into a vehicle */
$this->robot->setState(new VehicleRobotState($this->robot));
$this->robot->powerDown();
}

public function turnIntoVehicle()
{
/* implementation... */
}

public function turnIntoRobot()
{
/* implementation... */
}
}

圖 6 中一個不太明顯的地方就是狀態模式中的每個物件都有對上下文物件(機器人)的引用,因此每個物件都可以把狀態提昇到相應的狀態。


圖 6. 狀態模式的類圖
狀態模式的類圖

 

 

結束語

在 PHP 程式碼中使用設計模式可以使程式碼更容易閱讀、更易維護。透過使用已經建立的模式,您將從通用的設計結構中獲益,從而允許團隊的其他開發人員瞭解程式碼的意圖。它還使您可以從其他設計者完成的工作中獲益,因此無需從失敗的設計理念中吸取教訓。

 

<<<返回技術中心

技術文章

站內新聞

我要啦免费统计