天天看點

CRUD很無聊?一起學設計模式吧!— 觀察者模

定義

觀察者模式是對象的行為模式,又叫釋出-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。

觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀态上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。

一個軟體系統常常要求在某一個對象的狀态發生變化的時候,某些其他的對象做出相應的改變。做到這一點的設計方案有很多,但是為了使系統能夠易于複用,應該選擇低耦合度的設計方案。減少對象之間的耦合有利于系統的複用,但是同時設計師需要使這些低耦合度的對象之間能夠維持行動的協調一緻,保證高度的協作。觀察者模式是滿足這一要求的各種設計方案中最重要的一種。

顧名思義,觀察者主要是分為兩個大的對象角色,觀察者和主題(也叫被觀察者)。他們是如何做到松耦合的呢?

關于觀察者的一切,主題隻需要知道觀察者實作了某個接口,主題不需要知道觀察者的具體類是誰,做了什麼或其他任何細節。當有新的觀察者出現時,主題的代碼不需要做任何修改。如果有個新的對象需要成為觀察者并接收主題的變化通知,我們隻需要讓這個對象實作此觀察者接口,然後注冊成為觀察者即可。主題不在乎别的,它隻會發送通知給所有實作了觀察者接口的對象。

改變主題或觀察者其中一方,并不影響另外一方。兩者之間是松耦合的,是以隻要他們之間的接口仍被遵守,我們就可以自由的改變他們。

UML

CRUD很無聊?一起學設計模式吧!— 觀察者模

角色定義

觀察者模式涉及四個角色

  • 抽象主題(Subject):抽象主題角色把所有對觀察者對象的引用儲存在一個聚集(比如ArrayList對象)裡,每個主題都可以有任何數量的觀察者。抽象主題提供一個接口,可以增加和删除觀察者對 象,抽象主題角色又叫做抽象被觀察者(Observable)角色。
  • 具體主題(ConcreteSubject):将有關狀态存入具體觀察者對象;在具體主題的内部狀态改變時,給所有登記過的觀察者發出通知。具體主題角色又叫做具體被觀察者(Concrete Observable)角色
  • 觀察者對象(Observer):所有潛在的觀察者必須實作觀察者接口,這個接口一般隻有一個方法update(),當主題對象發生變化時它被調用。
  • 具體觀察者(ConcreteObserver):具體的觀察者可以是實作觀察者對象的任意類,觀察者必須要注冊具體的主題,以便接收更新。

場景實戰

平時我們都喜歡關注一些微信公衆号看一些大佬吹牛皮,當公衆号釋出消息後,作為粉絲的我們就可以收到消息。而對于公衆号作者而言,他們主要是管理粉絲還有吹牛皮,這個場景我們可以套用觀察者模式來實作。

代碼示例

抽象觀察者

首先我們定義一個抽象觀察者,這個類很簡單,隻要定義一個所有觀察者公有的方法。在我們的場景中,當公衆号釋出消息後我們就能得到通知,我們這裡使用一個String類型的參數用來接收公衆号的消息。

/**
 * Description:
 * 抽象觀察者,所有實作該接口的類都可以成為觀察者
 * @author jam
 * @date 2019/4/19下午11:25
 */
public interface Observer {
    public void recive(String message);
}      

具體觀察者

關注公衆号的讀者稱之為粉絲,這裡我們簡單的列印一下公衆号釋出的消息

/**
 * <p>
 * <code>User</code>
 * </p>
 * Description:
 * 具體觀察者 -- 粉絲類
 * @author jam
 * @date 2019/4/19下午11:26
 */
public class Fans implements Observer {
    private String name;

    public Fans(String name) {
        this.name = name;
    }

    @Override
    public void recive(String message) {
        System.out.println("粉絲 " + this.name + " 收到消息: "+message);
    }
}      

抽象主題(抽象被觀察者)

抽象主題主要是定義一些接口用于管理觀察者,公衆号有權利來管理所有的粉絲并在釋出消息後通知所有的粉絲。

/**
 * <p>
 * <code>Subject</code>
 * </p>
 * Description:
 *  定義抽象主題,主題需要實作添加、删除、通知觀察者
 * @author jam
 * @date 2019/4/19下午11:20
 */
public interface Subject {
    /**
     * 注冊成為觀察者
     * @param observer
     */
    public void attach(Observer observer);

    /**
     * 删除觀察者
     * @param observer
     */
    public void detach(Observer observer);

    /**
     * 通知所有觀察者
     */
    public void notifyObservers();
}      

具體主題(具體被觀察者)

具體主題除了必須要實作抽象主題定義的方法外,還需要有個額外的方法change(),當主題發生變化時才會告訴觀察者。在本場景中公衆号釋出消息,我們使用publish(String message)方法替代change()方法。

/**
 * <p>
 * <code>WechatServer</code>
 * </p>
 * Description:
 * 具體主題
 * @author jam
 * @date 2019/4/19下午11:24
 */
public class WechatServer implements Subject {
    private ArrayList<Observer> observers;
    private String message;

    public WechatServer() {
        observers = new ArrayList<>();
    }

    /**
     * 讓一個使用者注冊成為觀察者即粉絲
     * @param observer
     */
    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    /**
     * 不喜歡這個觀察者,删除掉
     * @param observer
     */
    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    /**
     * 通知所有的觀察者
     */
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.recive(message);
        }
    }

    /**
     * 當主題發生變化時通知觀察者
     * @param message
     */
    public void publish(String message){
        this.message = message;
        System.out.println("webchat publish message:" + message);
        notifyObservers();
    }
}      

測試類

public class Test {
    public static void main(String[] args) {
        WechatServer wechatServer = new WechatServer();
        Fans fans1 = new Fans("張三");
        Fans fans2 = new Fans("李四");
        Fans fans3 = new Fans("楊五");

        wechatServer.attach(fans1);
        wechatServer.attach(fans2);
        wechatServer.attach(fans3);
        wechatServer.publish("CRUD很無聊?跟我一起學指令模式吧!");

        System.out.println("--------");
                //删除其中一個粉絲
        wechatServer.detach(fans1);
        wechatServer.publish("CRUD很無聊?跟我一起學觀察者模式吧!");

    }
}      

執行結果

CRUD很無聊?一起學設計模式吧!— 觀察者模

通過執行結果我們可以發現,在公衆号釋出消息後所有的粉絲都可以收到消息,而取消關注或者被剔除粉絲隊伍以後就收不到消息了。

推模式與拉模式

我們上面的場景是觀察者模式中的推模式,這種場景是主題主動向觀察者推送資料,不管觀察者需要不需要。推模式的前提是主題對象知道觀察者需要的資料,觀察中的update()方法裡的參數是按照需要定義的方法,但是随着業務的發展會出現考慮不到的情形。

比如我們上述場景中粉絲隻需要知道公衆号釋出的内容,是以我們先約定String類型的參數,但是有些粉絲卻想知道這個消息的真實作者是誰(是否轉載?),這個時候就需要提供新的方法,比如update(String message,String author),或者幹脆重新實作觀察者,不管如何都得作相應的改動。

觀察者模式還有另外一個模式拉模式,這個模式不需要知道觀察者需要什麼資料,他把主題自身都傳遞給觀察者,update(Subject subject),然後對外提供一些getter方法,讓觀察者按需來取,這樣基本上可以适用各種情況的需要。

接下來我們用拉模式來實作上面的場景。

這裡我們不再适用約定參數處理recive方法,而是使用主題直接作為參數。

public interface Observer {
    /**
     * 使用主題作為參數
     * @param subject
     */
    public void recive(Subject subject);
}      

收到通知後,我們按需從主題對象中擷取相應的資料。

public class Fans implements Observer {
    private String name;

    public Fans(String name) {
        this.name = name;
    }

    @Override
    public void recive(Subject subject) {
        //觀察者可以同時觀察多個主題
        //是以我們需要確定被觀察者屬于我們需要的WechatServer類型
        //如果是其他類型可能需要作其他方式處理
        if(subject instanceof WechatServer){
            WechatServer wechatServer = (WechatServer) subject;
            System.out.println("粉絲 " + this.name + " 收到消息: "
                    + wechatServer.getMessage() + " 作者是:"
                    + wechatServer.getAuthor());
        }
    }
}      

抽象主題

未發生變化

public interface Subject {
    /**
     * 注冊成為觀察者
     * @param observer
     */
    public void attach(Observer observer);

    /**
     * 删除觀察者
     * @param observer
     */
    public void detach(Observer observer);

    /**
     * 通知所有觀察者
     */
    public void notifyObservers();
}      

具體主題

重點關注

notifyObservers()

方法,直接将this即目前主題作為參數傳遞給觀察者,并對外提供

getMessage()

getAuthor()

方法,好讓觀察者對象可以友善取走想要的資料。

public class WechatServer implements Subject {
    private ArrayList<Observer> observers;
    private String message;
    private String author;

    /**
     * 對外提供擷取内容的方法
     * @return
     */
    public String getMessage() {
        return message;
    }

    /**
     * 對外提供擷取作者的方法
     * @return
     */
    public String getAuthor() {
        return author;
    }

    public WechatServer() {
        observers = new ArrayList<>();
    }

    /**
     * 讓一個使用者注冊成為觀察者即粉絲
     * @param observer
     */
    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }

    /**
     * 不喜歡這個觀察者,删除掉
     * @param observer
     */
    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }

    /**
     * 通知所有的觀察者
     */
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            //這裡不再是具體的參數,而是把主題自身給通知給觀察者
            observer.recive(this);
        }
    }

    /**
     * 當主題發生變化時通知觀察者
     * @param message
     */
    public void publish(String message,String author){
        this.message = message;
        this.author = author;
        System.out.println("webchat publish message:" + message);
        notifyObservers();
    }
}      

public class Test {
    public static void main(String[] args) {
        WechatServer wechatServer = new WechatServer();
        Fans fans1 = new Fans("張三");
        Fans fans2 = new Fans("李四");
        Fans fans3 = new Fans("楊五");

        wechatServer.attach(fans1);
        wechatServer.attach(fans2);
        wechatServer.attach(fans3);
        wechatServer.publish("CRUD很無聊?跟我一起學指令模式吧!","JAVA日知錄");

        System.out.println("--------");

        wechatServer.detach(fans1);
        wechatServer.publish("CRUD很無聊?跟我一起學觀察者模式吧!","JAVA日知錄");

    }
}      

CRUD很無聊?一起學設計模式吧!— 觀察者模

使用拉模式後隻要主題提供了對應的get方法,基本可以滿足各種需求的場景。

再深入一點

觀察者模式在JAVA中已經有相應的實作,抽象觀察者角色由

java.util.Observer

充當,抽象主題角色由

java.util.Observable

充當。

我們可以利用java内置的觀察者模式很容易實作上面的推模式和拉模式的場景代碼,這裡就不再示範了。

最後提醒大家一下,主題角色Observable是一個類,我們要想要實作具體的主題必須要繼承它,如果某類想同時具有Observable和其他一個超類的行為,就會陷入兩難,畢竟JAVA不支援多重繼承。

如果你的應用場景中不需要考慮如上情形,那麼Observable可能會符合你的需求,否則還是需要使用自定義觀察者模式來實作你的需求,反正這也很簡單,不是嗎?