目錄:php
1.什麼是 SPLweb
2.SplSubject 和 SplObserver 接口設計模式
3.爲何使用 SplObjectStorage 類數組
4.模擬案例瀏覽器
5.結束語數據結構
6.下載資源app
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 的版本。函數
Observer 設計模式定義了對象間的一種一對多的依賴關係,當被觀察的對象發生改變時,全部依賴於它的對象都會獲得通知並被自動更新,並且被觀察的對象和觀察者之間是鬆耦合的。在該模式中,有目標(Subject)和觀察者(Observer)兩種角色。目標角色是被觀察的對象,持有並控制着某種狀態,能夠被任意多個觀察者做爲觀察的目標,SPL 中使用 SplSubject
接口規範了該角色的行爲:
觀察者角色是在目標發生改變時,須要獲得通知的對象。SPL 中用 SplObserver
接口規範了該角色的行爲:
該設計模式的核心思想是,SplSubject
對象會在其狀態改變時調用 notify()
方法,一旦這個方法被調用,任何先前經過 attach()
方法註冊上來的 SplObserver
對象都會以調用其 update()
方法的方式被更新。
SplObjectStorage
類實現了以對象爲鍵的映射(map)或對象的集合(若是忽略做爲鍵的對象所對應的數據)這種數據結構。這個類的實例很像一個數組,可是它所存放的對象都是惟一的。這個特色就爲快速實現 Observer 設計模式貢獻了很多力量,由於咱們不但願同一個觀察者被註冊屢次。該類的另外一個特色是,能夠直接從中刪除指定的對象,而不須要遍歷或搜索整個集合。
SplObjectStorage
類的實例之因此可以只存儲惟一的對象,是由於其 SplObjectStorage::attach()
方法的實現中先判斷了指定的對象是否已經被存儲:
function attach($obj, $inf = NULL) { if (is_object($obj) && !$this->contains($obj)) { $this->storage[] = array($obj, $inf); } }
下面咱們經過一個模擬案例來演示 SPL 在實現 Observer 設計模式上的威力。該案例模擬了一個網站的用戶管理模塊,該模塊包括 3 個主要功能:
每當這些功能完成後,都須要將密碼告知用戶。除了傳統的向用戶發送 Email 這種手段外,咱們還須要向用戶的手機發送短信,讓他們更加方便地知道密碼是什麼。假設咱們的網站還有一套站內的消息系統,咱們稱之爲小紙條,在用戶變動或重置密碼後,向他們發送小紙條會令他們高興的。
通過分析,該案例適合使用 Observer 設計模式解決,由於將密碼告知用戶的多種手段與用戶密碼的改變——不管是從無到有,用戶主動變動,仍是系統重置——造成了多對一的關係。
咱們決定定義一個 User 類表示用戶,實現需求中的 3 個功能。該類就是 Observer 設計模式中的目標(Subject)角色。咱們還須要一組類,實現利用各類手段向用戶發送新密碼的功能,這些類就充當了 Observer 設計模式中的觀察者(Observer)角色。
通過簡單地分析後,咱們畫出 UML 類圖:
根據 UML 類圖,首先,定義 1 個名爲 User 的類模擬案例中的用戶。儘管實際網站中的用戶要有更多的屬性,特別是一般須要用 ID 來標識每一個用戶,可是咱們爲了突出本文的主題,只保留了案例所需的屬性。
<?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)
接收一個用戶信息的數組,有了這個數組,就能知道郵件該發給誰,新密碼是什麼了。爲了節約篇幅,並且三個信息發送類很是相像,下面只給出其中一個的源代碼,完整的源代碼能夠下載本文的附件獲得。
<?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
的話,就不能正確分行顯示了)。
<?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;
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