這是一個設計模式系列,本書所有案例均來自「Head-First設計模式(中文版)」, 所有系列文章將同步到 Github地址, 歡迎大家
watch
,star
觀察者模式
定義了物件之間的一對多依賴,當一個物件改變狀態時,它的所有依賴者都將會收到通知並自動更新.
觀察者模式形容圖
設計謎題
有一個氣象觀察站,我們希望建立一個應用,有三種佈告板(用於顯示不同的氣象資料),當氣象站獲取到最新的測量資料時,我們希望三種佈告板能實時更新.
類圖設計
其中 WeatherData
用於獲取氣象站最新測量資料(三個get
方法),當資料更新時,會呼叫onChanged
方法(不要管為什麼,這是氣象站內部邏輯).
程式碼實現
主題介面
interface Sublect
{
public function registerObserver(Observer $observer);
public function removeObserver();
public function nitifyObservers();
}
複製程式碼
主題物件 WeatherData
class WeatherData implements Sublect
{
protected $observers = [];
protected $pressure, $temperature, $humidity;
public function registerObserver(Observer $observer)
{
if (array_search($observer, $this->observers) === false) {
$this->observers[] = $observer;
}
}
public function removeObserver()
{
if (($index = array_search($observer, $this->observers)) !== false) {
unset($this->observers[$index]);
}
}
public function nitifyObservers()
{
foreach ($this->observers as $observer) {
$observer->update($this->getPressure(), $this->getTemperature(), $this->getHumidity());
}
}
public function onChanged()
{
$this->nitifyObservers();
}
//獲取最新氣壓
public function getPressure()
{
return $this->pressure;
}
//獲取最新溫度
public function getTemperature()
{
return $this->temperature;
}
//獲取最新溼度
public function getHumidity()
{
return $this->humidity;
}
//測試
public function youNeedChanged()
{
$this->pressure = mt_rand(1, 99);
$this->temperature = mt_rand(1, 99);
$this->humidity = mt_rand(1, 99);
$this->onChanged();
}
}
複製程式碼
觀察者介面
interface Observer
{
//氣壓/溫度/溼度
public function update($pressure, $temperature, $humidity);
}
複製程式碼
顯示皮膚介面
interface DisplayElement
{
public function display();
}
複製程式碼
觀察者物件集
class CurrentConditionsDisplay implements Observer, DisplayElement
{
protected $subject;
protected $pressure, $temperature, $humidity;
//這裡為什麼會保留 Subject 介面的引用是為了方便的 remove 及 registe
public function __construct(Sublect $subject)
{
$this->subject = $subject;
$this->subject->registerObserver($this);
}
public function update($pressure, $temperature, $humidity)
{
$this->pressure = $pressure;
$this->temperature = $temperature;
$this->humidity = $humidity;
$this->display();
}
public function display()
{
echo "Current pressure: {$this->pressure}, Current temperature: {$this->temperature}";
}
}
//其他兩種佈告板省略
複製程式碼
測試
$weatherData = new WeatherData();
$display = new CurrentConditionsDisplay($weatherData);//把當前布告欄註冊成為觀察者
//$other = new OthersDisplay($weatherData);//把當前布告欄註冊成為觀察者
//$other = new OtherDisplay($weatherData);//把當前布告欄註冊成為觀察者
$weatherData->youNeedChanged();//氣象站資料更新了會導致佈告板實時更新
//Current pressure: 33, Current temperature: 46
複製程式碼
另一種形式的觀察者模式
我們知道,觀察者總是被動的接受主題物件的推送,但有些場景下,我們希望觀察者能主動的去獲取資料;畢竟觀察者數量這麼多,主題物件不可能事先知道每個觀察者需要的狀態,並且也不會導致明明只需要一點點資料,卻被迫收到一堆.
我們來重寫設計上面的問題.
類圖基本保持不變,只是在WeatherData
類新增了setChanged
方法並改變了Observer
介面update
簽名.
重構後的主題介面
interface Sublect
{
public function registerObserver(Observer $observer);
public function removeObserver();
public function nitifyObservers($args = null);
}
interface Observer
{
public function update(Sublect $subject, $object = null);
}
複製程式碼
重構後的主題物件
class WeatherData implements Sublect
{
protected $observers = [];
protected $pressure, $temperature, $humidity, $changed;
public function nitifyObservers($args = null)
{
if ($this->changed) {
foreach ($this->observers as $observer) {
$observer->update($this, $args);
}
$this->changed = false;
}
}
public function onChanged()
{
$this->setChanged();
$this->nitifyObservers([
`pressure` => $this->pressure,
`temperature` => $this->temperature,
`humidity` => $this->humidity,
]);
}
public function setChanged()//新增方法
{
$this->changed = true;
}
//其他方法保持不變
}
複製程式碼
重構後的佈告板物件
class CurrentConditionsDisplay implements Observer, DisplayElement
{
protected $subject;
protected $pressure, $temperature, $humidity;
//這裡為什麼會保留 Subject 介面的引用是為了方便的 remove 及 registe
public function __construct(Sublect $subject)
{
$this->subject = $subject;
$this->subject->registerObserver($this);
}
public function update(Sublect $subject, $object = null)
{
if ($subject instanceof Sublect) {
//你可以用 拉取 的形式獲取最新資料
$this->pressure = $subject->getPressure();
$this->temperature = $subject->getTemperature();
$this->humidity = $subject->getHumidity();
//也可以從推送資料中獲取
$this->pressure = $object[`pressure`];
$this->temperature = $object[`temperature`];
$this->humidity = $object[`humidity`];
}
$this->display();
}
public function display()
{
echo "Current pressure: {$this->pressure}, Current temperature: {$this->temperature}";
}
}
複製程式碼
為什麼要加一個 setChanged
方法
setChanged
讓你在更新觀察者時,有更多的彈性,能更適當的通知觀察者,比方說,如果沒有setCanged
方法,氣象站溫度變化十分之一度時,都會通知所有觀察者,你肯定不想讓這麼頻繁的更新吧.我們可以控制溫度變化達到一度時,呼叫 setChanged
,進行有效的更新.