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
| 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
| 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
| 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() { david := &Customer{name: "David", availableHourStart: 20, availableHourEnd: 22} olivia := &Customer{name: "Olivia", availableHourStart: 18, availableHourEnd: 20} tom := &Customer{name: "Tom", availableHourStart: 21, availableHourEnd: 24}
james := &James{name: "James"} james.addSubscriber(david) james.addSubscriber(olivia) james.addSubscriber(tom)
james.notifySubscribers(19, 24) james.notifySubscribers(20, 21) james.notifySubscribers(18, 20) }
✅ Line 群組通知 => James: 今天開始營業時間是: 19:00 到 24:00 喔!歡迎大家來! ___________________ David: 我們可以的時間是: 20:00 - 22:00 David: 時間可以,我們去詹姆的餐廳吃飯吧! ___________________ Olivia: 我們可以的時間是: 18:00 - 20:00 Olivia: 時間無法配合。。,下次再去好了。。。 ___________________ Tom: 我們可以的時間是: 21:00 - 24:00 Tom: 時間可以,我們去詹姆的餐廳吃飯吧!
✅ Line 群組通知 => James: 今天開始營業時間是: 20:00 到 21:00 喔!歡迎大家來! ___________________ David: 我們可以的時間是: 20:00 - 22:00 David: 時間無法配合。。,下次再去好了。。。 ___________________ Olivia: 我們可以的時間是: 18:00 - 20:00 Olivia: 時間無法配合。。,下次再去好了。。。 ___________________ Tom: 我們可以的時間是: 21:00 - 24:00 Tom: 時間無法配合。。,下次再去好了。。。
✅ Line 群組通知 => James: 今天開始營業時間是: 18:00 到 20: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"
type Publisher interface { addSubscriber(Subscriber) removeSubscriber(Subscriber) notifySubscribers(int, int) }
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() }
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() { david := &Customer{name: "David", availableHourStart: 20, availableHourEnd: 22} olivia := &Customer{name: "Olivia", availableHourStart: 18, availableHourEnd: 20} tom := &Customer{name: "Tom", availableHourStart: 21, availableHourEnd: 24}
james := &James{name: "James"} james.addSubscriber(david) james.addSubscriber(olivia) james.addSubscriber(tom)
james.notifySubscribers(19, 24) james.notifySubscribers(20, 21) 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)
|
觀察者設計模式優缺點
優點
缺點
觀察者設計模式使用時機
當某一個物件的動作,會影響另一群對象的行爲,那也許就可以考慮引入觀察者設計模式
總結
以上就是觀察者設計模式的介紹,感謝你的收看~!
參考