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<>();
//注冊觀察者
protected void 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. 總結
當存在相互關聯的對象,即某些對象狀態的改變會導致其他對象產生相應的變化。使用觀察者模式可以方便的維護關聯對象間行為的一致性,同時使其保持松耦合狀態,這樣雙方就可以相對獨立的進行擴展和變化,使得系統更具彈性。