系統中的每一個類應將重點放在某一個功能上,而不是其餘方面。一個對象只作一件事情,而且將他作好。
定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,有可能致使其它依賴對象的修改更新,那麼開發任務會很快變成一個產生bug和消除bug的惡性循環。當咱們建立一個對象的時候,一個對象的建立應當儘量減小和其它對象間的耦合!一個對象的改變儘量的不會引發代碼庫其它地方的修改。使用觀察者模式能有效的解決此問題,一個對象(目標對象)的狀態發生改變,全部的依賴對象(觀察者對象)都將獲得通知並被自動更新。php
觀察者模式(有時又被稱爲模型-視圖(View)模式、源-收聽者(Listener)模式或從屬者模式)是軟件設計模式的一種。在此種模式中,一個目標物件管理全部相依於它的觀察者物件,而且在它自己的狀態改變時主動發出通知。這一般經過呼叫各觀察者所提供的方法來實現。此種模式一般被用來實現事件處理系統。html
問題
假設一個負責處理用戶登陸的類:設計模式
class Login { const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $_status = array(); public function handleLogin($user, $pass, $ip) { switch (rand(1,3)) { case self::LOGIN_ACCESS: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $ret = true; break; case self::LOGIN_WRONG_PASS: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $ret = false; break; case self::LOGIN_USER_UNKNOWN: default: $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip); $ret = false; break; } return $ret; } private function setStatus($status, $user, $ip) { $this->_status = array($status, $user, $ip); } public function getStatus() { return $this->_status; } } $login = new Login(); $login->handleLogin('BNDong', '123456', '127.0.0.1'); var_dump($login->getStatus());
固然這個類並無實際功能, handleLogin 方法會存儲驗證用戶數據,該方法有3個潛在的結果。狀態標籤會被設置爲 LOGIN_USER_UNKNOWN 、 LOGIN_WRONG_PASS 或 LOGIN_ACCESS 。ide
如今看上去還能夠,可是一個登陸組件可不可能只有這麪點東西,咱們試着增長需求:(代碼的腐敗就是不斷的迭代出來的)函數
記錄登陸IP地址工具
public function handleLogin($user, $pass, $ip) { ... Logger::logIp($user, $ip, $this->getStatus()); ... }
登陸失敗發送郵件通知管理員優化
public function handleLogin($user, $pass, $ip) { ... !$ret && Notifier::mailWarning($user, $ip, $this->getStatus()); ... }
固然這些都是簡單的功能,可是依這種方式來處理 Login 類,會發現該類和系統的依賴愈來愈深,代碼的擴展和複用性愈來愈差! handleLogin 處理的東西愈來愈多。this
實現
觀察者模式的核心是把客戶元素(觀察者)從一箇中心類(主體)中分離開來。當主體知道事件發生時,觀察者須要被通知到。同時,咱們並不但願將主體與觀察者之間的關係進行硬編碼。編碼
爲了達到這個目的,咱們容許觀察者在主體上進行註冊。spa
interface Observable { public function attach(Observer $observer); public function detach(Observer $observer); public function notify(); } class Login implements Observable { const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $_status = array(); private $_observers; public function __construct() { $this->_observers = array(); } public function handleLogin($user, $pass, $ip) { switch (rand(1,3)) { case self::LOGIN_ACCESS: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $ret = true; break; case self::LOGIN_WRONG_PASS: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $ret = false; break; case self::LOGIN_USER_UNKNOWN: default: $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip); $ret = false; break; } $this->notify(); return $ret; } private function setStatus($status, $user, $ip) { $this->_status = array($status, $user, $ip); } public function getStatus() { return $this->_status; } 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); } } }
如今 Login 類管理着一系列觀察者對象。這些觀察者能夠由第三方經過 attach 方法添加進 Login 類,也能夠經過 detach 方法來移除。 notify 方法用來告訴觀察者一些相關事情發生了。 notify 方法會遍歷觀察者列表,調用每一個觀察者的 update 方法。
Login 類在它的 handleLogin 方法中調用 notify 方法。而後定義 Observer 接口,任何實現這個接口的對象均可以經過 attach 方法加入 Login 類中。
interface Observer { public function update(Observable $observable); } class SecurityMonitor implements Observer { public function update(Observable $observable) { $status = $observable->getStatus(); if ($status[0] == Login::LOGIN_WRONG_PASS) { // 發送郵件給系統管理員 print __CLASS__.":發送郵件給系統給管理員<br>"; } } } $login = new Login(); $login->attach(new SecurityMonitor()); $login->handleLogin('BNDong', '123456', '127.0.0.1');
至此實現了一個觀察者模式,減小了各個對象之間的耦合。
優化
這裏還存在一個問題,獲取主體類狀態是經過 getStatus 方法來獲取的,而並不能判斷調用的 getStatus 方法是存在而且可用的,因此要解決這個問題。
第一種方法:修改接口 Observer 中 update 方法參數 $observable 類型約束爲 Login ,可是這樣整個結構就被一個類限制了,多個登陸類不能兼容,因此不推薦!!
第二種方法:在接口 Observable 中添加 getStatus 方法,可是這樣會失去接口的通用性!!
第三種方法:繼續保持 Observable 接口的通用性,將會添加 Observer 類型的對象來執行一些它們共有的任務。
下面針對第三種方法來優化上面的代碼:
使用自建類優化
建立一個抽象超類:
abstract class LoginObserver implements Observer { private $_login; public function __construct(Login $login) { $this->_login = $login; $login->attach($this); } public function update(Observable $observable) { if ($observable == $this->_login) { $this->doUpdate($observable); } } abstract protected function doUpdate(Login $login); }
LoginObserver 類的構造函數須要一個 Login 對象做爲參數。 LoginObserver 保存對 Login 對象的引用,而且調用 Login::attach() 方法。當 update 方法被調用時, LoginObserver 會檢查參數傳入的 Observable 對象是不是正確的引用,而後 LoginObserver 會調用模板方法 doUpdate 。如今能夠建立一批 LoginObserver 對象,它們可以判斷使用的是 Login 對象,而不是任意 Observable 對象:
class SecurityMonitor extends LoginObserver { public function doUpdate(Login $login) { $status = $login->getStatus(); if ($status[0] == Login::LOGIN_WRONG_PASS) { // 發送郵件給系統管理員 print __CLASS__.":發送郵件給系統給管理員<br>"; } } } class GeneralLogger extends LoginObserver { public function doUpdate(Login $login) { $status = $login->getStatus(); // 記錄登陸數據到日誌 print __CLASS__.":記錄登陸數據到日誌<br>"; } } $login = new Login(); new SecurityMonitor($login); new GeneralLogger($login); $login->handleLogin('BNDong', '123456', '127.0.0.1');
所以在主體類和觀察者之間建立了一個很靈活的關係。
使用PHP內置SPL優化
PHP經過內置的SPL(Standard PHP Library,PHP標準類)擴展提供了對觀察者模式的原生支持。其中的觀察者(Observer)由3個元素組成:SplObserver、SplSubject 和 SplObjectStorage。SplObserver 和 SplSubject 都是接口,與以前示例中的 Observer 和 Observable 接口徹底相同。SplObjectStorage 是一個工具類,用於更好的存儲對象和刪除對象。
/** * The <b>SplSubject</b> interface is used alongside * <b>SplObserver</b> to implement the Observer Design Pattern. * @link http://php.net/manual/en/class.splsubject.php */ interface SplSubject { /** * Attach an SplObserver * @link http://php.net/manual/en/splsubject.attach.php * @param SplObserver $observer <p> * The <b>SplObserver</b> to attach. * </p> * @return void * @since 5.1.0 */ public function attach (SplObserver $observer); /** * Detach an observer * @link http://php.net/manual/en/splsubject.detach.php * @param SplObserver $observer <p> * The <b>SplObserver</b> to detach. * </p> * @return void * @since 5.1.0 */ public function detach (SplObserver $observer); /** * Notify an observer * @link http://php.net/manual/en/splsubject.notify.php * @return void * @since 5.1.0 */ public function notify (); }
/** * The <b>SplObserver</b> interface is used alongside * <b>SplSubject</b> to implement the Observer Design Pattern. * @link http://php.net/manual/en/class.splobserver.php */ interface SplObserver { /** * Receive update from subject * @link http://php.net/manual/en/splobserver.update.php * @param SplSubject $subject <p> * The <b>SplSubject</b> notifying the observer of an update. * </p> * @return void * @since 5.1.0 */ public function update (SplSubject $subject); }
/** * The SplObjectStorage class provides a map from objects to data or, by * ignoring data, an object set. This dual purpose can be useful in many * cases involving the need to uniquely identify objects. * @link http://php.net/manual/en/class.splobjectstorage.php */ class SplObjectStorage implements Countable, Iterator, Traversable, Serializable, ArrayAccess { /** * Adds an object in the storage * @link http://php.net/manual/en/splobjectstorage.attach.php * @param object $object <p> * The object to add. * </p> * @param mixed $data [optional] <p> * The data to associate with the object. * </p> * @return void * @since 5.1.0 */ public function attach ($object, $data = null) {} /** * Removes an object from the storage * @link http://php.net/manual/en/splobjectstorage.detach.php * @param object $object <p> * The object to remove. * </p> * @return void * @since 5.1.0 */ public function detach ($object) {} /** * Checks if the storage contains a specific object * @link http://php.net/manual/en/splobjectstorage.contains.php * @param object $object <p> * The object to look for. * </p> * @return bool true if the object is in the storage, false otherwise. * @since 5.1.0 */ public function contains ($object) {} /** * Adds all objects from another storage * @link http://php.net/manual/en/splobjectstorage.addall.php * @param SplObjectStorage $storage <p> * The storage you want to import. * </p> * @return void * @since 5.3.0 */ public function addAll ($storage) {} /** * Removes objects contained in another storage from the current storage * @link http://php.net/manual/en/splobjectstorage.removeall.php * @param SplObjectStorage $storage <p> * The storage containing the elements to remove. * </p> * @return void * @since 5.3.0 */ public function removeAll ($storage) {} /** * Removes all objects except for those contained in another storage from the current storage * @link http://php.net/manual/en/splobjectstorage.removeallexcept.php * @param SplObjectStorage $storage <p> * The storage containing the elements to retain in the current storage. * </p> * @return void * @since 5.3.6 */ public function removeAllExcept ($storage) {} /** * Returns the data associated with the current iterator entry * @link http://php.net/manual/en/splobjectstorage.getinfo.php * @return mixed The data associated with the current iterator position. * @since 5.3.0 */ public function getInfo () {} /** * Sets the data associated with the current iterator entry * @link http://php.net/manual/en/splobjectstorage.setinfo.php * @param mixed $data <p> * The data to associate with the current iterator entry. * </p> * @return void * @since 5.3.0 */ public function setInfo ($data) {} /** * Returns the number of objects in the storage * @link http://php.net/manual/en/splobjectstorage.count.php * @return int The number of objects in the storage. * @since 5.1.0 */ public function count () {} /** * Rewind the iterator to the first storage element * @link http://php.net/manual/en/splobjectstorage.rewind.php * @return void * @since 5.1.0 */ public function rewind () {} /** * Returns if the current iterator entry is valid * @link http://php.net/manual/en/splobjectstorage.valid.php * @return bool true if the iterator entry is valid, false otherwise. * @since 5.1.0 */ public function valid () {} /** * Returns the index at which the iterator currently is * @link http://php.net/manual/en/splobjectstorage.key.php * @return int The index corresponding to the position of the iterator. * @since 5.1.0 */ public function key () {} /** * Returns the current storage entry * @link http://php.net/manual/en/splobjectstorage.current.php * @return object The object at the current iterator position. * @since 5.1.0 */ public function current () {} /** * Move to the next entry * @link http://php.net/manual/en/splobjectstorage.next.php * @return void * @since 5.1.0 */ public function next () {} /** * Unserializes a storage from its string representation * @link http://php.net/manual/en/splobjectstorage.unserialize.php * @param string $serialized <p> * The serialized representation of a storage. * </p> * @return void * @since 5.2.2 */ public function unserialize ($serialized) {} /** * Serializes the storage * @link http://php.net/manual/en/splobjectstorage.serialize.php * @return string A string representing the storage. * @since 5.2.2 */ public function serialize () {} /** * Checks whether an object exists in the storage * @link http://php.net/manual/en/splobjectstorage.offsetexists.php * @param object $object <p> * The object to look for. * </p> * @return bool true if the object exists in the storage, * and false otherwise. * @since 5.3.0 */ public function offsetExists ($object) {} /** * Associates data to an object in the storage * @link http://php.net/manual/en/splobjectstorage.offsetset.php * @param object $object <p> * The object to associate data with. * </p> * @param mixed $data [optional] <p> * The data to associate with the object. * </p> * @return void * @since 5.3.0 */ public function offsetSet ($object, $data = null) {} /** * Removes an object from the storage * @link http://php.net/manual/en/splobjectstorage.offsetunset.php * @param object $object <p> * The object to remove. * </p> * @return void * @since 5.3.0 */ public function offsetUnset ($object) {} /** * Returns the data associated with an <type>object</type> * @link http://php.net/manual/en/splobjectstorage.offsetget.php * @param object $object <p> * The object to look for. * </p> * @return mixed The data previously associated with the object in the storage. * @since 5.3.0 */ public function offsetGet ($object) {} /** * Calculate a unique identifier for the contained objects * @link http://php.net/manual/en/splobjectstorage.gethash.php * @param $object <p> * object whose identifier is to be calculated. * @return string A string with the calculated identifier. * @since 5.4.0 */ public function getHash($object) {} }
下面是改進過的示例代碼:
class Login implements SplSubject { const LOGIN_USER_UNKNOWN = 1; const LOGIN_WRONG_PASS = 2; const LOGIN_ACCESS = 3; private $_status = array(); private $_storage; public function __construct() { $this->_storage = new SplObjectStorage(); } public function handleLogin($user, $pass, $ip) { switch (rand(1,3)) { case self::LOGIN_ACCESS: $this->setStatus(self::LOGIN_ACCESS, $user, $ip); $ret = true; break; case self::LOGIN_WRONG_PASS: $this->setStatus(self::LOGIN_WRONG_PASS, $user, $ip); $ret = false; break; case self::LOGIN_USER_UNKNOWN: default: $this->setStatus(self::LOGIN_USER_UNKNOWN, $user, $ip); $ret = false; break; } $this->notify(); return $ret; } private function setStatus($status, $user, $ip) { $this->_status = array($status, $user, $ip); } public function getStatus() { return $this->_status; } 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 ($subject == $this->_login) { $this->doUpdate($subject); } } abstract protected function doUpdate(Login $login); } class SecurityMonitor extends LoginObserver { public function doUpdate(Login $login) { $status = $login->getStatus(); if ($status[0] == Login::LOGIN_WRONG_PASS) { // 發送郵件給系統管理員 print __CLASS__.":發送郵件給系統給管理員<br>"; } } } class GeneralLogger extends LoginObserver { public function doUpdate(Login $login) { $status = $login->getStatus(); // 記錄登陸數據到日誌 print __CLASS__.":記錄登陸數據到日誌<br>"; } } $login = new Login(); new SecurityMonitor($login); new GeneralLogger($login); $login->handleLogin('BNDong', '123456', '127.0.0.1');
參考資料
《深刻PHP面向對象、模式與實踐》(第三版)
https://baike.baidu.com/item/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F/5881786?fr=aladdin
http://www.runoob.com/design-pattern/observer-pattern.html