0%

觀察者設計模式(Observer Design Pattern)


Hi 👋 ~ 今天跟大家聊聊觀察者設計模式(Observer Design Pattern) aka「訂閱者模式」。

「記得按讚、訂閱、加分享!!」

我想有看過 YouTube 影片的人對這句話都不陌生,
這樣 YouTuber 在上新片的時候,
我們才能收到新片通知,他們才有點閱率 😅

觀察者設計模式是什麼?

  • 觀察者是一種行爲設計模式
  • 能夠產生「訂閱機制」的功能,發佈者(YouTuber)接受其他物件(觀衆)的訂閱後,讓發佈者(YouTuber)可以發佈通知給有訂閱的對象(觀衆),讓這些對象(觀衆)作出相對應的動作(例如:看新片)

這次我們一樣用個小故事來瞭解觀察者模式吧 🙂

背景設定

這次的主角是財富自由的米其林三星廚師 - 詹姆。

詹姆自從中了樂透之後,
就過著財富自由的人生。
但無所事事又不知道幹嘛,
於是他決定在家鄉開了一間餐廳 。

餐廳的營業時間都是看他心情而定,
有可能一個月都沒有營業也是常有的事情,
因爲他還在巴厘島上度假。

因爲受到媒體報導後,
大家衝著米其林三星的光環慕名而來。
但卻因爲營業時間總是不確定,
所以往往總是撲空,
要吃到詹姆的料理都快要比中樂透的機率還低了 😭

然而詹姆雖然開店的時間很任性,
心底也是個善良的好人,
不想要讓很多客人總是撲空,
於是他開了一個 Line 群組,
請想要收到「今日的營業時間」通知的客人加入這個群組。

這樣就可以在他想到要營業的時候,
在發送訊息通知客人,
就不用在讓他們在外頭苦苦等待了 🎉

觀察者設計模式組成

成員 Function
Publisher * addSubscriber(s Subscriber)
* removeSubscribers(s Subscriber)
* notifySubscribers()
Subscriber * update(context)

以這個故事爲例:

  • 湯姆就是 Publisher 的角色,而客人就是 Subscriber 的角色
  • Subscriber 可以把自己加入 Publisher 的通知清單(Line 群組)
  • Publisher 會通知 Subscriber 事件(今日的營業時間)
  • Subscriber 就會針對這個事情作出相對應的動作(針對營業時間決定是否要到餐廳吃飯或無法參加)

那接下來我們就來看看怎麼用程式碼來呈現吧!
以下用 golang 作爲範例

我們可以先設定 Publiser interface

1
2
3
4
5
6
// Publisher interface ---------------------------
type Publisher interface {
addSubscriber(Subscriber)
removeSubscriber(Subscriber)
notifySubscribers(int, int)
}

以下是詹姆對於 Publiser interface 的實作

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
// Publisher Implementation ---------------------------
type James struct {
name string
lineGroup []Subscriber
}

func (j *James) addSubscriber(s Subscriber) {
j.lineGroup = append(j.lineGroup, s)
}

func (j *James) removeSubscriber(s Subscriber) {
for i, _s := range j.lineGroup {
if _s.getName() == s.getName() {
j.lineGroup = append(j.lineGroup[:i], j.lineGroup[i+1:]...)
}
}
}

func (j *James) notifySubscribers(businessOpenHour, businessClosedHour int) {
fmt.Printf("✅ Line 群組通知 => \n%v: 今天開始營業時間是: %v:00 到 %v:00 喔!歡迎大家來! \n", j.name, businessOpenHour, businessClosedHour)
for _, s := range j.lineGroup {
s.update(businessOpenHour, businessClosedHour)
}
fmt.Println()
}

接下來是 Subscriber interface

1
2
3
4
5
// Subscriber interface ---------------------------
type Subscriber interface {
update(businessOpenHour, businessClosedHour int)
getName() string
}

客人對於 Subscriber interface 的實作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Customer struct {
name string
availableHourStart int
availableHourEnd int
}

func (c *Customer) update(businessOpenHour, businessClosedHour int) {
fmt.Println("___________________")
fmt.Printf("%v: 我們可以的時間是: %v:00 - %v:00 \n", c.name, c.availableHourStart, c.availableHourEnd)
if c.availableHourStart >= businessOpenHour && c.availableHourEnd <= businessClosedHour {
fmt.Println(c.name + ": 時間可以,我們去詹姆的餐廳吃飯吧!")
} else {
fmt.Println(c.name + ": 時間無法配合。。,下次再去好了。。。")
}
}

func (c *Customer) getName() string {
return c.name
}

實際執行

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
func main() {
// Subscriber
david := &Customer{name: "David", availableHourStart: 20, availableHourEnd: 22}
olivia := &Customer{name: "Olivia", availableHourStart: 18, availableHourEnd: 20}
tom := &Customer{name: "Tom", availableHourStart: 21, availableHourEnd: 24}

// Publisher
james := &James{name: "James"}
james.addSubscriber(david)
james.addSubscriber(olivia)
james.addSubscriber(tom)

// 第 1 次營業通知
james.notifySubscribers(19, 24)
// 第 2 次營業通知
james.notifySubscribers(20, 21)
// 第 3 次營業通知
james.notifySubscribers(18, 20)
}

✅ Line 群組通知 =>
James: 今天開始營業時間是: 19:0024:00 喔!歡迎大家來!
___________________
David: 我們可以的時間是: 20:00 - 22:00
David: 時間可以,我們去詹姆的餐廳吃飯吧!
___________________
Olivia: 我們可以的時間是: 18:00 - 20:00
Olivia: 時間無法配合。。,下次再去好了。。。
___________________
Tom: 我們可以的時間是: 21:00 - 24:00
Tom: 時間可以,我們去詹姆的餐廳吃飯吧!

✅ Line 群組通知 =>
James: 今天開始營業時間是: 20:0021:00 喔!歡迎大家來!
___________________
David: 我們可以的時間是: 20:00 - 22:00
David: 時間無法配合。。,下次再去好了。。。
___________________
Olivia: 我們可以的時間是: 18:00 - 20:00
Olivia: 時間無法配合。。,下次再去好了。。。
___________________
Tom: 我們可以的時間是: 21:00 - 24:00
Tom: 時間無法配合。。,下次再去好了。。。

✅ Line 群組通知 =>
James: 今天開始營業時間是: 18:0020:00 喔!歡迎大家來!
___________________
David: 我們可以的時間是: 20:00 - 22:00
David: 時間無法配合。。,下次再去好了。。。
___________________
Olivia: 我們可以的時間是: 18:00 - 20:00
Olivia: 時間可以,我們去詹姆的餐廳吃飯吧!
___________________
Tom: 我們可以的時間是: 21:00 - 24:00
Tom: 時間無法配合。。,下次再去好了。。。

使用觀察者設計模式的完整程式碼

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package main

import "fmt"

// Publisher interface ---------------------------
type Publisher interface {
addSubscriber(Subscriber)
removeSubscriber(Subscriber)
notifySubscribers(int, int)
}

// Publisher Implementation ---------------------------
type James struct {
name string
lineGroup []Subscriber
}

func (j *James) addSubscriber(s Subscriber) {
j.lineGroup = append(j.lineGroup, s)
}

func (j *James) removeSubscriber(s Subscriber) {
for i, _s := range j.lineGroup {
if _s.getName() == s.getName() {
j.lineGroup = append(j.lineGroup[:i], j.lineGroup[i+1:]...)
}
}
}

func (j *James) notifySubscribers(businessOpenHour, businessClosedHour int) {
fmt.Printf("✅ Line 群組通知 => \n%v: 今天開始營業時間是: %v:00 到 %v:00 喔!歡迎大家來! \n", j.name, businessOpenHour, businessClosedHour)
for _, s := range j.lineGroup {
s.update(businessOpenHour, businessClosedHour)
}
fmt.Println()
}

// Subscriber interface ---------------------------
type Subscriber interface {
update(businessOpenHour, businessClosedHour int)
getName() string
}

type Customer struct {
name string
availableHourStart int
availableHourEnd int
}

func (c *Customer) update(businessOpenHour, businessClosedHour int) {
fmt.Println("___________________")
fmt.Printf("%v: 我們可以的時間是: %v:00 - %v:00 \n", c.name, c.availableHourStart, c.availableHourEnd)
if c.availableHourStart >= businessOpenHour && c.availableHourEnd <= businessClosedHour {
fmt.Println(c.name + ": 時間可以,我們去詹姆的餐廳吃飯吧!")
} else {
fmt.Println(c.name + ": 時間無法配合。。,下次再去好了。。。")
}
}

func (c *Customer) getName() string {
return c.name
}

func main() {
// Subscriber
david := &Customer{name: "David", availableHourStart: 20, availableHourEnd: 22}
olivia := &Customer{name: "Olivia", availableHourStart: 18, availableHourEnd: 20}
tom := &Customer{name: "Tom", availableHourStart: 21, availableHourEnd: 24}

// Publisher
james := &James{name: "James"}
james.addSubscriber(david)
james.addSubscriber(olivia)
james.addSubscriber(tom)

// 第 1 次營業通知
james.notifySubscribers(19, 24)
// 第 2 次營業通知
james.notifySubscribers(20, 21)
// 第 3 次營業通知
james.notifySubscribers(18, 20)
}

這樣寫的好處是什麼?

降低發佈者與訂閱者之間的耦合關係。

想像一下如果沒有使用觀察者設計模式(詹姆沒有使用 Line 群組通知),
詹姆必須要知道 David、Olivia 這些人的話,
就會在詹姆通知邏輯裡發生每個人狀況的判斷,例如:

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


func (j *James) notify(businessOpenHour, businessClosedHour int) {
fmt.Printf("✅ \n%v: 今天開始營業時間是: %v:00 到 %v:00 喔!歡迎大家來! \n", j.name, businessOpenHour, businessClosedHour)
david := &Customer{name: "David", availableHourStart: 20, availableHourEnd: 22}
fmt.Printf("%v: 我們可以的時間是: %v:00 - %v:00 \n", c.name, c.availableHourStart, c.availableHourEnd)
if david.availableHourStart >= businessOpenHour && david.availableHourEnd <= businessClosedHour {
fmt.Println("Daivd: 時間可以,我們去詹姆的餐廳吃飯吧!")
} else {
fmt.Println("Daivd: 時間無法配合。。,下次再去好了。。。")
}
olivia := &Customer{name: "Olivia", availableHourStart: 18, availableHourEnd: 20}
if olivia.availableHourStart >= businessOpenHour && olivia.availableHourEnd <= businessClosedHour {
fmt.Println("Olivia: 時間可以,我們去詹姆的餐廳吃飯吧!")
} else {
fmt.Println("Olivia: 時間無法配合。。,下次再去好了。。。")
}
tom := &Customer{name: "Tom", availableHourStart: 21, availableHourEnd: 24}
if tom.availableHourStart >= businessOpenHour && tom.availableHourEnd <= businessClosedHour {
fmt.Println("Tom: 時間可以,我們去詹姆的餐廳吃飯吧!")
} else {
fmt.Println("Tom: 時間無法配合。。,下次再去好了。。。")
}
fmt.Println()
}

改成觀察者模式使用像 Line 群組這樣的通知功能後,
除了降低耦合關係以外,
之後有人要加入這個 Line 群組,也是十分容易的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
david := &Customer{name: "David", availableHourStart: 20, availableHourEnd: 22}
olivia := &Customer{name: "Olivia", availableHourStart: 18, availableHourEnd: 20}
tom := &Customer{name: "Tom", availableHourStart: 21, availableHourEnd: 24}
+ peter := &Customer{name: "Peter", availableHourStart: 22, availableHourEnd: 24}

// Publisher
james := &James{name: "James"}
james.addSubscriber(david)
james.addSubscriber(olivia)
james.addSubscriber(tom)
+ james.addSubscriber(peter)

// 第 1 次營業通知
james.notifySubscribers(19, 24)

觀察者設計模式優缺點

優點

  • 🎉 降低發佈者與訂閱者之間的耦合關係

缺點

  • 😬 通知的順序是隨機的

觀察者設計模式使用時機

當某一個物件的動作,會影響另一群對象的行爲,那也許就可以考慮引入觀察者設計模式

總結

以上就是觀察者設計模式的介紹,感謝你的收看~!

參考