登入系統想必大家都做過,驗證用戶名密碼就登入成功,日誌系統應該記錄此次登錄,如果登入出錯,安全系統應該會記錄這次錯誤,郵件系統也應該會發送相關郵件給管理員,等等。這就好像登入系統被許多人監視一樣,一旦有什麼風吹草動,立即會被其它系統獲悉。那就用觀察者模式來試試,類別圖如下:
很簡單的模式,實作程式碼:
Php程式碼
<?php interface Observable{ function attach( Observer $observer ); function detach( Observer $observer ); function notify(); } class login implements Observable{ const LOGIN_USER_UNKNOW = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $status = array(); private $observers = array(); public function setStatus( $status, $user, $ip ) { $this->status = array( $status, $user, $ip ); } public function getStatus() { return $this->status; } public function handleLogin( $user, $pass, $ip ) { switch ( mt_rand( 1, 3 ) ) { case 1: $this->setStatus( self::LOGIN_USER_UNKNOW, $user, $ip ); $ret = false; break; case 2: $this->setStatus( self::LOGIN_WRONG_PASS, $user, $ip ); $ret = false; break; case 3: $this->setStatus( self::LOGIN_ACCESS, $user, $ip ); $ret = true; break; } $this->notify(); return $ret; } public function attach( Observer $observer ) { $this->observers[] = $observer; } public function detach( Observer $observer ) { $newObservers = array(); foreach ( $this->observers as $obs ) { if ( $obs !== $observer ) $newObservers[] = $obs; } $this->observers = $newObservers; } public function notify() { foreach ( $this->observers as $obs ) { $obs->update( $this ); } } } interface Observer{ function update( Observable $observable ); } class SecurityMonitor implements Observer{ function update( Observable $observable ) { $status = $observable->getStatus(); if($status[0] == Login::LOGIN_WRONG_PASS){ echo __CLASS__.":".$status[1]."于".$status[2]."登录失败"; } } } $login = new Login(); $login->attach(new SecurityMonitor()); $login->handleLogin('XXX','XXX','127.0.0.1'); ?>
於70. 0.1登入失敗[Finished in 0.1s]
程式碼中可以看到login物件主動加入SecurityMonitor物件觀察。這樣要呼叫Login::getStatus(),SecurityMonitor類別就必須了解更多資訊。雖然呼叫發生於一個ObServable物件上,但無法保證物件也是一個Login物件。為解決這個問題,有一個方法:斷斷續續保持ObServable介面的通用性,由ObServer類別負責保證它們的主體是正確的類型。它們甚至能將自己添加到主體上。類別圖如下:
實作程式碼如下:
Php程式碼
<?php interface Observable{ function attach( Observer $observer ); function detach( Observer $observer ); function notify(); } class login implements Observable{ const LOGIN_USER_UNKNOW = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $status = array(); private $observers = array(); public function setStatus( $status, $user, $ip ) { $this->status = array( $status, $user, $ip ); } public function getStatus() { return $this->status; } public function handleLogin( $user, $pass, $ip ) { switch ( mt_rand( 1, 3 ) ) { case 1: $this->setStatus( self::LOGIN_USER_UNKNOW, $user, $ip ); $ret = false; break; case 2: $this->setStatus( self::LOGIN_WRONG_PASS, $user, $ip ); $ret = false; break; case 3: $this->setStatus( self::LOGIN_ACCESS, $user, $ip ); $ret = true; break; } $this->notify(); return $ret; } public function attach( Observer $observer ) { $this->observers[] = $observer; } public function detach( Observer $observer ) { $newObservers = array(); foreach ( $this->observers as $obs ) { if ( $obs !== $observer ) $newObservers[] = $obs; } $this->observers = $newObservers; } public function notify() { foreach ( $this->observers as $obs ) { $obs->update( $this ); } } } interface Observer{ function update( Observable $observable ); } //以上代码和上例是一样的 abstract class LoginObserver implements Observer{ private $login; public function __construct( Login $login ) { $this->login = $login; $login->attach( $this ); } public function update( Observable $observable ) { if ( $this->login === $observable ) $this->doUpdate( $observable ); } abstract function doUpdate( Login $login ); } class SecurityMonitor extends LoginObserver{ public function doUpdate( Login $login ) { $status = $login->getStatus(); if ( $status[0] == Login::LOGIN_WRONG_PASS ) echo __CLASS__.":".$status[1]."于".$status[2]."登录失败"; } } $login = new Login(); new SecurityMonitor($login);//<strong>此外login对象是被动被观察的</strong> $login->handleLogin( 'XXX', 'XXX', '127.0.0.1' ); ?>
運作結果與上範例相同
在php5後,內建的SPL擴充提供了對觀察者模式的原生支援。將上例透過SPL改進後:
Php程式碼
<?php class login implements SplSubject{ const LOGIN_USER_UNKNOW = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $status = array(); // private $observers = array(); private $storage; public function __construct() { $this->storage = new SplObjectStorage(); } public function setStatus( $status, $user, $ip ) { $this->status = array( $status, $user, $ip ); } public function getStatus() { return $this->status; } public function handleLogin( $user, $pass, $ip ) { switch ( mt_rand( 1, 3 ) ) { case 1: $this->setStatus( self::LOGIN_USER_UNKNOW, $user, $ip ); $ret = false; break; case 2: $this->setStatus( self::LOGIN_WRONG_PASS, $user, $ip ); $ret = false; break; case 3: $this->setStatus( self::LOGIN_ACCESS, $user, $ip ); $ret = true; break; } $this->notify(); return $ret; } public function attach( SplObserver $observer ) { $this->storage->attach( $observer ); } public function detach( SplObserver $observer ) { $this->storage->detach( $observer ); } public function notify() { foreach ( $this->storage as $obs ) { $obs->update( $this ); } } } abstract class LoginObserver implements SplObserver{ private $login; public function __construct( Login $login ) { $this->login = $login; $login->attach( $this ); } public function update( SplSubject $subject ) { if ( $this->login === $subject ) $this->doUpdate( $subject ); } abstract function doUpdate( Login $login ); } class SecurityMonitor extends LoginObserver{ public function doUpdate( Login $login ) { $status = $login->getStatus(); if ( $status[0] == Login::LOGIN_WRONG_PASS ) echo __CLASS__.":".$status[1]."于".$status[2]."登录失败"; } } $login = new Login(); new SecurityMonitor( $login ); $login->handleLogin( 'XXX', 'XXX', '127.0.0.1' ); ?>
程式碼都寫完了,還是要懂點理論的。
觀察者模式的定義
定義物件間一種一對多的依賴關係,使得每當一個物件改變狀態,則所有依賴它的物件都會被通知並被自動更新。觀察者模式由四種角色構成:
1、Subject被觀察者
定義被觀察者必須實現的職責,它必須能夠動態地增加、取消觀察者。它一般是抽象類別或實現類,僅僅完成作為被觀察者必須實現的職責,管理觀察者並通過觀察者。
2、Observer觀察者
觀察者接收到訊息後,即進行update操作,對接收到的資訊進行處理。
3、ConcreteSubject具體的被觀察者
定義被觀察者自己的業務邏輯,同時定義對哪些事件進行通知。
4、ConcreteObserver具體的觀察者
每個觀察在接收到訊息後的處理反應是不同,各個觀察者有自己的處理邏輯。
觀察者模式的優點
1、觀察者和被觀察者之間是抽象耦合
如此設計,則不管是增加觀察者還是被觀察者都非常容易擴展,而且在java、php中都已經實現的抽象層級的定義,在系統擴展方面更是得心應手。
2、建立一套觸發機制
根據單一職責原則,每個類別的職責是單一的,那麼怎麼把各個單一的職責串聯成真實世界的複雜的邏輯關係呢?觀察者模式可以完美地實現這裡的鏈條形式
觀察者模式的缺點
觀察者模式需要考慮開發效率和運行效率的問題,一個被觀察者,多個觀察者,開發和調試就會比較複雜,而且在php中訊息的通知是順序執行,一個觀察者卡殼,會影響整體的執行效率。在這種情況下,一般會多考慮異步的方式。多級觸發時的效率更是讓人擔憂,設計時會注意。
觀察者模式的使用情境
1、關聯行為場景。要注意的是,關係行為是可分割的,而不是「組合」關係
2、事件多層次觸發場景
3、跨系統的訊息交換場景,如訊息佇列的處理機制