設計模式學習筆記 - 3

寫在前面

今日特餐:觀察者模式 (Observer)

先稍微複習上一篇筆記《設計模式學習筆記 - 2》的內容-策略模式:

可以定義和封裝一系列的演算法,並且讓它們是可替換/對調的。這個模式可以讓你在不影響用戶端的情況下獨立改變演算法。
我們透過冒險遊戲的角色及武器,對此模式有一定的認識與瞭解。

今天要學習的設計模式是觀察者模式,以下正文:

Observer 觀察者模式

發佈者 + 訂閱者 = 觀察者模式

觀察者模式定義務鍵之間的一對多依賴關係,當一個物件改變狀態時,依賴它的物件都會自動收到通知與更新。

可以想成youtuber 在影片的開頭或結尾,都會告訴觀眾,記得按讚、訂閱、開啟小鈴鐺,訂閱+開啟小鈴鐺,就可以在該youtuber 更新影片的時候,收到第一手的通知,而有訂閱的觀眾就成了觀察者。

為了確保每個youtuber 的行為,與每個觀眾的行為互相不干擾,所以在設計上會將youtuber 及 觀眾個別實做相同的介面:
youtuberInterface 會有訂閱、取消訂閱等方法,而AudienceInterface 會有接收通知的方法,確保不會因為每個youtuber 或觀眾不同,導致通知的時候出現問題。

Observer

根據上圖,我們透過簡單的程式碼來實做看看,其中我將上傳影片、取得訂閱者清單、增加訂閱者、取消訂閱等方法都直接放入介面中,是為了確定每個youtuber都是在相同的基礎行為上。

  • YoutuberInterface

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?php

    Interface Youtuber {
    public function register(Audience $audience);
    public function remove(Audience $audience);
    public function notifyAudience();
    public function uploadVideo();
    public function getAudienceList();
    public function setAudience(Audience $audience);
    public function removeAudience(Audience $audience);
    }
  • AudienceInterface

    1
    2
    3
    4
    5
    <?php

    Interface Audience {
    public function update(Youtuber $youtuber);
    }
  • YoutuberImplement

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    <?php

    class YoutuberImpl implements Youtuber {

    public $name;
    private $audienceList = [];

    public function __construct($name) {
    $this->name = $name;
    }
    public function register(Audience $audience) {
    echo 'You are an observer now!' . PHP_EOL;
    $this->setAudience($audience);
    echo 'there are ' . count($this->getAudienceList()) . ' audience !' . PHP_EOL;
    }
    public function remove(Audience $audience) {
    echo 'You are no longer an observer!' . PHP_EOL;
    $this->removeAudience($audience);
    }
    public function notifyAudience() {
    $audienceList = $this->getAudienceList();
    foreach ($audienceList as $audience) {
    $audience->update($this);
    }
    }

    public function uploadVideo() {
    echo 'video upload successful!' . PHP_EOL;
    $this->notifyAudience();
    }

    public function getAudienceList() {
    return $this->audienceList;
    }

    public function setAudience(Audience $audience) {
    $audienceList = $this->audienceList;
    $audienceList[] = $audience;
    $this->audienceList = $audienceList;
    }

    public function removeAudience(Audience $audience) {
    $audienceList = $this->audienceList;
    $newAudienceList = [];

    foreach ($audienceList as $item) {
    if ($audience != $item) {
    $newAudienceList[] = $item;
    }
    }

    $this->audienceList = $newAudienceList;
    echo 'there are ' . count($this->getAudienceList()) . ' audience !' . PHP_EOL;
    }
    }
  • AudienceImplement

    1
    2
    3
    4
    5
    6
    7
    <?php

    class AudienceImpl implements Audience {
    public function update(Youtuber $youtuber) {
    echo $youtuber->name . ' has uploaded a new video!' . PHP_EOL;
    }
    }
  • 測試

測試的部分我們只操作一個頻道及一個觀眾,作為簡單的示例:
我們建立一個新的頻道叫做蔡阿嘎,以及一位觀眾Andy,
Andy 可以針對蔡阿嘎這個頻道進行訂閱、取消訂閱,而蔡阿嘎頻道可以上傳影片,並且通知訂閱者Andy。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

$andy = new AudienceImpl();
$Aga = new YoutuberImpl('蔡阿嘎');

$Aga->register($andy);
# You are an observer now!
# there are 1 audience !

$Aga->uploadVideo();
# video upload successful!
# 蔡阿嘎 has uploaded a new video!

$Aga->remove($andy);
# You are no longer an observer!
# there are 0 audience !

思考

這邊例子僅簡單以echo 訊息的方式表示通知。未來可能要思考,通知的時候是傳遞哪些資料過去,傳遞所有資訊的話會不會造成觀察者的負擔。
再以書上的例子作為思考的出發點,要設計一個氣象站(Subject),每當天氣資訊更新的時候,就通知觀察者(Observer)們溫度、濕度、氣壓、降雨機率…等所有的天氣資訊。
但可能不是每個觀察者都需要所有的天氣資訊,比方說我每天出門前,我會在意的是今天的溫度及降雨機率,那麼其他資訊對我來說就是多餘的了。

所以可以調整為,天氣站要開放讓觀察者們自行取得需要的資料,而通知僅僅是告知他們,天氣資訊已經更新了。
個別的觀察者因為收到這則通知,自行向氣象站索取所需的資料:
getTemperature();
getHumidity();


以Youtube 的例子稍微說明,但實際的Youtube 應該做了更多更複雜的事情,
今天的筆記-觀察者模式到這邊告一段落,如果有謬誤的地方,歡迎email 給我指正或來信討論,謝謝!