php 觀察者模式
觀察者模式,也稱釋出-訂閱模式,定義了一個被觀察者和多個觀察者的、一對多的物件關係。
在被觀察者狀態發生變化的時候,它的所有觀察者都會收到通知,並自動更新。
觀察者模式通常用在實時事件處理系統、元件間解耦、資料庫驅動的訊息佇列系統,同時也是MVC設計模式中的重要組成部分。
以下我們以訂單建立為例。
當訂單建立後,系統會發送郵件和簡訊,並儲存日誌記錄。
1 問題
在沒有用觀察者模式的時候,如下:
class Order
{
// 訂單狀態
private $state = 0;
// 訂單狀態有變化時傳送通知
public function addOrder()
{
$this->state = 1;
// 傳送郵件
Email::update($this->state);
// 簡訊通知
Message::update($this->state);
// 記錄日誌
Log::update();
// 其他更多通知
}
}
程式碼中,在Order
類中呼叫各類的方法來實現通知。當在客戶端建立訂單:
$order = new Order();
$order->addOrder();
就會同時產生三個通知:傳送郵件、傳送簡訊和記錄日誌。
在系統小的時候,這是非常快捷有效的方式。
可是,當系統變大的時候,這種方法馬上面臨難以擴充套件的問題,並且容易出錯:
- 如果訂單不需要某種通知,比如不需要記錄日誌,則必須修改
Order
類,做狀態的判斷; - 如果再加一種通知方式,如系統訊息通知,則除了增加新類,同時還需要修改
Order
類和客戶端。
這兩條都不符合面向物件中的開閉原則,會讓系統越來越難維護。
2 解決
接下來我們用觀察者模式解決這個問題。
2.1 被觀察者
被觀察者是一些具體的例項,比如訂單管理、使用者登陸、評論回覆、狀態稽核等等。
別的功能會依賴於它們的狀態進行各種動作。
/**
* 被觀察者介面
*/
interface Observable
{
// 新增/註冊觀察者
public function attach(Observer $observer);
// 刪除觀察者
public function detach(Observer $observer);
// 觸發通知
public function notify();
}
/**
* 被觀察者
* 職責:新增觀察者到$observers屬性中,
* 有變動時通過notify()方法執行通知
*/
class Order implements Observable
{
// 儲存觀察者
private $observers = array();
// 訂單狀態
private $state = 0;
// 新增(註冊)觀察者
public function attach(Observer $observer)
{
$key = array_search($observer, $this->observers);
if ($key === false) {
$this->observers[] = $observer;
}
}
// 移除觀察者
public function detach(Observer $observer)
{
$key = array_search($observer, $this->observers);
if ($key !== false) {
unset($this->observers[$key]);
}
}
// 遍歷呼叫觀察者的update()方法進行通知,不關心其具體實現方式
public function notify()
{
foreach ($this->observers as $observer) {
// 把本類物件傳給觀察者,以便觀察者獲取當前類物件的資訊
$observer->update($this);
}
}
// 訂單狀態有變化時傳送通知
public function addOrder()
{
$this->state = 1;
$this->notify();
}
// 獲取提供給觀察者的狀態
public function getState()
{
return $this->state;
}
}
被觀察者至少要實現attach()
、detach()
、notify()
三個方法,用以新增、刪除和通知觀察者。
通知的方式是,在類中的其他方法(如:建立訂單)呼叫notify()
方法。
另外,觀察者可能用到被觀察者的一些狀態資訊。
所以,要在notify()
中把當前物件作為引數傳給觀察者,方便其通過提供的public
方法獲得被觀察者的狀態資訊。
本例用getState()
方法供給觀察者獲取狀態資訊。
2.2 觀察者
觀察者可能有多個,但每個觀察者都必須實現Observer
介面規定的update()
方法,這是接收被觀察者通知的唯一渠道。
/**
* 觀察者介面
*/
interface Observer
{
// 接收到通知的處理方法
public function update(Observable $observable);
}
/**
* 觀察者1:傳送郵件
*/
class Email implements Observer
{
public function update(Observable $observable)
{
$state = $observable->getState();
if ($state) {
echo '傳送郵件:您已經成功下單。';
} else {
echo '傳送郵件:下單失敗,請重試。';
}
}
}
/**
* 觀察者2:簡訊通知
*/
class Message implements Observer
{
public function update(Observable $observable)
{
$state = $observable->getState();
if ($state) {
echo '簡訊通知:您已下單成功。';
} else {
echo '簡訊通知:下單失敗,請重試。';
}
}
}
/**
* 觀察者3:記錄日誌
*/
class Log implements Observer
{
public function update(Observable $observable)
{
echo '記錄日誌:生成了一個訂單記錄。';
}
}
這裡有三個觀察者:傳送郵件、簡訊通知和記錄日誌,它們都實現了update()
方法。
其中,傳送郵件和簡訊依賴於訂單、也就是被觀察者的狀態,來決定傳送訊息的內容,記錄日誌則不需要訂單的狀態。
2.3 客戶端
然後我們建立一個客戶端,內容:
// 建立觀察者物件
$email = new Email();
$message = new Message();
$log = new Log();
// 建立訂單物件
$order = new Order();
// 向訂單物件中註冊3個觀察者:傳送郵件、簡訊通知、記錄日誌
$order->attach($email);
$order->attach($message);
$order->attach($log);
// 新增訂單,新增時會自動傳送通知給觀察者
$order->addOrder();
echo '<br />';
// 刪除記錄日誌觀察者
$order->detach($log);
// 新增另一個訂單,會再次傳送通知給觀察著
$order->addOrder();
執行應用後,會輸出這樣的訊息:
傳送郵件:您已經成功下單。新增日誌:生成了一個訂單記錄。系統訊息:您已下單成功。 傳送郵件:您已經成功下單。新增日誌:生成了一個訂單記錄。
對於不需要通知的觀察者,用detach()
移出觀察者列表即可。
這種情況就解開了類之間的耦合。
2.4 新增觀察者
如果再需要新增一個觀察者,如下,只需要新增觀察者類本身,實現update()
方法。
/**
* 觀察者4:系統訊息
*/
class Alert implements Observer
{
public function update(Observable $observable)
{
echo '系統訊息:您的訂單有更新了~~~';
}
}
再到客戶端中註冊Alert
類到觀察者列表中:
// 建立“系統訊息”觀察者
$alert = new Alert();
// 註冊觀察者到訂單物件中
$order->attach($alert);
就能訂閱被觀察者的通知。
3 特點
在觀察者模式中,被觀察者完全不需要關心觀察者,在自身狀態有變化是,遍歷執行觀察者update()
方法即完成通知。
在觀察者模式中,被觀察者通過新增attach()
方法,提供給觀察者註冊,使自己變得可見。
當被觀察者改變時,給註冊的觀察者傳送通知。至於觀察者如何處理通知,被觀察者不需要關心。
這是一種良好的設計,物件之間不必相互理解,同樣能夠相互通訊。
UML如下:
面向物件程式設計中,任何物件的狀態都非常重要,它們是物件間互動的橋樑。
當一個物件的改變需要被其他物件關注時,觀察者模式就派上用場了。