設計模式 - Observer Pattern 觀察者模式
Intro
假設youtube頻道需要很多種通知機制(如:訂閱、鈴鐺、寄信),而在頻道新增影片時我們必須通知我們的使用者。
實作
首先,我們先新增YoutubeChannel類別,因為我們希望在新增影片的時候可以通知我們的觀眾,所以我在類別中給訂了一個private變數,希望能夠將我需要通知的對象給儲存進去。
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
| class YoutubeChannel { private $Notifications = [];
public function attach($Notification) { $this->Notifications[] = $Notification; }
public function detach($Notification) { foreach ($this->Notifications as $key => $notification) { if ($notification->getNotificationId() == $Notification->getNotificationId()) { array_splice($this->Notifications, $key, 1); } } }
public function getNotifications(): array { return $this->Notifications; }
public function notify(): array { $notification_console = []; foreach ($this->Notifications as $notification) { $notification_console[] = $notification->update(); } return $notification_console; } }
|
再來是我們的通知類別,各種不一樣的通知都需要實作不同的通知方法,所以這邊都預留了一個update()給YoutubeChannel類別使用
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
| class Mail { private $NotificationId;
public function __construct(int $AccountId) { $this->setNotificationId($AccountId); }
public function getNotificationId(): int { return $this->NotificationId; }
public function setNotificationId($NotificationId) { $this->NotificationId = $NotificationId; }
public function update(): string { return 'NotificationID=' . $this->getNotificationId() . '+信件通知'; } }
class Bells { private $NotificationId;
public function __construct(int $AccountId) { $this->setNotificationId($AccountId); }
public function getNotificationId(): int { return $this->NotificationId; }
public function setNotificationId($NotificationId) { $this->NotificationId = $NotificationId; }
public function update(): string { return 'NotificationID=' . $this->getNotificationId() . '+鈴鐺通知'; } }
|
當我們實際上在使用的時候,會像下面這種方式
1 2 3 4 5 6 7 8
| $SomeOnesChannel = new YoutubeChannel(); $Bells = new Bells(1); $Mail = new Mail(2); $SomeOnesChannel->attach($Bells); $SomeOnesChannel->attach($Mail); $Notifications = $SomeOnesChannel->notify();
|
類別圖
而當我們去關心這個通知機制的類別圖我們會發現,YoutubeChannel類別與通知類別處於一種基於實體類別的強依賴狀態。
Plant UML
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
| @startuml
skinparam classAttributeIconSize 0
class Mail{ {field} - NotificationId : int {method} + __construct(int $AccountId) : void {method} + getNotificationId() : int {method} + setNotificationId() : void {method} + update() : void }
class Bells{ {field} - NotificationId : int {method} + __construct(int $AccountId) : void {method} + getNotificationId() : int {method} + setNotificationId() : void {method} + update() : void }
class YoutubeChannel{ {field} - Notifications : array {method} + attach($Notification) : void {method} + detach($Notification) : void {method} + notify() : array {method} + getNotifications() : array
}
YoutubeChannel -down-> Mail : notify(update) YoutubeChannel -down-> Bells : notify(update)
@enduml
|
為了將耦合鬆散化,首先我們將YoutubeChannel類別中的attach()、detach()、notify()提取成interface,如此一來要是我有別的服務(例如:twitch)擁有類似的機制,我便可以透過實作這個interface來確保我的服務也擁有相似的行為
另外我將attach()、detach()的輸入參數限制為底下會提到的IObserver介面
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
| interface ISubject { public function attach(IObserver $Notifications);
public function detach(IObserver $Notification);
public function notify(): array; }
class YoutubeChannel implements ISubject { private $Notifications = [];
public function attach(IObserver $Notification) { $this->Notifications[] = $Notification; }
public function detach(IObserver $Notification) { foreach ($this->Notifications as $key => $notification) { if ($notification->getNotificationId() == $Notification->getNotificationId()) { array_splice($this->Notifications, $key, 1); } } }
public function getNotifications(): array { return $this->Notifications; }
public function notify(): array { $notification_console = []; foreach ($this->Notifications as $notification) { $notification_console[] = $notification->update(); } return $notification_console; } }
|
再來我們希望我們的被通知者都有相同的接口可以讓社群媒體通知,所以我們將update()也提取成介面,如此一來要是我有新增了別的通知方式(例如:Line),我只要實作這個interface就可以確保我的通知類別可以跟YoutubeChannel類別或是其他的社群媒體類別能夠安心的配合
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 56
| interface IObserver { public function update(): string; }
class Mail implements IObserver { private $NotificationId;
public function __construct(int $AccountId) { $this->setNotificationId($AccountId); }
public function getNotificationId(): int { return $this->NotificationId; }
public function setNotificationId($NotificationId) { $this->NotificationId = $NotificationId; }
public function update(): string { return 'NotificationID=' . $this->getNotificationId() . '+信件通知'; } }
class Bells implements IObserver { private $NotificationId;
public function __construct(int $AccountId) { $this->setNotificationId($AccountId); }
public function getNotificationId(): int { return $this->NotificationId; }
public function setNotificationId($NotificationId) { $this->NotificationId = $NotificationId; }
public function update(): string { return 'NotificationID=' . $this->getNotificationId() . '+鈴鐺通知'; } }
|
類別圖
透過這種方式重新整理之後,我們可以看到我們的類別關係有了改變,實體的相依變成以interface為基底去做依賴,我們通知者與被通知者之間的依賴給抽象化了
Plant UML
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
| @startuml
skinparam classAttributeIconSize 0
interface IObserver{ {method} + update() : void }
class Mail{ {field} - NotificationId : int {method} + __construct(int $AccountId) : void {method} + getNotificationId() : int {method} + setNotificationId() : void {method} + update() : void }
class Bells{ {field} - NotificationId : int {method} + __construct(int $AccountId) : void {method} + getNotificationId() : int {method} + setNotificationId() : void {method} + update() : void }
interface ISubject{ {method} + attach(IObserver $Notification) : void {method} + detach(IObserver $Notification) : void {method} + notify() : array }
class YoutubeChannel{ {field} - Notifications : array {method} + attach(IObserver $Notification) : void {method} + detach(IObserver $Notification) : void {method} + notify() : array {method} + getNotifications() : array }
Bells .up.|> IObserver Mail .up.|> IObserver YoutubeChannel .up.|> ISubject
ISubject -right-> IObserver : notify(update)
@enduml
|
透過提取ISubject與IObserver,不只將依賴從實體化為抽象,將強耦合變為弱耦合,我們還確保了通知者與被通知者的行為架構,此種設計方式我們稱之為「觀察者模式」
時機
- 想要在一個物件的狀態改變時通知很多相關物件的時候
- 某一個物件有變化時,對應物件也得跟著改變,但事先又不知道對應的物件要做的改變有多少時
- 當物件必須能夠通知其他的物件,但又不能假設後者是誰
目的
定義一種一對多的物件依賴關係,讓該物件的狀態一有變動,就自動通知其他相依物件做出該做的更新行為。
類別圖
Plant UML
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
| @startuml
skinparam classAttributeIconSize 0
interface IObserver{ {method} + update() } note right of IObserver : ISubscribe
interface ISubject{ {method} + attach(IObserver Observer) {method} + detach(IObserver Observer) {method} + notify() } note left of ISubject : IPublish
class Observer{ {method} + update() } note bottom of Observer : Subscriber
class Subject{ {method} + attach() {method} + detach() {method} + notify() } note bottom of Subject : Publisher
ISubject -right-> IObserver : Notify(update) Subject .up.|> ISubject Observer .up.|> IObserver @enduml
|
優點
- 這樣的類別關係遵守開放封閉原則,加入新的訂閱者並不會修改到發布者(反之亦然)
- 可以在程式運行的時候才動態的去構築類別間的依賴關係,且隨時可以解除
- Observer與Subject之間的耦合為抽象的、較微弱
缺點
- 接收過多額外通知
- 由於Observer互相獨立,如果我們要改變Subject的話,有可能因為改變而影響到數個Observer以及其相依的動作,而導致錯誤難以追查
實際案例
PHP
事實上,在PHP的Library中有提供一組可以實現Observer Pattern的Interface
SplSubject
SplObserver
在Stackoverflow上有人發問了這兩個介面的使用時機
Angular with RxJS
Angular作為前端框架,在更新頁面資料上使用了觀察者模式
我可以建立一個Observable的實例(也就是上面提到的Subject),在Observable實例中,定義了一個subscribe()函式(也就是上面提到的attach()),而subscribe()只能傳入符合Observer介面的參數(也就是上面提到的IObserver)
底下的範例為Angular在接API的時候使用RxJS去實作觀察者模式,可以讓response回來之後自動的去渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { from } from 'rxjs';
const data = from(fetch('/api/endpoint'));
data.subscribe({ next(response) { console.log(response); }, error(err) { console.error('Error: ' + err); }, complete() { console.log('Completed'); } });
|
類別圖
其架構的類別圖大概長得像式這樣(僅示意)
Plant UML
@startuml
skinparam classAttributeIconSize 0
class Observable{
{method} + subscribe(Observer observer)
{method} + SomeFunctionToCall_next()
}
class observer{
{method} + next()
{method} + error()
{method} + complete()
}
interface Observer{
{method} + next()
{method} + error()
{method} + complete()
}
Observable -right-> Observer : call next() to update
observer .up.|> Observer : implements
@enduml
```__