設計模式 一書將設計模式引入軟體社區,該書的作者是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides Design(俗稱 “四人幫”)。所介紹的設計模式背後的核心概念非常簡單。經過多年的軟體開發實踐,Gamma 等人發現了某些具有固定設計的模式,就像建築師設計房子和建築物一樣,可以為浴室的位置或廚房的構造方式開發模板。使用這些模板或者說設計模式 意味著可以更快地設計更好的建築物。同樣的概念也適用於軟體。
設計模式不僅代表著更快開發健壯軟體的有用方法,而且還提供了以友好的術語封裝大型理念的方法。例如,您可以說您正在編寫一個提供鬆散耦合的訊息傳遞系統,也可以說你正在編寫名稱為觀察者 的模式。
用較小的樣本展示模式的價值是非常困難的。這往往有些大材小用的意味,因為模式實際上是在大型程式碼程式庫中發揮作用的。本文不展示大型應用程式,所以您需要思索的是在您自己的大型應用程式中應用樣本原理的方法 —— 而不是本文示範的代碼本身。這不是說您不應該在小應用程式中使用模式。很多良好的應用程式都以小應用程式為起點,逐漸發展到大型應用程式,所以沒有理由不以此類紮實的編碼實踐為基礎。
既然您已經瞭解了設計模式以及它們的有用之處,現在我們來看看 PHP V5 的五種常用模式。
原廠模式
最初在設計模式 一書中,許多設計模式都鼓勵使用鬆散耦合。要理解這個概念,讓我們最好談一下許多開發人員從事大型系統的艱苦曆程。在更改一個程式碼片段時,就會發生問題,系統其他部分 —— 您曾認為完全不相關的部分中也有可能出現級聯破壞。
該問題在於緊密耦合 。系統某個部分中的函數和類嚴重依賴於系統的其他部分中函數和類的行為和結構。您需要一組模式,使這些類能夠相互連信,但不希望將它們緊密綁定在一起,以避免出現聯鎖。
在大型系統中,許多代碼依賴於少數幾個關鍵類。需要更改這些類時,可能會出現困難。例如,假設您有一個從檔案讀取的 User
類。您希望將其更改為從資料庫讀取的其他類,但是,所有的代碼都引用從檔案讀取的原始類。這時候,使用原廠模式會很方便。
原廠模式 是一種類,它具有為您建立對象的某些方法。您可以使用工廠類建立對象,而不直接使用 new
。這樣,如果您想要更改所建立的物件類型,只需更改該工廠即可。使用該工廠的所有代碼會自動更改。
清單 1 顯示工廠類的一個示列。等式的伺服器端包括兩個部分:資料庫和一組 PHP 頁面,這些頁面允許您添加反饋、請求反饋列表並擷取與特定反饋相關的文章。
清單 1. Factory1.php
<?php interface IUser { function getName(); }
class User implements IUser { public function __construct( $id ) { }
public function getName() { return "Jack"; } }
class UserFactory { public static function Create( $id ) { return new User( $id ); } }
$uo = UserFactory::Create( 1 ); echo( $uo->getName()."\n" ); ?>
|
IUser
介面定義使用者物件應執行什麼操作。IUser
的實現稱為 User
,UserFactory
工廠類則建立 IUser
對象。此關係可以用圖 1 中的 UML 表示。
圖 1. 工廠類及其相關 IUser 介面和使用者類
如果您使用 php
解譯器在命令列上運行此代碼,將得到如下結果:
測試代碼會向工廠請求 User
對象,並輸出 getName
方法的結果。
有一種原廠模式的變體使用Factory 方法。類中的這些公用靜態方法構造該類型的對象。如果建立此類型的對象非常重要,此方法非常有用。例如,假設您需要先建立對象,然後設定許多屬性。此版本的原廠模式會將該進程封裝在單個位置中,這樣,不用複製複雜的初始化代碼,也不必將複製好的代碼在在程式碼程式庫中到處粘貼。
清單 2 顯示使用Factory 方法的一個樣本。
清單 2. Factory2.php
<?php interface IUser { function getName(); }
class User implements IUser { public static function Load( $id ) { return new User( $id ); }
public static function Create( ) { return new User( null ); }
public function __construct( $id ) { }
public function getName() { return "Jack"; } }
$uo = User::Load( 1 ); echo( $uo->getName()."\n" ); ?>
|
這段代碼要簡單得多。它僅有一個介面 IUser
和一個實現此介面的 User
類。User
類有兩個建立對象的靜態方法。此關係可用圖 2 中的 UML 表示。
圖 2. IUser 介面和帶有Factory 方法的 user 類
在命令列中運行指令碼產生的結果與清單 1 的結果相同,如下所示:
如上所述,有時此類模式在規模較小的環境中似乎有些大材小用。不過,最好還是學習這種紮實的編碼形式,以便應用於任意規模的項目中。
單元素模式
某些應用程式資源是獨佔的,因為有且只有一個此類型的資源。例如,通過資料庫控制代碼到資料庫的串連是獨佔的。您希望在應用程式中共用資料庫控制代碼,因為在保持串連開啟或關閉時,它是一種開銷,在擷取單個頁面的過程中更是如此。
單元素模式可以滿足此要求。如果應用程式每次包含且僅包含一個對象,那麼這個對象就是一個單元素(Singleton)。清單 3 中的代碼顯示了 PHP V5 中的一個資料庫連接單元素。
清單 3. Singleton.php
<?php require_once("DB.php");
class DatabaseConnection { public static function get() { static $db = null; if ( $db == null ) $db = new DatabaseConnection(); return $db; }
private $_handle = null;
private function __construct() { $dsn = 'mysql://root:password@localhost/photos'; $this->_handle =& DB::Connect( $dsn, array() ); }
public function handle() { return $this->_handle; } }
print( "Handle = ".DatabaseConnection::get()->handle()."\n" ); print( "Handle = ".DatabaseConnection::get()->handle()."\n" ); ?>
|
此代碼顯示名為 DatabaseConnection
的單個類。您不能建立自已的 DatabaseConnection
,因為建構函式是專用的。但使用靜態 get
方法,您可以獲得且僅獲得一個 DatabaseConnection
對象。此代碼的 UML 3 所示。
圖 3. 資料庫連接單元素
在兩次調用間,handle
方法返回的資料庫控制代碼是相同的,這就是最好的證明。您可以在命令列中運行代碼來觀察這一點。
% php singleton.php Handle = Object id #3 Handle = Object id #3 %
|
返回的兩個控制代碼是同一對象。如果您在整個應用程式中使用資料庫連接單元素,那麼就可以在任何地方重用同一控制代碼。
您可以使用全域變數儲存資料庫控制代碼,但是,該方法僅適用於較小的應用程式。在較大的應用程式中,應避免使用全域變數,並使用對象和方法訪問資源。
觀察者模式
觀察者模式為您提供了避免組件之間緊密耦合的另一種方法。該模式非常簡單:一個對象通過添加一個方法(該方法允許另一個對象,即觀察者 註冊自己)使本身變得可觀察。當可觀察的對象更改時,它會將訊息發送到登入的觀察者。這些觀察者使用該資訊執行的操作與可觀察的對象無關。結果是對象可以相互對話,而不必瞭解原因。
一個簡單樣本是系統中的使用者列表。清單 4 中的代碼顯示一個使用者列表,添加使用者時,它將發送出一條訊息。添加使用者時,通過發送訊息的日誌觀察者可以觀察此列表。
清單 4. Observer.php
<?php interface IObserver { function onChanged( $sender, $args ); }
interface IObservable { function addObserver( $observer ); }
class UserList implements IObservable { private $_observers = array();
public function addCustomer( $name ) { foreach( $this->_observers as $obs ) $obs->onChanged( $this, $name ); }
public function addObserver( $observer ) { $this->_observers []= $observer; } }
class UserListLogger implements IObserver { public function onChanged( $sender, $args ) { echo( "'$args' added to user list\n" ); } }
$ul = new UserList(); $ul->addObserver( new UserListLogger() ); $ul->addCustomer( "Jack" ); ?>
|
此代碼定義四個元素:兩個介面和兩個類。IObservable
介面定義可以被觀察的對象,UserList
實現該介面,以便將本身註冊為可觀察。IObserver
列表定義要通過怎樣的方法才能成為觀察者,UserListLogger
實現 IObserver
介面。圖 4 的 UML 中展示了這些元素。
圖 4. 可觀察的使用者列表和使用者列表事件記錄程式
如果在命令列中運行它,您將看到以下輸出:
% php observer.php 'Jack' added to user list %
|
測試代碼建立 UserList
,並將 UserListLogger
觀察者添加到其中。然後添加一個消費者,並將這一更改通知 UserListLogger
。
認識到 UserList
不知道日誌程式將執行什麼操作很關鍵。可能存在一個或多個執行其他動作的偵聽程式。例如,您可能有一個向新使用者發送訊息的觀察者,歡迎新使用者使用該系統。這種方法的價值在於 UserList
忽略所有依賴它的對象,它主要關注在列表更改時維護使用者列表並發送訊息這一工作。
此模式不限於記憶體中的對象。它是在較大的應用程式中使用的資料庫驅動的訊息查詢系統的基礎。
命令鏈模式
命令鏈 模式以鬆散耦合主題為基礎,發送訊息、命令和請求,或通過一組處理常式發送任意內容。每個處理常式都會自行判斷自己能否處理請求。如果可以,該請求被處理,進程停止。您可以為系統添加或移除處理常式,而不影響其他處理常式。清單 5 顯示了此模式的一個樣本。
清單 5. Chain.php
<?php interface ICommand { function onCommand( $name, $args ); }
class CommandChain { private $_commands = array();
public function addCommand( $cmd ) { $this->_commands []= $cmd; }
public function runCommand( $name, $args ) { foreach( $this->_commands as $cmd ) { if ( $cmd->onCommand( $name, $args ) ) return; } } }
class UserCommand implements ICommand { public function onCommand( $name, $args ) { if ( $name != 'addUser' ) return false; echo( "UserCommand handling 'addUser'\n" ); return true; } }
class MailCommand implements ICommand { public function onCommand( $name, $args ) { if ( $name != 'mail' ) return false; echo( "MailCommand handling 'mail'\n" ); return true; } }
$cc = new CommandChain(); $cc->addCommand( new UserCommand() ); $cc->addCommand( new MailCommand() ); $cc->runCommand( 'addUser', null ); $cc->runCommand( 'mail', null ); ?>
|
此代碼定義維護 ICommand
對象列表的 CommandChain
類。兩個類都可以實現 ICommand
介面 —— 一個對郵件的請求作出響應,另一個對添加使用者作出響應。 圖 5 給出了 UML。
圖 5. 命令鏈及其相關命令
如果您運行包含某些測試代碼的指令碼,則會得到以下輸出:
% php chain.php UserCommand handling 'addUser' MailCommand handling 'mail' %
|
代碼首先建立 CommandChain
對象,並為它添加兩個命令對象的執行個體。然後運行兩個命令以查看誰對這些命令作出了響應。如果命令的名稱匹配 UserCommand
或 MailCommand
,則代碼失敗,不發生任何操作。
為處理請求而建立可擴充的架構時,命令鏈模式很有價值,使用它可以解決許多問題。
策略模式
我們講述的最後一個設計模式是策略 模式。在此模式中,演算法是從複雜類提取的,因而可以方便地替換。例如,如果要更改搜尋引擎中排列頁的方法,則策略模式是一個不錯的選擇。思考一下搜尋引擎的幾個部分 —— 一部分遍曆頁面,一部分對每頁排列,另一部分基於排列的結果排序。在複雜的樣本中,這些部分都在同一個類中。通過使用原則模式,您可將排列部分放入另一個類中,以便更改頁排列的方式,而不影響搜尋引擎的其餘代碼。
作為一個較簡單的樣本,清單 6 顯示了一個使用者列表類,它提供了一個根據一組隨插即用的策略尋找一組使用者的方法。
清單 6. Strategy.php
<?php interface IStrategy { function filter( $record ); }
class FindAfterStrategy implements IStrategy { private $_name;
public function __construct( $name ) { $this->_name = $name; }
public function filter( $record ) { return strcmp( $this->_name, $record ) <= 0; } }
class RandomStrategy implements IStrategy { public function filter( $record ) { return rand( 0, 1 ) >= 0.5; } }
class UserList { private $_list = array();
public function __construct( $names ) { if ( $names != null ) { foreach( $names as $name ) { $this->_list []= $name; } } }
public function add( $name ) { $this->_list []= $name; }
public function find( $filter ) { $recs = array(); foreach( $this->_list as $user ) { if ( $filter->filter( $user ) ) $recs []= $user; } return $recs; } }
$ul = new UserList( array( "Andy", "Jack", "Lori", "Megan" ) ); $f1 = $ul->find( new FindAfterStrategy( "J" ) ); print_r( $f1 );
$f2 = $ul->find( new RandomStrategy() ); print_r( $f2 ); ?>
|
此代碼的 UML 6 所示。
圖 6. 使用者列表和用於選擇使用者的策略
UserList
類是打包名稱數組的一個封裝器。它實現 find
方法,該方法利用幾個策略之一來選擇這些名稱的子集。這些策略由 IStrategy
介面定義,該介面有兩個實現:一個隨機播放使用者,另一個根據指定名稱選擇其後的所有名稱。運行測試代碼時,將得到以下輸出:
% php strategy.php Array ( [0] => Jack [1] => Lori [2] => Megan ) Array ( [0] => Andy [1] => Megan ) %
|
測試代碼為兩個策略運行同一使用者列表,並顯示結果。在第一種情況中,策略尋找排列在 J
後的任何名稱,所以您將得到 Jack、Lori 和 Megan。第二個策略隨機選取名稱,每次會產生不同的結果。在這種情況下,結果為 Andy 和 Megan。
策略模式非常適合複雜資料管理系統或資料處理系統,二者在資料篩選、搜尋或處理的方式方面需要較高的靈活性。
結束語
本文介紹的僅僅是 PHP 應用程式中使用的幾種最常見的設計模式。在設計模式 一書中示範了更多的設計模式。不要因架構的神秘性而放棄。模式是一種絕妙的理念,適用於任何程式設計語言、任何技能水平。