前言
觀察者模式是一個使用率非常高的模式,常用與GUI系統、訂閱——發布系統。觀察者模式一個重要作用就是解耦,將被觀察者和觀察者解耦,使得他們之間的依賴性更小,甚至做到毫無依賴。
觀察者模式定義
定義對象間一種一對多的依賴關系,使得每當一個對象改變狀態,則所有依賴它的對象都會得到通知并被自動更新。
觀察者模式的UML類圖
觀察者模式的UML類圖.png
- Subject: 抽象主題,也就是被觀察的角色,抽象主題角色把所有觀察者對象的引用保存在一個集合里,每個主題都可以有任意數量的觀察者,抽象主題提供一個接口,可以增加和刪除觀察者對象。
- ConcreteSubject: 具體主題,該角色將有關狀態存入具體觀察者對象,在具體主題的內部狀態發生改變時,給所有注冊過的觀察者發出通知,具體主題角色又叫具體被觀察者(ConcreteObserver)角色。
- Observer: 抽象觀察者,該角色是觀察者的抽象類,它定義了一個更新接口,當收到主題的更改通知時更新自己。
- ConcreteObserver: 具體觀察者,該角色實現抽象觀察者角色所定義的更新接口,當收到主題變化通知時更新自己。
觀察者模式的簡單示例
以微信公眾號為例,相信每個人都會關注一些自己感興趣的公眾號,比如我關注了InfoQ,這其實就是一種訂閱——發布模式,當InfoQ 發布新文章時,所有關注它的人都能收到通知。
/**
* 抽象主題
*/
public abstract class Subject {
//保存觀察者的集合
protected List<Observer> observers = new ArrayList<>();
/**
* 注冊觀察者
*
* @param observer 觀察者
*/
public void registerObservers(Observer observer) {
}
/**
* 刪除觀察者
*
* @param observer 觀察者
*/
public void unRegisterObservers(Observer observer) {
}
/**
* 通知所有觀察者
*/
@Override
public void notifyObservers(String content) {
for (Observer observer : observers) {
observer.update(content);
}
}
}
/**
* 具體主題
*/
public class InfoQ extends Subject {
@Override
public void registerObservers(Observer observer) {
observers.add(observer);
}
@Override
public void unRegisterObservers(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String content) {
for (Observer observer : observers) {
observer.update(content);
}
}
}
/**
* 抽象觀察者
*/
public interface Observer {
/**
* 用于更新自身的方法
* @param content 更新的內容
*/
void update(String content);
}
/**
* 具體觀察者
*/
public class Coder implements Observer {
private String mName;
public Coder(String name) {
mName = name;
}
@Override
public void update(String content) {
System.out.println("Hi: " + mName + " " + content);
}
}
/**
* 客戶端調用
*/
public class Client {
public static void main(String[] args) {
InfoQ infoQ = new InfoQ();
Coder coder1 = new Coder("linda");
Coder coder2 = new Coder("mary");
Coder coder3 = new Coder("andy");
infoQ.registerObservers(coder1);
infoQ.registerObservers(coder2);
infoQ.registerObservers(coder3);
infoQ.notifyObservers("InfoQ發布新內容了,快來看看吧。");
}
}
輸出日志如下:
Hi: linda InfoQ發布新內容了,快來看看吧。
Hi: mary InfoQ發布新內容了,快來看看吧。
Hi: andy InfoQ發布新內容了,快來看看吧。
以上示例中,Subject對應抽象主題的角色,InfoQ對應具體主題角色,Observer即抽象觀察者角色,Code即具體觀察者的角色,當InfoQ狀態改變時,會遍歷所有的Coder并通知Coder調用update()更新自身狀態,完成了一對多的通知功能。在整個過程中InfoQ和Coder完全沒有耦合,而是依賴Subject、Observer這些抽象類。保證了系統的靈活性,擴展性。
推模型和拉模型
- 推模型
主題對象向觀察者推送主題的詳細信息,不管觀察者是否需要,推送的信息通常是主題對象的全部或部分數據。 - 拉模型
被觀察者通過把自身的引用傳遞給觀察者,需要觀察者自行通過該引用來獲取相關的信息。
如果想換成拉模型,只需要把主題對象自身通過update(Subject subject)方法傳遞給觀察者,這樣在觀察者就可以根據需要獲取數據了。如下:
//具體主題,即被觀察者
public class InfoQ extends Subject {
@Override
public void registerObservers(Observer observer) {
observers.add(observer);
}
@Override
public void unRegisterObservers(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(this);
}
}
public String getContent() {
return "InfoQ發布新內容了,快來看看吧。";
}
}
/**
* 具體觀察者
*/
public class Coder implements Observer {
private String mName;
public Coder(String name) {
mName = name;
}
@Override
public void update(Subject subject) {
String content = ((InfoQ) subject).getContent();
System.out.println("Hi: " + mName + " " + content);
}
}
兩種模式的比較
- 推模型是假定主題對象知道觀察者需要的數據;而拉模型是主題對象不知道觀察者具體需要什么數據,沒有辦法的情況下,干脆把自身傳遞給觀察者,讓觀察者自己去按需要取值。
- 推模型可能會使得觀察者對象難以復用,因為觀察者的update()方法是按需要定義的參數,可能無法兼顧沒有考慮到的使用情況。這就意味著出現新情況的時候,就可能提供新的update()方法,或者是干脆重新實現觀察者;而拉模型就不會造成這樣的情況,因為拉模型下,update()方法的參數是主題對象本身,這基本上是主題對象能傳遞的最大數據集合了,基本上可以適應各種情況的需要。
Java中內置的觀察者模式
在Java的java.util包中有一個類Observable(被觀察者)和一個接口Observable(觀察者)共同構成觀察者模式。
Observable 源碼如下:
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
//創建一個空集合,用來存儲觀察者
public Observable() {
obs = new Vector<>();
}
//注冊觀察者,將一個觀察者添加到集合里
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
//刪除觀察者,將一個觀察者從集合里移除
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
//通知觀察者更新
public void notifyObservers() {
notifyObservers(null);
}
//通知觀察者更新,會調用update方法,并傳入自身和arg作為參數。
public void notifyObservers(Object arg) {
/*
* a temporary array buffer, used as a snapshot of the state of
* current Observers.
*/
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
//清空觀察者
public synchronized void deleteObservers() {
obs.removeAllElements();
}
//將標記changed設為true
protected synchronized void setChanged() {
changed = true;
}
//將標記changed設為false
protected synchronized void clearChanged() {
changed = false;
}
//檢測是否被觀察者狀態發生變化
public synchronized boolean hasChanged() {
return changed;
}
//獲取觀察者的數量
public synchronized int countObservers() {
return obs.size();
}
}
Observer源碼如下:
public interface Observer {
//用于更新觀察者自身
void update(Observable o, Object arg);
}
可以看到 Java 內置的Observable已經幫我們實現了多個方法,方便了開發人員。
使用Java內置的觀察者模式實現上面的小例子。
//具體主題,即被觀察者
public class InfoQ extends Observable {
@Override
public void notifyObservers() {
setChanged();
super.notifyObservers();
}
public String getContent() {
return "InfoQ發布新內容了,快來看看吧。";
}
}
//具體觀察者
public class Coder implements Observer {
private String mName;
public Coder(String name) {
mName = name;
}
@Override
public void update(Observable o, Object arg) {
String content = ((InfoQ)o).getContent();
System.out.println("Hi: " + mName + " " + content);
}
}
public class Client {
public static void main(String[] args) {
InfoQ infoQ = new InfoQ();
Coder coder1 = new Coder("linda");
Coder coder2 = new Coder("mary");
Coder coder3 = new Coder("andy");
infoQ.addObserver(coder1);
infoQ.addObserver(coder2);
infoQ.addObserver(coder3);
infoQ.notifyObservers();
}
}
日志如下:
Hi: andy InfoQ發布新內容了,快來看看吧。
Hi: mary InfoQ發布新內容了,快來看看吧。
Hi: linda InfoQ發布新內容了,快來看看吧。
總結
優點
- 觀察者和被觀察者之間是抽象耦合,應對業務變化。
- 增強系統靈活性、可擴展性
缺點
程序中包括一個被觀察者、多個觀察者,被觀察者發出的通知默認順序執行,一個觀察者卡頓,會影響整體的執行效率,這種情況下,一般考慮采用異步的方式。