用脫口秀大會(huì)來(lái)講觀察者模式
大家好,我是悟空。
最近正在熱播的脫口秀大會(huì),想必大家都看過(guò)了吧,那這次我來(lái)帶著大家來(lái)看下大會(huì)上的觀察者模式吧。
一、脫口秀
首先是脫口秀的角色劃分:
我們把脫口秀演員:當(dāng)做一個(gè)被被觀察者(Observable)。
4 位領(lǐng)笑員 + 180 位觀眾,當(dāng)做觀察者(Observer)。
領(lǐng)笑員的職責(zé):當(dāng)脫口秀演員表現(xiàn)好時(shí),拍燈,表示非常好笑。
觀眾的職責(zé):當(dāng)脫口秀演員表現(xiàn)好時(shí),拿起手中的遙控器,按下按鍵表示非常喜歡。
這種場(chǎng)景就非常符合觀察者模式了,簡(jiǎn)單來(lái)說(shuō)就是一批觀察者對(duì)要觀察的對(duì)象進(jìn)行觀察,對(duì)觀察對(duì)象進(jìn)行反應(yīng)。
說(shuō)完上面的例子,想必大家對(duì)觀察者模式已經(jīng)有了初步的印象了。
那我們?cè)賮?lái)看看在程序設(shè)計(jì)的世界中,觀察者模式是怎么樣的。
二、觀察者模式
GoF 設(shè)計(jì)模式那本書(shū)中講到:在對(duì)象之間定義一個(gè)一對(duì)多的依賴,當(dāng)一個(gè)對(duì)象狀態(tài)改變的時(shí)候,所有依賴的對(duì)象都會(huì)自動(dòng)收到通知,這就是觀察者模式。
觀察者模式有很多其他稱呼,比如發(fā)布訂閱,監(jiān)聽(tīng)回調(diào)等等,其實(shí)只要場(chǎng)景符合上面的描述,都可以叫做觀察者模式。
Java API 內(nèi)置了觀察者模式,非常方便使用。用法:java.util 包內(nèi)包含最基本的 Observer 接口(觀察者接口)和 Observable 類(被觀察者父類)。另外他們之間可以用推(push)或拉(pull)的方式傳送數(shù)據(jù)。
另外很重要的一點(diǎn):被觀察者和觀察者之間的關(guān)系是一對(duì)多的。如上面的脫口秀的例子,觀眾是很多個(gè),演員一次只有一個(gè)(或一個(gè)脫口秀組合)。
三、被觀察者怎么工作的?
只需要這個(gè)類繼承 Observable 類即可。我來(lái)帶著大家看下這個(gè) Observable 類的構(gòu)成。
添加觀察者
我們首先想一下,我們想要觀察別人的時(shí)候,是不是就需要被添加成別人的觀察者,那么就需要一個(gè)添加觀察者的方法,Observale 給我們提供了一個(gè)添加成為別人的觀察者的方法:addObserver。
存放觀察者
當(dāng)有很多想要成為觀察者的時(shí)候,是不是就得有個(gè)地方專門(mén)來(lái)存這些觀察者?
Observable 給我們提供了一個(gè)存放所有觀察者的地方:一個(gè) Vector 集合。
移除觀察者
當(dāng)我們不想被某個(gè)人觀察,是不是就移除掉就可以了。
Observable 給我們提供了一個(gè)移除觀察者的方法:deleteObserver。
被觀察者如何發(fā)出通知?
當(dāng)被觀察對(duì)象,想告訴觀察者,他的狀態(tài)已經(jīng)變了,是不是就要發(fā)個(gè)通知?
Observable 給我們提供了兩個(gè)方法:
notifyObservers() 或 notifyObservers(Object arg)。
區(qū)別就是一個(gè)帶參,一個(gè)不帶參。不帶參的方式常用在觀察者通過(guò) pull 的方式來(lái)獲取數(shù)據(jù)。
如下圖所示,通過(guò) push 的方式通知觀察者。
那么通知的具體細(xì)節(jié)是怎么樣的?
說(shuō)白了,就三步:
- 被觀察對(duì)象,先判斷自己狀態(tài)是否有改變。
- 從 vector 集合中獲取所有添加的觀察者。
- 循環(huán)遍歷觀察者,調(diào)用觀察者的 update 方法。
看下源碼更清晰,注釋都加上了。
- public void notifyObservers(Object var1) {
- Object[] var2;
- synchronized(this) {
- //當(dāng)調(diào)用 setChange() 方法后,this.changed = true
- if (!this.changed) {
- return;
- }
- // 獲取所有觀察者
- var2 = this.obs.toArray();
- // 重置 change 狀態(tài)
- this.clearChanged();
- }
- // 循環(huán)遍歷通知觀察者
- for(int var3 = var2.length - 1; var3 >= 0; --var3) {
- ((Observer)var2[var3]).update(this, var1);
- }
- }
為什么要有 setChanged?
在被觀察者發(fā)送通知前,被觀察對(duì)象都會(huì)調(diào)用下 setChanged() 方法,標(biāo)記狀態(tài)已經(jīng)改變了。
- protected synchronized void clearChanged() {
- this.changed = false;
- }
那為什么需要調(diào)用下這個(gè)?不調(diào)用可以嗎?
當(dāng)被觀察對(duì)象調(diào)用 notifyObservers 方法中,會(huì)判斷狀態(tài)是否有改變,如果沒(méi)有改變,則不會(huì)通知觀察者。
這樣做的好處:可以在通知觀察者時(shí)有更多的彈性。如果不想持續(xù)不斷地通知觀察者,就可以適當(dāng)?shù)乜刂?setChanged 方法的調(diào)用。
其他:還可以用 clearChanged,重置 changed 狀態(tài),hasChanged 方法獲取 changed 狀態(tài)。
四、觀察者如何工作的?
其實(shí)很簡(jiǎn)單,觀察者實(shí)現(xiàn)了 Observer 接口就可以成為觀察者。
- public interface Observer {
- void update(Observable var1, Object var2);
- }
然后觀察者實(shí)現(xiàn)了 update 方法,就是給被觀察對(duì)象來(lái)調(diào)用的。
關(guān)于推模式和拉模式的小插曲:
如果想用推模式,調(diào)用帶參的 notifyObservers 方法把參數(shù)傳給觀察者就可以了,如果想用拉模式,就需要主動(dòng)調(diào)用被觀察者的 get 數(shù)據(jù)的方法,用帶參的或不帶參的方式通知觀察者都是可以的。
五、代碼實(shí)現(xiàn)
我們把領(lǐng)笑員定義為 Leader 類,觀眾定義成 Viewer 類,脫口秀演員定義為 Actor 類。
領(lǐng)笑員都在看演員表演脫口秀,需要成為演員的觀察者。調(diào)用 actor.addObserver(leader) 就可以了.
觀眾也是類似,調(diào)用 actor.addObserver(viewer) 就好了。
根據(jù)前面講解的原理,領(lǐng)笑員和觀眾必須繼承 observer 接口,然后實(shí)現(xiàn) update 方法。
如下所示:當(dāng)收到通知后,做出相應(yīng)反應(yīng),比如拍燈。
演員的每次的梗說(shuō)完后,都會(huì)調(diào)用 setChanged() 方法,和 notifyObservers(參數(shù)) 來(lái)通知觀察者,然后所有觀察者的 update 方法都會(huì)被觸發(fā)。
來(lái)看下演員通知的代碼:
執(zhí)行結(jié)果如下,王勉的表現(xiàn)非常精彩,領(lǐng)笑員拍燈了!
源碼下載,在公眾號(hào)后臺(tái)回復(fù):觀察者。
好了,觀察者模式還是挺有意思的。那在電商中如何應(yīng)用的呢?
六、關(guān)于設(shè)計(jì)模式
上面關(guān)于觀察者和被觀察者的工作原理有些坑,不知道大家注意到?jīng)]?
- 觀察者需要被添加到具體某個(gè)被觀察者的集合中,才能觀察,相當(dāng)于面向細(xì)節(jié)了,違背了面向抽象的原則。
- Observable 是一個(gè)類,而不是一個(gè)接口,而且 Observable 也沒(méi)有實(shí)現(xiàn)接口,這個(gè)就違背了面向接口編程。
- 必須有一個(gè)類來(lái)繼承 Observable ,如果某個(gè)類相同時(shí)擁有 Observer 類的功能,又想擁有另外一個(gè)類的功能,那么就會(huì)陷入兩難,因?yàn)?Java 不支持多重繼承,限制了 Observable 的復(fù)用潛力。
- 另外 Observer API 中的 setChanged() 方法被保護(hù)起來(lái)了(被定義成 protected 方法),那么除非繼承 Observable,否則無(wú)法創(chuàng)建 Observable 實(shí)例并組合到你自己的對(duì)象中。違反了“多用組合,少用繼承”的原則。
七、架構(gòu)設(shè)計(jì)的問(wèn)題
問(wèn)題1:上面的觀察者模式都是同步阻塞的方式,被觀察者需要等待觀察者全部執(zhí)行完后,才會(huì)執(zhí)行后續(xù)代碼。怎么通過(guò)異步的方式來(lái)通知觀察者呢?
- 方案1:?jiǎn)?dòng)一個(gè)線程來(lái)調(diào)用 notifyObservers 方法。
- 方案2:Google Guava EventBus 框架的設(shè)計(jì)思想
問(wèn)題2:跨進(jìn)程怎么通信?
- 方案1:我們看到被觀察者每次都要調(diào)用觀察者的 update 方法來(lái)通知觀察者,所以跨進(jìn)程該怎么做?我們可以同步調(diào)用 RPC 接口來(lái)實(shí)現(xiàn)。
- 方案2:消息隊(duì)列,可以有多個(gè)消費(fèi)者和生產(chǎn)者,消費(fèi)者訂閱消息,類似觀察者。但是引入了消息隊(duì)列,增加了維護(hù)成本。
問(wèn)題3:跨機(jī)器怎么通信?
- 還是引入消息隊(duì)列。
八、電商中應(yīng)用
商品庫(kù)存可以作為一個(gè)被觀察者,商品入庫(kù)單作為觀察者,當(dāng)商品庫(kù)存變了后,需要生成一個(gè)商品入庫(kù)單,就可以用觀察者模式,商品入庫(kù)單和商品庫(kù)存進(jìn)行解耦,如果后續(xù)還要生成其他類型的入庫(kù)單再加上發(fā)送一條消息給管理員,直接添加觀察者就可以了。
九、后記
本篇通過(guò)脫口秀大會(huì)來(lái)講解觀察者模式,涉及到了三種角色,領(lǐng)笑員,觀眾,脫口秀演員。
然后詳細(xì)講解了觀察者和被觀察者的工作原理,另外探討了這種模式有哪些設(shè)計(jì)模式相關(guān)的問(wèn)題。
然后從架構(gòu)設(shè)計(jì)的角度來(lái)分析了觀察者模式引入的問(wèn)題:同步調(diào)用,跨進(jìn)程通信,跨機(jī)器通信。
最后簡(jiǎn)單講了下電商中的應(yīng)用場(chǎng)景,拋轉(zhuǎn)引玉,希望大家留言探討。
本文轉(zhuǎn)載自微信公眾號(hào)「悟空聊架構(gòu)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系悟空聊架構(gòu)公眾號(hào)。