設計模式系列——觀察者模式

前言

觀察者模式是一個使用率非常高的模式,常用與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發布新內容了,快來看看吧。

總結

優點
  • 觀察者和被觀察者之間是抽象耦合,應對業務變化。
  • 增強系統靈活性、可擴展性
缺點

程序中包括一個被觀察者、多個觀察者,被觀察者發出的通知默認順序執行,一個觀察者卡頓,會影響整體的執行效率,這種情況下,一般考慮采用異步的方式。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容