觀察者模式——從JDK到Spring

大猿帥發表於2020-07-22
文章收錄在 GitHub JavaKeeper ,N線網際網路開發必備技能兵器譜

在軟體系統中經常會有這樣的需求:如果一個物件的狀態發生改變,某些與它相關的物件也要隨之做出相應的變化。

img

  • 微信公眾號,如果一個使用者訂閱了某個公眾號,那麼便會收到公眾號發來的訊息,那麼,公眾號就是『被觀察者』,而使用者就是『觀察者』
  • 氣象站可以將每天預測到的溫度、溼度、氣壓等以公告的形式釋出給各種第三方網站,如果天氣資料有更新,要能夠實時的通知給第三方,這裡的氣象局就是『被觀察者』,第三方網站就是『觀察者』
  • MVC 模式中的模型與檢視的關係也屬於觀察與被觀察

觀察者模式是使用頻率較高的設計模式之一。

觀察者模式包含觀察目標和觀察者兩類物件,一個目標可以有任意數目的與之相依賴的觀察者,一旦觀察目標的狀態發生改變,所有的觀察者都將得到通知。

定義

觀察者模式(Observer Pattern): 定義物件間一種一對多的依賴關係,使得當每一個物件改變狀態,則所有依賴於它的物件都會得到通知並自動更新。

觀察者模式是一種物件行為型模式

觀察者模式的別名包括髮布-訂閱(Publish/Subscribe)模式、模型-檢視(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。

細究的話,釋出訂閱和觀察者有些不同,可以理解成釋出訂閱模式屬於廣義上的觀察者模式。

img

角色

  • Subject(目標):被觀察者,它是指被觀察的物件。 從類圖中可以看到,類中有一個用來存放觀察者物件的Vector 容器(之所以使用Vector而不使用List,是因為多執行緒操作時,Vector在是安全的,而List則是不安全的),這個Vector容器是被觀察者類的核心,另外還有三個方法:attach方法是向這個容器中新增觀察者物件;detach方法是從容器中移除觀察者物件;notify方法是依次呼叫觀察者物件的對應方法。這個角色可以是介面,也可以是抽象類或者具體的類,因為很多情況下會與其他的模式混用,所以使用抽象類的情況比較多。
  • ConcreteSubject(具體目標):具體目標是目標類的子類,通常它包含經常發生改變的資料,當它的狀態發生改變時,向它的各個觀察者發出通知。同時它還實現了在目標類中定義的抽象業務邏輯方法(如果有的話)。如果無須擴充套件目標類,則具體目標類可以省略。
  • Observer(觀察者):觀察者將對觀察目標的改變做出反應,觀察者一般定義為介面,該介面宣告瞭更新資料的方法 update(),因此又稱為抽象觀察者
  • ConcreteObserver(具體觀察者):在具體觀察者中維護一個指向具體目標物件的引用,它儲存具體觀察者的有關狀態,這些狀態需要和具體目標的狀態保持一致;它實現了在抽象觀察者 Observer 中定義的 update()方法。通常在實現時,可以呼叫具體目標類的 attach() 方法將自己新增到目標類的集合中或通過 detach() 方法將自己從目標類的集合中刪除。

類圖

再記錄下 UML 類圖的注意事項,這裡我的 Subject 是抽象方法,所以用斜體,抽象方法也要用斜體,具體的各種箭頭意義,我之前也總結過《設計模式前傳——學設計模式前你要知道這些》(被網上各種帖子毒害過的自己,認真記錄~~~)。

例項

1、定義觀察者介面

interface Observer {
    public void update();
}

2、定義被觀察者

abstract class Subject {
    private Vector<Observer> obs = new Vector();

    public void addObserver(Observer obs){
        this.obs.add(obs);
    }
    public void delObserver(Observer obs){
        this.obs.remove(obs);
    }
    protected void notifyObserver(){
        for(Observer o: obs){
            o.update();
        }
    }
    public abstract void doSomething();
}

3、具體的被觀察者

class ConcreteSubject extends Subject {
    public void doSomething(){
        System.out.println("被觀察者事件發生改變");
        this.notifyObserver();
    }
}

4、具體的被觀察者

class ConcreteObserver1 implements Observer {
    public void update() {
        System.out.println("觀察者1收到資訊,並進行處理");
    }
}
class ConcreteObserver2 implements Observer {
    public void update() {
        System.out.println("觀察者2收到資訊,並進行處理");
    }
}

5、客戶端

public class Client {
    public static void main(String[] args){
        Subject sub = new ConcreteSubject();
        sub.addObserver(new ConcreteObserver1()); //新增觀察者1
        sub.addObserver(new ConcreteObserver2()); //新增觀察者2
        sub.doSomething();
    }
}

輸出

被觀察者事件發生改變
觀察者1收到資訊,並進行處理
觀察者2收到資訊,並進行處理

通過執行結果可以看到,我們只呼叫了 Subject 的方法,但同時兩個觀察者的相關方法都被呼叫了。仔細看一下程式碼,其實很簡單,就是在 Subject 類中關聯一下 Observer 類,並且在 doSomething() 方法中遍歷一下 Observerupdate() 方法就行了。

優缺點

優點

  • 降低了目標與觀察者之間的耦合關係,兩者之間是抽象耦合關係
  • 目標與觀察者之間建立了一套觸發機制
  • 支援廣播通訊
  • 符合“開閉原則”的要求

缺點

  • 目標與觀察者之間的依賴關係並沒有完全解除,而且有可能出現迴圈引用
  • 當觀察者物件很多時,通知的釋出會花費很多時間,影響程式的效率

應用

JDK中的觀察者模式

觀察者模式在 Java 語言中的地位非常重要。在 JDK 的 java.util 包中,提供了 Observable 類以及 Observer 介面,它們構成了 JDK 對觀察者模式的支援(可以去檢視下原始碼,寫的比較嚴謹)。but,在 Java9 被棄用了。

Spring 中的觀察者模式

Spring 事件驅動模型也是觀察者模式很經典的應用。就是我們常見的專案中最常見的事件監聽器。

1. Spring 中觀察者模式的四個角色

  1. 事件:ApplicationEvent 是所有事件物件的父類。ApplicationEvent 繼承自 jdk 的 EventObject, 所有的事件都需要繼承 ApplicationEvent, 並且通過 source 得到事件源。

    Spring 也為我們提供了很多內建事件,ContextRefreshedEventContextStartedEventContextStoppedEventContextClosedEventRequestHandledEvent

  2. 事件監聽:ApplicationListener,也就是觀察者,繼承自 jdk 的 EventListener,該類中只有一個方法 onApplicationEvent。當監聽的事件發生後該方法會被執行。
  3. 事件源:ApplicationContextApplicationContext 是 Spring 中的核心容器,在事件監聽中 ApplicationContext 可以作為事件的釋出者,也就是事件源。因為 ApplicationContext 繼承自 ApplicationEventPublisher。在 ApplicationEventPublisher 中定義了事件釋出的方法:publishEvent(Object event)
  4. 事件管理:ApplicationEventMulticaster,用於事件監聽器的註冊和事件的廣播。監聽器的註冊就是通過它來實現的,它的作用是把 Applicationcontext 釋出的 Event 廣播給它的監聽器列表。

2. coding~~

1、定義事件

public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
        System.out.println("my Event");
    }
}

2、實現事件監聽器

@Component
class MyListenerA implements ApplicationListener<MyEvent> {
    public void onApplicationEvent(MyEvent AyEvent) {
        System.out.println("ListenerA received");
    }
}

@Component
class MyListenerB implements ApplicationListener<MyEvent> {
    public void onApplicationEvent(MyEvent AyEvent) {
        System.out.println("ListenerB received");
    }
}

3、事件釋出者

@Component
public class MyPublisher implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext=applicationContext;
    }
    
    public void publishEvent(ApplicationEvent event){
        System.out.println("publish event");
        applicationContext.publishEvent(event);
    }
}

4、測試,先用註解方式將 MyPublisher 注入 Spring

@Configuration
@ComponentScan
public class AppConfig {

    @Bean(name = "myPublisher")
    public MyPublisher myPublisher(){
        return new MyPublisher();
    }
}
public class Client {

    @Test
    public void main() {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        MyPublisher myPublisher = (MyPublisher) context.getBean("myPublisher");
        myPublisher.publishEvent(new MyEvent(this));
    }
}

5、輸出

my Event
publish event
ListenerA received
ListenerB received

瞎扯

設計模式真的只是一種設計思想,不需要非得有多個觀察者才可以用觀察者模式,只有一個觀察者,我也要用。

再舉個例子,我是做廣告投放的嘛(廣告投放的商品檔案一般為 xml),假如我的廣告位有些空閒流量,這我得利用起來呀,所以我就從淘寶客或者拼夕夕的多多客上通過開放的 API 獲取一些,這個時候我也可以用觀察者模式,每次請求 10 萬條商品,我就生成一個新的商品檔案,這個時候我也可以用觀察者模式,獲取商品的類是被觀察者,寫商品檔案的是觀察者,當商品夠10萬條了,就通知觀察者重新寫到一個新的檔案。

大佬可能覺這麼實現有點費勁,不用設計模式也好,或者用訊息佇列也好,其實都只是一種手段,選擇適合自己業務的,開心就好。

參考

https://design-patterns.readt...

https://www.cnblogs.com/jmcui...

相關文章