設計模式之觀察者模式

2022-09-22 18:06:11

觀察者模式是極其重要的一個設計模式,也是我幾年開發過程中使用最多的設計模式,本文首先概述觀察者模式的基本概念和Demo實現,接著是觀察者模式在Java和Spring中的應用,最後是對觀察者模式的應用場景和優缺點進行總結。

一、概念理解

觀察者模式:定義物件之間的一種一對多的依賴關係,使得每當一個物件的狀態發生變化時,其相關的依賴物件都可以得到通知並被自動更新。主要用於多個不同的物件對一個物件的某個方法會做出不同的反應!

概念啥意思呢?也就是說,如果使用觀察者模式在A的業務邏輯中呼叫B的業務邏輯,即使B的業務邏輯報錯了,仍然不影響A的執行。

比如,在我最近公司開發商城系統的過程中,提交訂單成功以後要刪除購物車中的資訊,如果我先寫訂單提交邏輯,接著寫刪除購物車邏輯,這樣當然沒有什麼問題,但是這樣程式的健壯性太差了。

應該將該業務分成兩步,一是處理訂單成功處理邏輯,二是刪除購物車中的資訊。即使刪除購物車報錯了,提交訂單邏輯仍然不影響。

那應該怎麼做才能讓他們互不影響呢?需要在購物車物件中要有一個方法用於刪除購物車,還要有一個物件A用於注入(add)購物車物件和通知(notify)購物車執行它的方法。

在執行時先呼叫物件A的add方法將購物車物件新增到物件A中,在訂單提交成功以後,呼叫物件A的通知notify購物車方法執行清除購物車邏輯。

在觀察者模式中,購物車就稱為觀察者,物件A就稱為目標物件。在面向介面程式設計原則下,觀察者模式應該包括四個角色:

1、目標介面(subject) :它是一個抽象類,也是所有目標物件的父類別。它用一個列表記錄當前目標物件有哪些觀察者物件,並提供增加、刪除觀察者物件和通知觀察者物件的方法宣告。

2、具體目標類:可以有多個不同的具體目標類,它們同時繼承Subject類。一個目標物件就是某個具體目標類的物件,一個具體目標類負責定義它自身的事務邏輯,並在狀態改變時通知它的所有觀察者物件。

3、觀察者介面(Listener) 它也是一個抽象類,是所有觀察者物件的父類別;它為所有的觀察者物件都定義了一個名為update(notify)的方法。當目標物件的狀態改變時,它就是通過呼叫它的所有觀察者物件的update(notify)方法來通知它們的。

4、具體觀察者類,可以有多個不同的具體觀察者類,它們同時繼承Listener類。一個觀察者物件就是某個具體觀察者類的物件。每個具體觀察者類都要重定義Listener類中定義的update(notify)方法,在該方法中實現它自己的任務邏輯,當它被通知的時候(目標物件呼叫它的update(notify)方法)就執行自己特有的任務。在我們的例子中是購物車觀察者,當然還能有別的,如紀錄檔觀察者。

我們基於四個角色實現demo。

二、案例實現

目標介面:包括註冊、移除、通知監聽者的方法宣告。

/**
 * 這是被觀察的物件
 * 目標類
 * @author tcy
 * @Date 17-09-2022
 */
public interface SubjectAbstract<T> {
    //註冊監聽者
    public void registerListener(T t);
    //移除監聽者
    public void removeListener(T t);
    //通知監聽者
    public void notifyListener();
}

目標介面實現:裡面需要一個listenerList陣列儲存所有的觀察者,需要定義add和remove觀察者的方法,需要給出notify方法通知所有的觀察者物件。

/**
 * 
 * 具體目標類
 * @author tcy
 * @Date 17-09-2022
 */
public class SubjectImpl implements SubjectAbstract<ListenerAbstract> {

    //監聽者的註冊列表
    private List<ListenerAbstract> listenerList = new ArrayList<>();

    @Override
    public void registerListener(ListenerAbstract myListener) {
        listenerList.add(myListener);
    }

    @Override
    public void removeListener(ListenerAbstract myListener) {
        listenerList.remove(myListener);
    }

    @Override
    public void notifyListener() {
        for (ListenerAbstract myListener : listenerList) {
            System.out.println("收到推播事件,開始呼叫非同步邏輯...");
            myListener.onEvent();
        }
    }
}

觀察者介面:宣告響應方法

/**
 * 
 * 觀察者-介面
 * @author tcy
 * @Date 17-09-2022
 */
public interface ListenerAbstract {
    void onEvent();
}

觀察者介面:實現響應方法,處理清除購物車的邏輯。

/**
 * 具體觀察者類 購物車
 * @author tcy
 * @Date 17-09-2022
 */
public class ListenerMyShopCart implements ListenerAbstract {
    @Override
    public void onEvent() {

            //...省略購物車處理邏輯
            System.out.println("刪除購物車中的資訊...");

    }
}

我們使用Client模擬提交訂單操作。

/**
 * 先使用具體目標物件的registerListener方法新增具體觀察者物件,
 * 然後呼叫其notify方法通知觀察者
 * @author tcy
 * @Date 17-09-2022
 */
public class Client {
    public static void main(String[] args) {

        System.out.println("訂單成功處理邏輯...");
        //建立目標物件
        SubjectImpl subject=new SubjectImpl();

        //具體觀察者註冊入 目標物件
        ListenerMyShopCart shopCart=new ListenerMyShopCart();
        //向觀察者中註冊listener
        subject.registerListener(shopCart);

        //釋出事件,通知觀察者
        subject.notifyListener();

    }
}

這樣就實現了訂單的處理邏輯和購物車的邏輯解耦,即使購物車邏輯報錯也不會影響訂單處理邏輯。

既然觀察者模式是很常用的模式,而且抽象觀察者和抽象目標類方法宣告都是固定的,作為高階語言Java,Java設計者乾脆內建兩個介面,開發者直接實現介面就能使用觀察者模式。

三、Java中的觀察者模式

在 Java 中,通過 java.util.Observable 類和 java.util.Observer 介面定義觀察者模式,只要實現它們的子類就可以編寫觀察者模式範例。

Observable 類是抽象目標類,它有一個 Vector 向量,用於儲存所有要通知的觀察者物件,下面來介紹它最重要的 3 個方法。

void addObserver(Observer o) 方法:用於將新的觀察者物件新增到向量中。

void notifyObservers(Object arg) 方法:呼叫向量中的所有觀察者物件的 update() 方法,通知它們資料發生改變。通常越晚加入向量的觀察者越先得到通知。

void setChange() 方法:用來設定一個 boolean 型別的內部標誌位,註明目標物件發生了變化。當它為真時,notifyObservers() 才會通知觀察者。

Observer 介面是抽象觀察者,它監視目標物件的變化,當目標物件發生變化時,觀察者得到通知,並呼叫 void update(Observable o,Object arg) 方法,進行相應的工作。

我們基於Java的兩個介面,改造我們的案例。

具體目標類:

/**
 * 具體目標類 
 * @author tcy
 * @Date 19-09-2022
 */
public class SubjectObservable extends Observable {

    public void notifyListener() {
        super.setChanged();
        System.out.println("收到推播的訊息...");
        super.notifyObservers();    //通知觀察者購物車事件
    }

}

具體觀察者類:

/**
 * 觀察者實現類
 * @author tcy
 * @Date 19-09-2022
 */
public class ShopCartObserver implements Observer {

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("清除購物車...");

    }
}

依舊是Client模擬訂單處理邏輯。

/**
 * @author tcy
 * @Date 19-09-2022
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("訂單提交成功...");
        SubjectObservable observable = new SubjectObservable();

        Observer shopCartObserver = new ShopCartObserver(); //購物車

        observable.addObserver(shopCartObserver);
        observable.notifyListener();


    }

}

這樣也能實現觀察者邏輯,但Java中的觀察者模式有一定的侷限性。

Observable是個類,而不是一個介面,沒有實現Serializable,所以,不能序列化和它的子類,而且他是執行緒不安全的,無法保證觀察者的執行順序。在JDK9之後已經啟用了。

寫Java的恐怕沒有不用Spring的了,作為優秀的開源框架,Spring中也有觀察者模式的大量應用,而且Spring是在java的基礎之上改造的,很好的規避了Java觀察者模式的不足之處。

四、Spring如何使用觀察者模式

在第一章節典型的觀察者模式中包含著四個角色:目標類、目標類實現、觀察者、觀察者實現類。而在Spring下的觀察者模式略有不同,Spring對其做了部分改造。

事件:

Spring中定義最頂層的事件ApplicationEvent,這個介面最終還是繼承了EventObject介面。

只是在基礎之上增加了構造和獲取當前時間戳的方法,Spring所有的事件都要實現這個介面,比如Spring中內建的ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent...看名字大概就知道這些事件用於哪些地方,分別是容器重新整理後、開始時、停止時...

目標類介面:

Spirng中的ApplicationEventMulticaster介面就是範例中目標類,我們可以對比我們的目標介面和ApplicationEventMulticaster介面,長的非常像。

觀察者介面:

觀察者ApplicationListener用於監聽事件,只有一個方法onApplicationEvent事件發生後該事件執行。與我們樣例中的抽象觀察者並無太大的不同。

目標類實現:

在我們案例中目標類的職責直接在一個類中實現,註冊監聽器、廣播事件(呼叫監聽器方法)。

在Spring中兩個實現類分別拆分開來,Spring啟動過程中會呼叫registerListeners()方法,看名字我們大概就已經知道是註冊所有的監聽器,該方法完成原目標類的註冊監聽器職責。

在Spring中事件源ApplicationContext用於廣播事件,使用者不必再顯示的呼叫監聽器的方法,交給Spring呼叫,該方法完成原目標類的廣播事件職責。

我們基於Spring的觀察者模式繼續改造我們的案例。

購物車事件:

/**
 * 購物車事件
 * @author tcy
 * @Date 19-09-2022
 */
@Component
public class EventShopCart extends ApplicationEvent {

    private String orderId;

    public EventShopCart(Object source, String orderId) {
        super(source);
        this.orderId=orderId;
    }


    public EventShopCart() {
        super(1);
    }
}

釋出者(模擬Spring呼叫監聽器的方法,實際開發不需要寫):

/**
 * 釋出者
 * @author tcy
 * @Date 19-09-2022
 */
@Component
public class MyPublisher implements ApplicationContextAware {
    private ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 釋出事件
     * 監聽該事件的監聽者都可以獲取訊息
     *
     * @param myEvent
     */
    public void workEvent(EventShopCart myEvent) {
        //該方法會呼叫監聽器實現的方法
        applicationContext.publishEvent(myEvent);
    }
}

監聽者:

/**
 * 監聽者
 * @author tcy
 * @Date 19-09-2022
 */
@Component
public class ListenerShopCart implements ApplicationListener<EventShopCart> {
    @Override
    public void onApplicationEvent(EventShopCart myEvent) {
        System.out.println("清除購物車成功...");
    }
}

Client模擬呼叫:

/**
 * @author tcy
 * @Date 19-09-2022
 */
public class Client {

    public static void main(String[] args) {
        ApplicationContext ac =new AnnotationConfigApplicationContext("cn.sky1998.behavior.observer.spring");

        System.out.println("訂單提交成功...");
        MyPublisher bean = ac.getBean(MyPublisher.class);
        EventShopCart myEvent = ac.getBean(EventShopCart.class);

        bean.workEvent(myEvent);
    }
}

通過Spring實現觀察者模式比我們手動寫簡單的多。

使用Spring實現觀察者模式時,觀察者介面、目標介面、目標實現,我們都不需要管,只負責繼承ApplicationEvent類定義我們自己的事件,並實現ApplicationListener<自定義事件>介面實現我們的觀察者,並在對應的業務中呼叫applicationContext.publishEvent(new ShopCartEvent(cmOrderItemList)),即實現了觀察者模式。

讀者可以拉取完整程式碼本地學習,實現程式碼均測試通過上傳到碼雲

五、總結

Spring使用觀察者模式我在很久之前就使用過,但是並不清楚為什麼要這樣寫,學了觀察者模式以後,寫起來變得通透多了。

雖然觀察者模式的概念是:一對多的依賴關係,但不一定觀察者有多個才能使用,我們的例子都是使用的一個觀察者。

它很好的降低了目標與觀察者之間的耦合關係,目標與觀察者建立一套觸發機制,也讓他成為了最常見的設計模式。

設計模式的學習要成體系,推薦你看我往期釋出的設計模式文章。

一、設計模式概述

二、設計模式之工廠方法和抽象工廠

三、設計模式之單例和原型

四、設計模式之建造者模式

五、設計模式之代理模式

六、設計模式之介面卡模式

七、設計模式之橋接模式

八、設計模式之組合模式

九、設計模式之裝飾器模式

十、設計模式之外觀模式

十一、外觀模式之享元模式

十二、設計模式之責任鏈模式

十三、設計模式之命令模式

十四、設計模式之直譯器模式

十五、設計模式之迭代器模式

十六、設計模式之中介者模式

十七、設計模式之備忘錄模式