Menu

5 種常見的 PHP 設計模式

簡介: 設計模式只是為 Java™ 架構師準備的 —— 至少您可能一直這樣認為。實際上,設計模式對於每個人都非常有用。如果這些工具不是 “架構太空人” 的專利,那麼它們又是什麼?為什麼說它們在 PHP 應用程序中非常有用?本文解釋了這些問題。

 

設計模式 一書將設計模式引入軟件社區,該書的作者是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides Design(俗稱 “四人幫”)。所介紹的設計模式背後的核心概念非常簡單。經過多年的軟件開發實踐,Gamma 等人發現了某些具有固定設計的模式,就像建築師設計房子和建築物一樣,可以為浴室的位置或廚房的構造方式開發模板。使用這些模板或者說設計模式 意味著可以更快地設計更好的建築物。同樣的概念也適用於軟件。

設計模式不僅代表著更快開發健壯軟件的有用方法,而且還提供了以友好的術語封裝大型理念的方法。例如,您可以說您正在編寫一個提供鬆散耦合的消息傳遞系統,也可以說你正在編寫名稱為觀察者 的模式。

用較小的示例展示模式的價值是非常困難的。這往往有些大材小用的意味,因為模式實際上是在大型代碼庫中發揮作用的。本文不展示大型應用程序,所以您需要思索的是在您自己的大型應用程序中應用示例原理的方法 —— 而不是本文演示的代碼本身。這不是說您不應該在小應用程序中使用模式。很多良好的應用程序都以小應用程序為起點,逐漸發展到大型應用程序,所以沒有理由不以此類紮實的編碼實踐為基礎。

既然您已經瞭解了設計模式以及它們的有用之處,現在我們來看看 PHP V5 的五種常用模式。

 

最初在設計模式 一書中,許多設計模式都鼓勵使用鬆散耦合。要理解這個概念,讓我們最好談一下許多開發人員從事大型系統的艱苦歷程。在更改一個代碼片段時,就會發生問題,系統其他部分 —— 您曾認為完全不相關的部分中也有可能出現級聯破壞。

該問題在於緊密耦合 。系統某個部分中的函數和類嚴重依賴於系統的其他部分中函數和類的行為和結構。您需要一組模式,使這些類能夠相互通信,但不希望將它們緊密綁定在一起,以避免出現聯鎖。

在大型系統中,許多代碼依賴於少數幾個關鍵類。需要更改這些類時,可能會出現困難。例如,假設您有一個從文件讀取的 User類。您希望將其更改為從數據庫讀取的其他類,但是,所有的代碼都引用從文件讀取的原始類。這時候,使用工廠模式會很方便。

工廠模式 是一種類,它具有為您創建對象的某些方法。您可以使用工廠類創建對象,而不直接使用 new。這樣,如果您想要更改所創建的對象類型,只需更改該工廠即可。使用該工廠的所有代碼會自動更改。

清單 1 顯示工廠類的一個示列。等式的服務器端包括兩個部分:數據庫和一組 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 的實現稱為 UserUserFactory 工廠類則創建 IUser 對象。此關係可以用圖 1 中的 UML 表示。



工廠類及其相關 IUser 接口和用戶類

如果您使用 php 解釋器在命令行上運行此代碼,將得到如下結果:

% php factory1.php 
Jack
%

測試代碼會向工廠請求 User 對象,並輸出 getName 方法的結果。

有一種工廠模式的變體使用工廠方法。類中的這些公共靜態方法構造該類型的對象。如果創建此類型的對象非常重要,此方法非常有用。例如,假設您需要先創建對象,然後設置許多屬性。此版本的工廠模式會將該進程封裝在單個位置中,這樣,不用複製複雜的初始化代碼,也不必將複製好的代碼在在代碼庫中到處粘貼。

清單 2 顯示使用工廠方法的一個示例。



<?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 表示。



IUser 接口和帶有工廠方法的用戶類 

在命令行中運行腳本產生的結果與清單 1 的結果相同,如下所示:

% php factory2.php 
Jack
%

如上所述,有時此類模式在規模較小的環境中似乎有些大材小用。不過,最好還是學習這種紮實的編碼形式,以便應用於任意規模的項目中。

 

某些應用程序資源是獨佔的,因為有且只有一個此類型的資源。例如,通過數據庫句柄到數據庫的連接是獨佔的。您希望在應用程序中共享數據庫句柄,因為在保持連接打開或關閉時,它是一種開銷,在獲取單個頁面的過程中更是如此。

單元素模式可以滿足此要求。如果應用程序每次包含且僅包含一個對象,那麼這個對象就是一個單元素(Singleton)。清單 3 中的代碼顯示了 PHP V5 中的一個數據庫連接單元素。



<?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 所示。



數據庫連接單元素 

在兩次調用間,handle 方法返回的數據庫句柄是相同的,這就是最好的證明。您可以在命令行中運行代碼來觀察這一點。

% php singleton.php 
Handle = Object id #3
Handle = Object id #3
%

返回的兩個句柄是同一對象。如果您在整個應用程序中使用數據庫連接單元素,那麼就可以在任何地方重用同一句柄。

您可以使用全局變量存儲數據庫句柄,但是,該方法僅適用於較小的應用程序。在較大的應用程序中,應避免使用全局變量,並使用對象和方法訪問資源。

 

觀察者模式為您提供了避免組件之間緊密耦合的另一種方法。該模式非常簡單:一個對象通過添加一個方法(該方法允許另一個對象,即觀察者 註冊自己)使本身變得可觀察。當可觀察的對象更改時,它會將消息發送到已註冊的觀察者。這些觀察者使用該信息執行的操作與可觀察的對象無關。結果是對象可以相互對話,而不必瞭解原因。

一個簡單示例是系統中的用戶列表。清單 4 中的代碼顯示一個用戶列表,添加用戶時,它將發送出一條消息。添加用戶時,通過發送消息的日誌觀察者可以觀察此列表。



<?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 中展示了這些元素。



可觀察的用戶列表和用戶列表事件日誌程序 

如果在命令行中運行它,您將看到以下輸出:

% php observer.php 
'Jack' added to user list
%

測試代碼創建 UserList,並將 UserListLogger 觀察者添加到其中。然後添加一個消費者,並將這一更改通知UserListLogger

認識到 UserList 不知道日誌程序將執行什麼操作很關鍵。可能存在一個或多個執行其他操作的偵聽程序。例如,您可能有一個向新用戶發送消息的觀察者,歡迎新用戶使用該系統。這種方法的價值在於 UserList 忽略所有依賴它的對象,它主要關注在列表更改時維護用戶列表並發送消息這一工作。

此模式不限於內存中的對象。它是在較大的應用程序中使用的數據庫驅動的消息查詢系統的基礎。

 

命令鏈 模式以鬆散耦合主題為基礎,發送消息、命令和請求,或通過一組處理程序發送任意內容。每個處理程序都會自行判斷自己能否處理請求。如果可以,該請求被處理,進程停止。您可以為系統添加或移除處理程序,而不影響其他處理程序。清單 5 顯示了此模式的一個示例。



<?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。



命令鏈及其相關命令

如果您運行包含某些測試代碼的腳本,則會得到以下輸出:

% php chain.php 
UserCommand handling 'addUser'
MailCommand handling 'mail'
%

代碼首先創建 CommandChain 對象,並為它添加兩個命令對象的實例。然後運行兩個命令以查看誰對這些命令作出了響應。如果命令的名稱匹配 UserCommand 或 MailCommand,則代碼失敗,不發生任何操作。

為處理請求而創建可擴展的架構時,命令鏈模式很有價值,使用它可以解決許多問題。

 

我們講述的最後一個設計模式是策略 模式。在此模式中,算法是從複雜類提取的,因而可以方便地替換。例如,如果要更改搜索引擎中排列頁的方法,則策略模式是一個不錯的選擇。思考一下搜索引擎的幾個部分 —— 一部分遍歷頁面,一部分對每頁排列,另一部分基於排列的結果排序。在複雜的示例中,這些部分都在同一個類中。通過使用策略模式,您可將排列部分放入另一個類中,以便更改頁排列的方式,而不影響搜索引擎的其餘代碼。

作為一個較簡單的示例,清單 6 顯示了一個用戶列表類,它提供了一個根據一組即插即用的策略查找一組用戶的方法。



<?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 所示。



用戶列表和用於選擇用戶的策略 

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 應用程序中使用的幾種最常見的設計模式。在設計模式 一書中演示了更多的設計模式。不要因架構的神秘性而放棄。模式是一種絕妙的理念,適用於任何編程語言、任何技能水平。


學習

  • PHP.net 是面向 PHP 開發人員的資源。
  • Wikipedia 中有關於設計模式的 優秀文章
  • C2 Wiki 查找有關設計模式(如 觀察者單元素 等)信息的另一個好去處。 
  • 任何工程師都應該閱讀 設計模式 原書。
  • O'Reilly 的 Head First Design Patterns 是學習設計模式的輕量級方法。
  • PHP Hacks 一書中介紹了針對模式設計的幾種黑客技術,可擴展本文介紹的示例。
  • 瀏覽 IBM developerWorks 的 PHP 項目資源,瞭解關於 PHP 的詳細內容。
  • 關注最新的 developerWorks 技術事件和網絡廣播
  • 瞭解全球範圍內即將開展的研討會、內部預映、網絡廣播和其他 活動,這些都是 IBM 開放源碼開發人員感興趣的內容。
  • 訪問 developerWorks 開放源碼專區,獲得廣泛的 how-to 信息、工具和項目更新,幫助您使用開放源碼技術進行開發,並與 IBM 產品結合使用。
  • 收聽針對軟件開發人員的有趣的訪談和討論,務必瀏覽 developerWorks 技術講座

獲得產品和技術

  • 使用 IBM 試用軟件 構建下一個開發項目,這些軟件可以直接從 developerWorks 下載獲得。

討論

 

 

Jack Herrington 是一名具有 20 多年工作經驗的高級軟件工程師,他撰寫了三本書:Code Generation in ActionPodcasting Hacks 和 PHP Hacks,他還編寫了 30 多篇文章。