1. Brief
一直對Observer Pattern和Pub/Sub Pattern有所混淆,下面打算通過這兩篇Blog來梳理這兩種模式。若有紕漏請大家指正。
2. Use Case
首先我們來面對一個老到跌渣的故事,并以從未聽說過Observer Pattern為前提。
假設要設計一個新聞訂閱系統,新聞分為商業、體育和八卦3種,而查收終端有PC、移動終端等,后續還不斷增加新聞種類和查收終端。
需求如上,下面我們根據OOD的方式來構建概念模型。
新聞 <- 分類新聞
終端 <- 分類終端
然后構造實體模型
// 新聞相關實體模型 class NewsType{ constructor(){} } class BusinessNewsType extends NewsType{} class SportNewsType extends NewsType{} class EntertaintmentNewsType extends NewsType{} // 終端相關實體模型 class Term{ getNews(news){} } class PCTerm extend Term{} class MobileTerm extend Term{}
接著我們關聯已有經驗——現實生活中的送報服務,發現用戶A、用戶B均訂閱了A報的X早報,那么每天早上報紙剛印刷出來就會馬上送到用戶A和B那了。突然多了家訂報有米送的Y早報,用戶A和用戶C跑去訂閱,用戶B直接就退了X早報,這時X早報為留住老用戶就推送“老客戶免費贈送半年X晚報”,于是用戶A取消了退訂的想法。
很明顯 新聞訂閱系統 就是線下業務直挪到線上的做法,通過分析線下業務流程我們可以找到設計方案。線下業務流程如下:
訂閱:用戶到報社訂閱
退訂:用戶到報社退訂
分發報紙:報社向所有訂閱者分發報紙
按這思路構建 新聞訂閱系統 的原型:
class NewsType{ constructor(){ this.subs = [] } /* 訂閱 * @param {Term} term - 終端 * @returns {Boolean} */ sub(term){ // 排除 for(let _sub of this.subs) if(_sub === term) return false return Boolean(this.subs.push(term)) } /* 退訂 * @param {Term} term - 終端 * @return {Boolean} */ unsub(term){ for(let i = 0, sub; sub = subs[i]; ++i) if(sub === term) return Boolean(this.subs.splice(i, 1)) } /* * 分發新聞 */ notify(news){ for(let sub of this.subs) sub.getNews(news) } } class BusinessNewsType extends NewsType{ pubNews(title, content){ var news = {title: title, content: content} super.notify(news) } } // definition of SportNewsType....... class PCTerm extends Term{ getNews(news){ alert(news.title + ';' + news.content) } } class MobileTerm extends Term{ getNews(news){ console.log(news.title + ';' + news.content) } } // 主程序則作為 新聞中心 與 用戶交互的場所 var businessNewsType = new BusinessNewsType() var sportNewsType = new SportNewsType() var pcTerm = new PCTerm() var mobileTerm = new MobileTerm() businessNewsType.sub(pcTerm) businessNewsType.sub(mobileTerm) sportNewsType.sub(mobileTerm)
上述原型基本勾勒出新聞訂閱系統中對象及其關聯的方式,我們就可以在這之上再細化和優化了。而從上述是原型我們不難發現 新聞 與 終端 均可獨立開發,然后在主程序中做關聯即可。新聞類型 和 終端類型的增刪并不會對其他已有的新聞類型和終端類型有影響,除了在主程序中增刪關聯外。
現在我們作個簡單的分析總結:
1. 不穩定因素(新聞類型 和 終端類型)解耦 -> 最小化不穩定因素所影響的范圍(范圍越小,后期改動越少);
2. 關聯規則接口/契約化 -> 固化關聯規則 和 關聯發生的形式 便于后期維護。
這些是我面對未知問題的分析、解構方法,希望和大家一起探討更美好的方法。
3. What Is Observer Pattern?
Observer Pattern(觀察者模式),狹義上是指Observer/Subscriber關注Observable/Subject的狀態,并根據Observable/Subject的狀態作出響應。廣義上是指Observer/Subscriber關注Observable/Subject的狀態或行為或兩者兼備,并作出響應。
Roles
Observable/Subject(被觀察者):定義被觀察者的公共狀態和行為
ConcreteObservable(具體的被觀察者):定義具體的被觀察者的狀態和行為
Observer(觀察者):定義觀察者的公共狀態和行為
ConcreteObserver(具體的觀察者):定義具體的觀察者的狀態和行為
Two Methods: Push & Pull
上面第2節中的實現是由Observable/Subject來維護Observer組,那是不是只能這樣呢?答案是否定的。它只是Push方式的實現,我們還可以采用Pull方式呢!
Push Model:推方式,也就是由Observable/Subject主動發起與Observer/Subscriber通信,并將自身的所有信息推給Observer/Subscriber,即使大部分信息最后都沒用上。
pros: 1. 觀察者實時響應被觀察者的狀態變化和行為狀況;
cons: 1. 觀察者被硬塞一些被觀察者的無效信息;2. 被觀察者狀態變化頻密,導致觀察者忙于響應,消耗資源。
Pull Model:拉方式,也就是由主動Observer/Subscriber發起與Observable/Subject通信,并根據自身需要從Observable/Subject上拉去有效信息。一般通過定時器或特定事件觸發執行。
pros: 1. 觀察者可按需從被觀察者處提取有效信息;2. 自主控制通信節奏,以免被狀態頻密變化的被觀察者牽著鼻子走;
cons: 1. 獲取被觀察者狀態變化上存在滯后甚至丟失的情況。
下面是Pull Model的實現方式
// Pull Model implementation
class FooNewsType{ constructor(){ this.news = [] } addNews(title, content){ this.news.push({title: title, content: content, timestamp:(+new Date())}) } } class PCTerm{ constructor(){ this.subjects = [] this.newsTiles = [] this.lastPullDate = 0 } subTo(newsType){ this.subjects.push(newType) } unsubFrom(newsType){ for(let i = 0, n; n = this.subjects[i]; ++i) if(n === newsType) return this.subjects.splice(i, 1) } // 拉數據 pull(){ for(let sub of this.subjects) for(let news of sub.news) if(news.timestamp > this.lastPullDate) this.newsTiles .push(news.title) } } // 主程序 var businessNewsType = new BusinessNewsType() var pcTerm = new PCTerm() pcTerm.subTo(businessNewsType) businessNewsType .addNews('Say Hi', 'Hello World!') // 其他代碼........ pcTerm.pull()
Improvement of Push Model
針對Push Model所帶來的問題1,我們可以通過增強sub函數來解決
// definition sub(term, aspect){ this.subs.push({term: term, aspect: aspect}) } notify(news){ for(let sub of this.subs) sub.term.getNews(sub.aspect && sub.aspect(news) || news) } // usage new BusinessNewsType() .sub(new PCTerm() , (news)=>{ return news.title })
針對問題2,我們可以通過 定時推送通知 + 溢出通知 的方式解決,不過具體還是看業務需求咯
constructor(interval = 100, ceiling = 5){ this.ceiling = ceiling this.timer = setInterval(()=>{ if (!this.pools.length || !this.subs.length) return for(let sub of this.subs) for(let n of news) sub.term.getNews(sub.aspect && sub.aspect(n) || n) }, interval) } notify(news){ this.pools.push(news) if (this.pools.length < this.ceiling) return var news = this.pools.splice(0, this.pools.length) for(let sub of this.subs) for(let n of news) sub.term.getNews(sub.aspect && sub.aspect(n) || n) }
Specific Implementation Problems —— Making sure Subject state is self-consistent before notification
就是確保Subject狀態變化完成后,再通知Subscriber。反例如下:
notify(news){ for(let sub of this.subs) sub.term.getNews(sub.aspect && sub.aspect(news) || news) // 發生在通知觀察者之后 news.title = 'changed' }
相當于為每次Subject狀態的整體變化打個版本號,然后將屬于該版本的Subject狀態發送給Subscriber,之后的狀態變化就屬于下一個版本了。
4. Diff Between Observer Pattern and Pub/Sub Pattern
兩者區別主要體現在以下2點
1. 耦合度
Observer Pattern: Subscriber 和 Subject 兩者感知對方的存在,但不受對方的具體實現 和 數目 所限制 => 弱依賴。關聯規則內置在Subscriber 或 Subject中。
Pub/Sub Pattern: Publisher 和 Subscriber 兩者相互間毫無存在感,通過Message Broker關聯兩種角色,并且將關聯規則藏進Message Broker中。
2. 影響范圍
Observer Pattern作為Design Pattern存在,而Pub/Sub Pattern則作為Architecture Pattern存在,明顯Observer Pattern的影響范圍較小。也就是說在采用Pub/Sub Pattern時,需要更謹慎。
5. We Used Observer Pattern Already
其實我們現在用到很多框架、類庫均采用了Observer Pattern,如MVC和Event Mechanism等。
MVC中M(odel)作為觀察者,而V(iew)作為被觀察者;
而Event Mechanism則是更為典型的Observer Pattern,C#在語法層面(event關鍵字),而Java通過內置類庫對其提供支持。
6. Conclusion
洋洋灑灑寫了這么多,若有紕漏請大家指正,謝謝!
尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4627487.html ^_^肥子John
7. Thanks
http://wiki.jikexueyuan.com/project/javascript-design-patterns/observer-pattern.html
http://www.joezimjs.com/javascript/javascript-design-patterns-observer/
http://www.oodesign.com/observer-pattern.html
文章列表