解耦關聯物件——觀察者模式詳解
1. 觀察者模式簡介
在軟體開發中,觀察者模式是使用頻率最高的設計模式之一,如果你做過web開發,對它應該更不會陌生,因為典型的MVC架構就是對觀察者模式的一種延伸。在軟體開發中經常會碰到這種困境:系統由若干個相互協作的類構成,類之間常有一對多的依賴關係,當被依賴物件的狀態變化時,其他所有依賴物件都要發生改變。以MVC為例,模型(Model)物件封裝了資料,檢視(View)物件對資料進行渲染和進行圖形表示。當模型中的資料改變時,檢視應該馬上得到反饋從而改變其顯示的內容。我們需要維護這種具有依賴關係的物件之間的一致性,又不希望為了維護這種一致性導致類之間緊密耦合。而觀察者模式正式對這一困境的回答。觀察者模式的的最大好處是可以實現具有關聯關係的物件之間的解耦,使得雙方可以獨立的進行擴充套件和變化,使得系統具有更好的彈性。
2. 觀察者模式詳解
2.1 觀察者模式定義
觀察者模式定義了物件之間的一對多依賴關係,每當物件改變狀態,所有依賴於它的物件都會得到通知並被自動更新。
2.2觀察者模式的結構
觀察者模式的結構相對簡單,可以用 <Head First設計模式>
中的一張圖來描述
觀察者模式的主要角色:
-
Subject(主題)/Observable(被觀察者)
通常以抽象類或者介面的形式存在,定義了被觀察者即主題必須實現的職責:1.必須能動態的註冊和移除觀察者 2.在主題狀態改變時能通知觀察者進行更新。
-
Observer(觀察者)
定義了觀察者的主要職責:在主題狀態改變時需要進行更新,具體的更新邏輯由具體觀察者自行實現。
-
ConcreteSubject(具體的主題)/ConcreteObservable(具體的被觀察者)
根據業務實際實現抽象主題中定義的介面,並對特定的觀察者進行通知。
-
ConcreteObserver(具體的觀察者)
根據業務實際實現自己的更新邏輯,在主題狀態改變時進行更新。
2.3 觀察者模式的簡單實現
觀察者模式又被稱為釋出訂閱模式,以客戶訂閱報紙為例,客戶相當於觀察者,而報社則是被觀察者。客戶可以向報社訂閱報紙,也可以取消訂閱。當報社有新報紙出版時,就會將報紙傳送給訂閱的客戶。
-
主題抽象
使用抽象類定義並實現了主題具有的基本職責:新增/移除觀察者,在主題狀態變化時通知所有註冊的觀察者。
/** * @author: takumiCX * @create: 2018-10-29 **/ public abstract class Subject { //觀察者集合 private CopyOnWriteArrayList<Observer> observers=new CopyOnWriteArrayList<>(); //註冊觀察者 protectedvoid registerObserver(Observer observer){ observers.add(observer); } //移除觀察者 protected boolean removeObserver(Observer observer){ return observers.remove(observer); } /** * 通知觀察者 * @param msg 傳送給觀察者的訊息 */ protected void notifyObservers(String msg){ for(Observer observer:observers){ observer.update(msg); } } /** * 通知觀察者 */ protected void notifyObservers(){ for(Observer observer:observers){ observer.update(); } } }
該抽象類內部維護了一個執行緒安全的CopyOnWriteArrayList來儲存觀察者集合,並沒有使用Vector或者SynchronizedList等常見同步容器,在需要頻繁增刪觀察者的情況下可以一定程度提升效能。
- 具體的主題實現——報社
/** * @author: takumiCX * @create: 2018-10-29 **/ public class NewsPaperSubject extends Subject { //報紙的期號 private String date; public String getDate() { return date; } public void setDate(String date) { this.date = date; } /** * 通知訂閱的客戶接收新一期的報紙 */ public void postNewPublication(){ notifyObservers(date); } }
具體的主題實現類繼承了主題抽象類,並添加了一個狀態變數,表示報紙的期號,在通知訂閱的客戶時需要將該資訊也一起傳過去。 postNewPublication()
方法當有新一期的報紙發行時,會通過呼叫該方法對訂閱的客戶進行通知。
- 觀察者抽象
/** * @author: takumiCX * @create: 2018-10-29 **/ public interface Observer { void update(String msg); void update(); }
- 具體的觀察者實現——客戶
/** * @author: takumiCX * @create: 2018-10-29 **/ public class CustomerObserver implements Observer { //客戶姓名 private String name; public CustomerObserver(String name) { this.name = name; } @Override public void update(String msg) { System.out.println(name+" 您好!"+msg+" 期的報紙已傳送,請注意接收!"); } @Override public void update() { } }
-
uml類結構
- 測試程式碼
/** * @author: takumiCX * @create: 2018-10-29 **/ public class Test { public static void main(String[] args) { //報社(主題) NewsPaperSubject newsPaperSubject = new NewsPaperSubject(); //客戶1(觀察者) CustomerObserver observer1 = new CustomerObserver("趙雲"); //客戶2(觀察者) CustomerObserver observer2 = new CustomerObserver("馬超"); CustomerObserver observer3 = new CustomerObserver("張飛"); //向主題註冊觀察者 newsPaperSubject.registerObserver(observer1); newsPaperSubject.registerObserver(observer2); newsPaperSubject.registerObserver(observer3); //報紙的期號 String date="2018-10-29"; newsPaperSubject.setDate(date); //通知所有訂閱的客戶接收報紙 newsPaperSubject.postNewPublication(); } }
-
測試結果
2.4 使用JDK內建的觀察者實現
JDK內建了對觀察者模式的支援,只要繼承或者實現相應的抽象類或介面
- 主題實現類
/** * @author: takumiCX * @create: 2018-10-29 **/ public class JDKNewsPaperObservable extends Observable { //報紙的期號 private String date; public String getDate() { return date; } public void setDate(String date) { this.date = date; } public void postNewPublication(){ //將狀態改變的標誌位置位true setChanged(); //通知所有觀察者 notifyObservers(date); } }
JKD中對主題通過抽象類Observable進行了抽象,實現自定義主題只要繼承該抽象類即可。注意該抽象類內部有一個標註主題狀態是否改變的標誌位,預設為false
private boolean changed = false;
在通知觀察者前必須先通過呼叫setChanged()方法將該標誌位置為true。在通知觀察者進行更新的方法被呼叫後,該標誌位會被重新置為false。
- 具體的觀察者物件
/** * @author: takumiCX * @create: 2018-10-29 **/ public class JDKCustomerObserver implements Observer { //客戶姓名 private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public JDKCustomerObserver(String name) { this.name = name; } @Override public void update(Observable o, Object arg) { System.out.println(name+" 您好!"+arg+" 期的報紙已傳送,請注意接收!"); } }
-
uml類結構
- 測試程式碼
/** * @author: takumiCX * @create: 2018-10-29 **/ public class Test2 { public static void main(String[] args) { //報社(主題) JDKNewsPaperObservable jdkNewsPaperObservable = new JDKNewsPaperObservable(); //客戶1(觀察者) JDKCustomerObserver observer1 = new JDKCustomerObserver("趙雲"); //客戶2(觀察者) JDKCustomerObserver observer2 = new JDKCustomerObserver("馬超"); JDKCustomerObserver observer3 = new JDKCustomerObserver("張飛"); //向主題註冊觀察者 jdkNewsPaperObservable.addObserver(observer1); jdkNewsPaperObservable.addObserver(observer2); jdkNewsPaperObservable.addObserver(observer3); //報紙的期號 String date="2018-10-29"; jdkNewsPaperObservable.setDate(date); //通知所有訂閱的客戶接收報紙 jdkNewsPaperObservable.postNewPublication(); } }
-
執行結果
3 使用觀察者模式需要注意的地方
1.多個觀察者預設是被順序呼叫而執行的,當一個觀察者的業務邏輯執行卡頓,或者執行時間過長,會導致後續觀察者的業務邏輯執行被延遲,也會影響整體的執行效率。
解決辦法:採用非同步的方式進行處理,比如將觀察者的業務邏輯放到執行緒池中去執行。
2.當多個物件既是觀察者又是被觀察者將導致系統難以除錯和維護。
解決辦法:不允許觀察者模式中存在既是觀察者又是被觀察者的物件。
4. 總結
當存在相互關聯的物件,即某些物件狀態的改變會導致其他物件產生相應的變化。使用觀察者模式可以方便的維護關聯物件間行為的一致性,同時使其保持鬆耦合狀態,這樣雙方就可以相對獨立的進行擴充套件和變化,使得系統更具彈性。