深入理解Java設(shè)計(jì)模式之觀察者模式
一、什么是觀察者模式
在許多設(shè)計(jì)中,經(jīng)常涉及多個對象都對一個特殊對象中的數(shù)據(jù)變化感興趣,而且這多個對象都希望跟蹤那個特殊對象中的數(shù)據(jù)變化,也就是說當(dāng)對象間存在一對多關(guān)系時,在這樣的情況下就可以使用觀察者模式。當(dāng)一個對象被修改時,則會自動通知它的依賴對象。
觀察者模式是關(guān)于多個對象想知道一個對象中數(shù)據(jù)變化情況的一種成熟的模式。觀察者模式中有一個稱作“主題”的對象和若干個稱作“觀察者”的對象,“主題”和“觀察者”間是一種一對多的依賴關(guān)系,當(dāng)“主題”的狀態(tài)發(fā)生變化時,所有“觀察者”都得到通知。
主要解決:一個對象狀態(tài)改變給其他對象通知的問題,而且要考慮到易用和低耦合,保證高度的協(xié)作。
二、觀察者模式的結(jié)構(gòu)
觀察者模式的結(jié)構(gòu)中包含四種角色:
(1)主題(Subject
):主題是一個接口,該接口規(guī)定了具體主題需要實(shí)現(xiàn)的方法,比如,添加、刪除觀察者以及通知觀察者更新數(shù)據(jù)的方法。
(2)觀察者(Observer
):觀察者是一個接口,該接口規(guī)定了具體觀察者用來更新數(shù)據(jù)的方法。
(3)具體主題(ConcreteSubject
):具體主題是實(shí)現(xiàn)主題接口類的一個實(shí)例,該實(shí)例包含有可以經(jīng)常發(fā)生變化的數(shù)據(jù)。具體主題需使用一個集合,比如ArrayList,存放觀察者的引用,以便數(shù)據(jù)變化時通知具體觀察者。
(4)具體觀察者(ConcreteObserver
):具體觀察者是實(shí)現(xiàn)觀察者接口類的一個實(shí)例。具體觀察者包含有可以存放具體主題引用的主題接口變量,以便具體觀察者讓具體主題將自己的引用添加到具體主題的集合中,使自己成為它的觀察者,或讓這個具體主題將自己從具體主題的集合中刪除,使自己不再是它的觀察者。
三、觀察者模式的使用場景
(1)當(dāng)一個對象的數(shù)據(jù)更新時需要通知其他對象,但這個對象又不希望和被通知的那些對象形成緊耦合。
(2)當(dāng)一個對象的數(shù)據(jù)更新時,這個對象需要讓其他對象也各自更新自己的數(shù)據(jù),但這個對象不知道具體有多少對象需要更新數(shù)據(jù)。
觀察者模式在實(shí)際項(xiàng)目的應(yīng)用中非常常見,比如你到 ATM 機(jī)器上取錢,多次輸錯密碼,卡就會被 ATM吞掉,吞卡動作發(fā)生的時候,會觸發(fā)哪些事件呢?第一攝像頭連續(xù)快拍,第二,通知監(jiān)控系統(tǒng),吞卡發(fā)生;第三,初始化 ATM 機(jī)屏幕,返回最初狀態(tài),你不能因?yàn)榫屯塘艘粡埧ǎ麄€ ATM 都不能用了吧,一般前兩個動作都是通過觀察者模式來完成的。觀察者可以實(shí)現(xiàn)消息的廣播,一個消息可以觸發(fā)多個事件,這是觀察者模式非常重要的功能。
使用觀察者模式也有兩個重點(diǎn)問題要解決:
廣播鏈的問題。
如果你做過數(shù)據(jù)庫的觸發(fā)器,你就應(yīng)該知道有一個觸發(fā)器鏈的問題,比如表 A 上寫了一個觸發(fā)器,內(nèi)容是一個字段更新后更新表 B 的一條數(shù)據(jù),而表 B 上也有個觸發(fā)器,要更新表 C,表 C 也有觸發(fā)器…,完蛋了,這個數(shù)據(jù)庫基本上就毀掉了!我們的觀察者模式也是一樣的問題,一個觀察者可以有雙重身份,即使觀察者,也是被觀察者,這沒什么問題呀,但是鏈一旦建立,這個邏輯就比較復(fù)雜,可維護(hù)性非常差,根據(jù)經(jīng)驗(yàn)建議,在一個觀察者模式中最多出現(xiàn)一個對象既是觀察者也是被觀察者,也就是說消息最多轉(zhuǎn)發(fā)一次(傳遞兩次),這還是比較好控制的;
異步處理問題。
被觀察者發(fā)生動作了,觀察者要做出回應(yīng),如果觀察者比較多,而且處理時間比較長怎么辦?那就用異步唄,異步處理就要考慮線程安全和隊(duì)列的問題,這個大家有時間看看 Message Queue,就會有更深的了解。
四、觀察者模式的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
1、具體主題和具體觀察者是松耦合關(guān)系。由于主題接口僅僅依賴于觀察者接口,因此具體主題只是知道它的觀察者是實(shí)現(xiàn)觀察者接口的某個類的實(shí)例,但不需要知道具體是哪個類。同樣,由于觀察者僅僅依賴于主題接口,因此具體觀察者只是知道它依賴的主題是實(shí)現(xiàn)主題接口的某個類的實(shí)例,但不需要知道具體是哪個類。
2、觀察者模式滿足“開-閉原則”。主題接口僅僅依賴于觀察者接口,這樣,就可以讓創(chuàng)建具體主題的類也僅僅是依賴于觀察者接口,因此,如果增加新的實(shí)現(xiàn)觀察者接口的類,不必修改創(chuàng)建具體主題的類的代碼。。同樣,創(chuàng)建具體觀察者的類僅僅依賴于主題接口,如果增加新的實(shí)現(xiàn)主題接口的類,也不必修改創(chuàng)建具體觀察者類的代碼。
缺點(diǎn):
1、如果一個被觀察者對象有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費(fèi)很多時間。
2、如果在觀察者和觀察目標(biāo)之間有循環(huán)依賴的話,觀察目標(biāo)會觸發(fā)它們之間進(jìn)行循環(huán)調(diào)用,可能導(dǎo)致系統(tǒng)崩潰。
3、觀察者模式?jīng)]有相應(yīng)的機(jī)制讓觀察者知道所觀察的目標(biāo)對象是怎么發(fā)生變化的,而僅僅只是知道觀察目標(biāo)發(fā)生了變化。
五、觀察者模式的實(shí)現(xiàn)
Observer類---抽象觀察者,為所有具體觀察者定義一個接口,在得到主題通知時更新自己。
這個接口叫做更新接口,抽象觀察者一般用一個抽象類或者一個接口實(shí)現(xiàn)。更新接口通常包括一個Update方法,這個方法叫做更新方法。
abstract class Observer { public abstract void Update(); }
Subject類---主題或者抽象通知者,一般用一個抽象類或者一個接口實(shí)現(xiàn)。
它把所有對觀察者對象的引用保存到一個聚集里,每個主題都可以有任何數(shù)量的觀察者。抽象主題提供一個接口,可以增加和刪除觀察者。
abstract class Subject { private List<Observer> observers = new List<Observer>(); //增加觀察者 public void Attach(Observer observer) { observers.Add(observer); } //移除觀察者 public void Detach(Observer observer) { observers.Remove(observer); } //通知 public void Notify() { foreach (var item in observers) { item.Update(); } } }
ConcreteSubject類---具體主題或者具體通知者,將有關(guān)狀態(tài)存入具體觀察者對象;在具體主題的內(nèi)部狀態(tài)改變時,給所有登記過的觀察者發(fā)送通知。
具體主題角色通常用一個具體類實(shí)現(xiàn)。
class ConcreteSubject : Subject { private string subjectState; //具體被觀察者狀態(tài) public string SubjectState { get { return subjectState; } set { subjectState = value; } } }
ConcreteObserver類---具體觀察者,實(shí)現(xiàn)抽象觀察者角色所要求的更新接口,以便使本身的狀態(tài)與主題的狀態(tài)相協(xié)調(diào)。
具體觀察者角色可以保存一個指向具體主題對象的引用。具體觀察者角色通常用一個具體類實(shí)現(xiàn)。
class ConcreteObserver : Observer { private string name; private string observerState; private ConcreteSubject subject; public ConcreteObserver(ConcreteSubject subject, string name) { this.subject = subject; this.name = name; } public override void Update() { observerState = subject.SubjectState; Console.WriteLine("觀察者{0}的新狀態(tài)是{1}", name, observerState); } public ConcreteSubject Subject { get { return subject; } set { subject = value; } } }
客戶端代碼
static void Main(string[] args) { ConcreteSubject cs = new ConcreteSubject(); cs.Attach(new ConcreteObserver(cs, "X")); cs.Attach(new ConcreteObserver(cs, "Y")); cs.Attach(new ConcreteObserver(cs, "Z")); cs.SubjectState = "ABC"; cs.Notify(); Console.Read(); }
結(jié)果
觀察者X的新狀態(tài)是ABC
觀察者Y的新狀態(tài)是ABC
觀察者Z的新狀態(tài)是ABC
六、觀察者模式和委托的結(jié)合
上述代碼盡管已經(jīng)用了依賴倒轉(zhuǎn)原則,但是“抽象通知者”還是依賴“抽象觀察者”,也就是說,萬一沒有了抽象觀察者這樣的接口,這個通知功能就發(fā)送不了。
另外就是每個具體觀察者,它不一定是Update的方法調(diào)用。
目的:通知者和觀察者之間根本就互相不知道,由客戶端來決定通知誰
//通知者接口 interface Subject { void Notify(); string SubjectState { get; set; } }
具體觀察者類
//看股票的同事 class StockObserver { private string name; private Subject sub; public StockObserver(string name, Subject sub) { this.sub = sub; this.name = name; } //關(guān)閉股票 public void CloseStock() { Console.WriteLine("{0}{1}關(guān)閉股票,繼續(xù)工作", sub.SubjectState, sub); } } //看NBA的同事 class NBAObserver { private string name; private Subject sub; public NBAObserver(string name, Subject sub) { this.sub = sub; this.name = name; } //關(guān)閉NBA public void CloseNBA() { Console.WriteLine("{0}{1}關(guān)閉NBA,繼續(xù)工作", sub.SubjectState, sub); } }
聲明一個委托,無參數(shù),無返回值
//聲明一個委托,無參數(shù),無返回值 delegate void EventHandler();
主題或者抽象通知者
//老板類 class Boss : Subject { private string action; //聲明委托事件Update public event EventHandler Update; public string SubjectState { get { return action; } set { action = value; } } public void Notify() { //在訪問通知時,調(diào)用Update Update(); } } //秘書類 class Secretary : Subject { //與老板類類似,省略...... }
客戶端代碼
static void Main(string[] args) { //老板張 Boss Zhang = new Boss(); StockObserver tongshi1 = new StockObserver("張三",Zhang); NBAObserver tongshi2 = new NBAObserver("李四",Zhang); Zhang.Update += new EventHandler(tongshi1.CloseStock); Zhang.Update += new EventHandler(tongshi2.CloseNBA); Zhang.SubjectState = "老板張駕到!"; Zhang.Notify(); Console.Read(); }
結(jié)果
老板張駕到!張三關(guān)閉股票,繼續(xù)工作 老板張駕到!李四關(guān)閉NBA,繼續(xù)工作
七、總結(jié)
實(shí)現(xiàn)觀察者模式的時候要注意,觀察者和被觀察對象之間的互動關(guān)系不能體現(xiàn)成類之間的直接調(diào)用,否則就將使觀察者和被觀察對象之間緊密的耦合起來,從根本上違反面向?qū)ο蟮脑O(shè)計(jì)的原則。無論是觀察者“觀察”觀察對象,還是被觀察者將自己的改變“通知”觀察者,都不應(yīng)該直接調(diào)用。
另外redis里的pub/sub也可以實(shí)現(xiàn)觀察者模式。
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
dockerfile-maven-plugin極簡教程(推薦)
這篇文章主要介紹了dockerfile-maven-plugin極簡教程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10Java中new Date().getTime()指定時區(qū)的時間戳問題小結(jié)
本文主要介紹了Java中new Date().getTime()時間戳問題小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07SSH框架網(wǎng)上商城項(xiàng)目第25戰(zhàn)之使用java email給用戶發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了SSH框架網(wǎng)上商城項(xiàng)目第25戰(zhàn)之使用java email給用戶發(fā)送郵件,感興趣的小伙伴們可以參考一下2016-06-06MVC+DAO設(shè)計(jì)模式下的設(shè)計(jì)流程詳解
這篇文章主要介紹了MVC+DAO設(shè)計(jì)模式下的設(shè)計(jì)流程詳解,分別介紹了數(shù)據(jù)庫設(shè)計(jì)、設(shè)計(jì)符合java bean標(biāo)準(zhǔn)的entity類、設(shè)計(jì)訪問數(shù)據(jù)庫的DAO接口等內(nèi)容,具有一定參考價值,需要的朋友可以了解下。2017-11-11JavaFX桌面應(yīng)用未響應(yīng)問題解決方案
這篇文章主要介紹了JavaFX桌面應(yīng)用未響應(yīng)問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07logback高效狀態(tài)管理器StatusManager源碼解析
這篇文章主要為大家介紹了logback高效狀態(tài)管理器StatusManager源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11java?random隨機(jī)數(shù)的用法及常見應(yīng)用場景
這篇文章主要給大家介紹了關(guān)于java?random隨機(jī)數(shù)的用法及常見應(yīng)用場景的相關(guān)資料,Java中的Random類是用來生成偽隨機(jī)數(shù)的工具類,它可以用來生成隨機(jī)的整數(shù)、浮點(diǎn)數(shù)和布爾值,需要的朋友可以參考下2023-11-11