天天看點

設計模式之觀察者模式

文章收錄在 GitHub JavaKeeper ,N線網際網路開發必備技能兵器譜

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

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

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

設計模式之觀察者模式

觀察者模式包含觀察目标和觀察者兩類對象,一個目标可以有任意數目的與之相依賴的觀察者,一旦觀察目标的狀态發生改變,所有的觀察者都将得到通知。

定義

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

觀察者模式是一種對象行為型模式。

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

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

設計模式之觀察者模式

角色

  • 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()

方法中周遊一下

Observer

update()

方法就行了。

優缺點

優點

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

缺點

  • 目标與觀察者之間的依賴關系并沒有完全解除,而且有可能出現循環引用
  • 當觀察者對象很多時,通知的釋出會花費很多時間,影響程式的效率

應用

JDK中的觀察者模式

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

Spring 中的觀察者模式

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

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

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

Spring 也為我們提供了很多内置事件,

ContextRefreshedEvent

ContextStartedEvent

ContextStoppedEvent

ContextClosedEvent

RequestHandledEvent

  1. 事件監聽:ApplicationListener,也就是觀察者,繼承自 jdk 的 EventListener,該類中隻有一個方法 onApplicationEvent。當監聽的事件發生後該方法會被執行。
  2. 事件源:ApplicationContext,

    ApplicationContext

    是 Spring 中的核心容器,在事件監聽中 ApplicationContext 可以作為事件的釋出者,也就是事件源。因為 ApplicationContext 繼承自 ApplicationEventPublisher。在

    ApplicationEventPublisher

    中定義了事件釋出的方法:

    publishEvent(Object event)

  3. 事件管理: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.readthedocs.io/zh_CN/latest/behavioral_patterns/observer.html https://www.cnblogs.com/jmcui/p/11054756.html