利用 SPL 快速實現 Observer 設計模式

目錄:php

1.什麼是 SPLweb

2.SplSubject 和 SplObserver 接口設計模式

3.爲何使用 SplObjectStorage 類數組

4.模擬案例瀏覽器

5.結束語數據結構

6.下載資源app

什麼是 SPL

SPL(Standard PHP Library)即標準 PHP 庫,是 PHP 5 在面向對象上能力提高的真實寫照,它由一系列內置的類、接口和函數構成。SPL 經過加入集合,迭代器,新的異常類型,文件和數據處理類等提高了 PHP 語言的生產力。它還提供了一些十分有用的特性,如本文要介紹的內置 Observer 設計模式。dom

本文介紹如何經過使用 SPL 提供的 SplSubject和 SplObserver接口以及 SplObjectStorage類,快速實現 Observer 設計模式。jsp

SPL 在大多數 PHP 5 系統上都是默認開啓的,儘管如此,因爲 SPL 的功能在 PHP 5.2 版本發生了引人注目的改進,因此建議讀者在實踐本文內容時,使用不低於 PHP 5.2 的版本。函數

SplSubject 和 SplObserver 接口

Observer 設計模式定義了對象間的一種一對多的依賴關係,當被觀察的對象發生改變時,全部依賴於它的對象都會獲得通知並被自動更新,並且被觀察的對象和觀察者之間是鬆耦合的。在該模式中,有目標(Subject)和觀察者(Observer)兩種角色。目標角色是被觀察的對象,持有並控制着某種狀態,能夠被任意多個觀察者做爲觀察的目標,SPL 中使用 SplSubject接口規範了該角色的行爲:

表 1. SplSubject 接口中的方法

觀察者角色是在目標發生改變時,須要獲得通知的對象。SPL 中用 SplObserver接口規範了該角色的行爲:

表 2. SplObserver 中的方法

該設計模式的核心思想是,SplSubject對象會在其狀態改變時調用 notify()方法,一旦這個方法被調用,任何先前經過 attach()方法註冊上來的 SplObserver對象都會以調用其 update()方法的方式被更新。

爲何使用 SplObjectStorage 類

SplObjectStorage類實現了以對象爲鍵的映射(map)或對象的集合(若是忽略做爲鍵的對象所對應的數據)這種數據結構。這個類的實例很像一個數組,可是它所存放的對象都是惟一的。這個特色就爲快速實現 Observer 設計模式貢獻了很多力量,由於咱們不但願同一個觀察者被註冊屢次。該類的另外一個特色是,能夠直接從中刪除指定的對象,而不須要遍歷或搜索整個集合。

SplObjectStorage類的實例之因此可以只存儲惟一的對象,是由於其 SplObjectStorage::attach()方法的實現中先判斷了指定的對象是否已經被存儲:

清單 1. SplObjectStorage::attach() 方法的部分源代碼
function attach($obj, $inf = NULL) 
{ 
   if (is_object($obj) && !$this->contains($obj)) 
   { 
       $this->storage[] = array($obj, $inf); 
   } 
}

模擬案例

下面咱們經過一個模擬案例來演示 SPL 在實現 Observer 設計模式上的威力。該案例模擬了一個網站的用戶管理模塊,該模塊包括 3 個主要功能:

  • 新增 1 個用戶
  • 把指定用戶的密碼變動爲他所指定的新密碼
  • 在用戶忘記密碼時重置其密碼

每當這些功能完成後,都須要將密碼告知用戶。除了傳統的向用戶發送 Email 這種手段外,咱們還須要向用戶的手機發送短信,讓他們更加方便地知道密碼是什麼。假設咱們的網站還有一套站內的消息系統,咱們稱之爲小紙條,在用戶變動或重置密碼後,向他們發送小紙條會令他們高興的。

通過分析,該案例適合使用 Observer 設計模式解決,由於將密碼告知用戶的多種手段與用戶密碼的改變——不管是從無到有,用戶主動變動,仍是系統重置——造成了多對一的關係。

咱們決定定義一個 User 類表示用戶,實現需求中的 3 個功能。該類就是 Observer 設計模式中的目標(Subject)角色。咱們還須要一組類,實現利用各類手段向用戶發送新密碼的功能,這些類就充當了 Observer 設計模式中的觀察者(Observer)角色。

通過簡單地分析後,咱們畫出 UML 類圖:

圖 1. 模擬案例的 UML 類圖

 

根據 UML 類圖,首先,定義 1 個名爲 User 的類模擬案例中的用戶。儘管實際網站中的用戶要有更多的屬性,特別是一般須要用 ID 來標識每一個用戶,可是咱們爲了突出本文的主題,只保留了案例所需的屬性。

清單 2. User 類的源代碼
<?php
 
class User implements SplSubject { 
 
   private $email; 
   private $username; 
   private $mobile; 
   private $password; 
   /** 
    * @var SplObjectStorage 
    */ 
   private $observers = NULL; 
 
   public function __construct($email, $username, $mobile, $password) { 
       $this->email = $email; 
       $this->username = $username; 
       $this->mobile = $mobile; 
       $this->password = $password; 
 
       $this->observers = new SplObjectStorage(); 
   } 
 
   public function attach(SplObserver $observer) { 
       $this->observers->attach($observer); 
   } 
 
   public function detach(SplObserver $observer) { 
       $this->observers->detach($observer);
   } 
 
   public function notify() { 
       $userInfo = array( 
           'username' => $this->username, 
           'password' => $this->password, 
           'email' => $this->email, 
           'mobile' => $this->mobile, 
       ); 
       foreach ($this->observers as $observer) { 
           $observer->update($this, $userInfo); 
       } 
   } 
 
   public function create() { 
       echo __METHOD__, PHP_EOL; 
       $this->notify(); 
   } 
 
   public function changePassword($newPassword) { 
       echo __METHOD__, PHP_EOL; 
       $this->password = $newPassword; 
       $this->notify(); 
   } 
 
   public function resetPassword() { 
       echo __METHOD__, PHP_EOL; 
       $this->password = mt_rand(100000, 999999); 
       $this->notify(); 
   } 
 
}

User 類要想充當目標角色,就須要實現 SplSubject接口,而按照實現接口的法則,attach()detach()和 notify()就必須被實現。請注意,因爲在 SplSubject接口中,attach() 和detach() 的參數都使用了類型提示(type hinting),在實現這兩個方法時,也不能省略參數前面的類型。咱們還使用了 $observers實例屬性保存一個 SplObjectStorage對象,用來存放全部註冊上來的觀察者。

的確,一個數組就能解決問題,可是很快就能夠發現,使用了 SplObjectStorage以後刪除一個觀察者實現起來是多麼簡單,直接委託給 SplObjectStorage對象!是的,不須要再使用最原始的 for語句遍歷觀察者數組或者使用 array_search函數,1 行搞定。

接下來分別定義充當觀察者角色的 3 個信息發送類。爲了簡單,咱們只是經過輸出文原本僞裝發送信息。可即便是僞裝,依然須要知道用戶的信息。可看看 SplObserver接口 update()方法的簽名,多麼使人沮喪,它沒法接受目標角色經過調用其 notify() 方法發送通告時給出的參數。若是你試圖在重寫 update()方法時加上第 2 個參數,會獲得一個相似

Fatal error: Declaration of EmailSender::update() must be compatible with that of SplObserver::update() 的錯誤而使代碼執行終止。

其實,當目標所持有的狀態(在本例中是用戶的密碼)更新時,如何通知觀察者有兩種方法。「拉」的方法和「推」的方法。SPL 使用的是「拉」的方法,觀察者須要經過目標的引用(做爲 update()方法的參數傳入)來訪問其屬性。「拉」的方法須要讓觀察者更瞭解目標都擁有哪些屬性,這增長了它們耦合度。並且主題也要對觀察者門戶大開,違背了封裝性。解決的方法是在目標中提供一系列 getter 方法,如 getPassword()來讓觀察者得到用戶的密碼。

雖然「拉」的方法可能被認爲更加正確,可是咱們以爲讓主題把用戶的信息「推」過來更加方便。既然經過在重寫 update()方法時加上第 2 個參數是行不通的,那麼就從別的方向上着手。好在 PHP 在方法調用上有這樣的特性,只要給定的參數(實參)很多於定義時指定的必選參數(沒有默認值的參數),PHP 就不會報錯。傳入一個方法的參數個數,能夠經過 func_num_args() 函數獲取;多餘的參數可使用 func_get_arg()函數讀取。注意該函數是從 0 開始計數的,即 0 表示第 1 個實參。利用這個小技巧,update()方法能夠經過 func_get_arg(1)接收一個用戶信息的數組,有了這個數組,就能知道郵件該發給誰,新密碼是什麼了。爲了節約篇幅,並且三個信息發送類很是相像,下面只給出其中一個的源代碼,完整的源代碼能夠下載本文的附件獲得。

清單 3. Email_Sender 類的源代碼
<?php 
 
class EmailSender implements SplObserver { 
 
   public function update(SplSubject $subject) { 
       if (func_num_args() === 2) { 
           $userInfo = func_get_arg(1); 
           echo "向 {$userInfo['email']} 發送電子郵件成功。內容是:你好 {$userInfo['username']}" . 
           "你的新密碼是 {$userInfo['password']},請妥善保管", PHP_EOL; 
       } 
   } 
 
}

最後咱們寫一個測試腳本 test.php。建議使用 CLI 的方式 php – f test.php來執行該腳本,但因爲設置了 Content-Type響應頭部字段爲 text/plain,在瀏覽器中應該也能看到一行一行顯示的結果(由於沒有用 <br />作換行符而是使用常量 PHP_EOL,因此不設置 Content-Type的話,就不能正確分行顯示了)。

清單 4. 用於測試的腳本
<?php
 
header('Content-Type: text/plain'); 
 
function __autoload($class_name) { 
   require_once "$class_name.php"; 
} 
 
$email_sender = new EmailSender(); 
$mobile_sender = new MobileSender(); 
$web_sender = new WebsiteSender(); 
 
$user = new User('user1@domain.com', '張三', '13610002000', '123456'); 
 
// 建立用戶時經過 Email 和手機短信通知用戶
$user->attach($email_sender); 
$user->attach($mobile_sender); 
$user->create($user); 
echo PHP_EOL; 
 
// 用戶忘記密碼後重置密碼,還須要經過站內小紙條通知用戶
$user->attach($web_sender); 
$user->resetPassword(); 
echo PHP_EOL; 
 
// 用戶變動了密碼,可是不要給他的手機發短信
$user->detach($mobile_sender); 
$user->changePassword('654321'); 
echo PHP_EOL;
清單 5. 運行結果
User::create 
向 user1@domain.com 發送電子郵件成功。內容是:你好張三你的新密碼是 123456,請妥善保管
向 13610002000 發送短消息成功。內容是:你好張三你的新密碼是 123456,請妥善保管
 
 User::resetPassword 
向 user1@domain.com 發送電子郵件成功。內容是:你好張三你的新密碼是 363989,請妥善保管
向 13610002000 發送短消息成功。內容是:你好張三你的新密碼是 363989,請妥善保管
這是 1 封站內小紙條。你好張三,你的新密碼是 363989,請妥善保管
 
 User::changePassword 
向 user1@domain.com 發送電子郵件成功。內容是:你好張三你的新密碼是 654321,請妥善保管
這是 1 封站內小紙條。你好張三,你的新密碼是 654321,請妥善保管

咱們看到,用戶 張三 能夠經過多種手段知道他的密碼是什麼。

結束語

對於經驗豐富的開發者,即便不使用 SPL 也能夠輕鬆實現 Observer 設計模式,可是使用 SPL 帶來了更高的效率,特別在結合了 SplObjectStorage以後,註冊和刪除觀察者都由它的實例代理完成。雖然在使用「推」的方式更新 Observer 時,SplObserver的 update()方法只接受 1 個參數顯得美中不足,或者說 SPL 內置的 Observer 設計模式只支持經過「拉模式」獲取通知,可是經過本文的介紹的小技巧便可彌補。所以,SPL 在快速實現 Observer 設計模式上成爲了首選。

下載資源

樣例代碼(observer_pattern.rar | 26KB)

原文地址:https://www.ibm.com/developerworks/cn/opensource/os-cn-observerspl/#ibm-pcon

相關文章
相關標籤/搜索