三種方式實現(xiàn)觀察者模式 及 Spring中的事件編程模型

觀察者模式可以說是眾多設(shè)計模式中淮韭,最容易理解的設(shè)計模式之一了,觀察者模式在Spring中也隨處可見贴届,面試的時候靠粪,面試官可能會問,嘿毫蚓,你既然讀過Spring源碼占键,那你說說Spring中運用的設(shè)計模式吧,你可以自信的告訴他元潘,Spring中的ApplicationListener就運用了觀察者模式畔乙。

讓我們一步一步來,首先我們要知道到底什么是觀察者模式翩概,用Java是如何實現(xiàn)的牲距,在這里,我將會用三種方式來實現(xiàn)觀察者模式钥庇。

什么是觀察者模式

在現(xiàn)實生活中牍鞠,觀察者模式處處可見,比如

  • 看新聞评姨,只要新聞開始播放了难述,就會把新聞推送給訂閱了新聞的用戶,在這里吐句,新聞就是【被觀察者】胁后,而用戶就是【觀察者】。

  • 微信公眾號嗦枢,如果一個用戶訂閱了某個公眾號攀芯,那么便會收到公眾號發(fā)來的消息,那么文虏,公眾號就是【被觀察者】侣诺,而用戶就是【觀察者】。

  • 熱水器择葡,假設(shè)熱水器由三部分組成紧武,熱水器剃氧,警報器敏储,顯示器,熱水器僅僅負(fù)責(zé)燒水朋鞍,當(dāng)水溫到達(dá)設(shè)定的溫度后已添,通知警報器妥箕,警報器發(fā)出警報,顯示器也需要訂閱熱水器的燒水事件更舞,從而獲得水溫畦幢,并顯示。熱水器就是【被觀察者】缆蝉,警報器宇葱,顯示器就是【觀察者】。

在這里刊头,可以看到黍瞧,【觀察者】已經(jīng)失去自主的權(quán)利,只能被動的接收來自【被觀察者】的事件原杂,無法主動觀察印颤。【觀察者】成為了“受”穿肄,而【被觀察者】成為了“攻”年局。【被觀察者】只是通知【觀察者】咸产,不關(guān)心【觀察者】收到通知后矢否,會執(zhí)行怎樣的動作。

而在設(shè)計模式中脑溢,又把【被觀察者】稱為【主題】兴喂。

在觀察者設(shè)計模式中,一般有四個角色:

  • 抽象主題角色(Subject)
  • 具體主題角色(ConcreteSubject)
  • 抽象觀察者角色(Observer)
  • 具體觀察者角色(ConcreteObserver)

其中焚志,【主題】需要有一個列表字段衣迷,用來保存【觀察者】的引用,提供兩個方法(虛方法)酱酬,即【刪除觀察者】【增加觀察者】壶谒,還需要提供一個給客戶端調(diào)用的方法,通知各個【觀察者】:你們關(guān)心(訂閱)的事件已經(jīng)推送給你們了膳沽。

下面汗菜,我就用三種方式來實現(xiàn)觀察者模式。

經(jīng)典

public class News {
    private String title;
    private String content;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

此類不屬于觀察者模式必須的類挑社,用來存放事件的信息陨界。

public interface Subject {
    List<People> peopleList = new ArrayList<>();

    default void add(People people) {
        peopleList.add(people);
    }

    default void remove(People people) {
        peopleList.remove(people);
    }

    void update();
}

抽象主題角色,在這個角色中痛阻,有一個字段peopleList菌瘪,用來保存【觀察者】的引用,同時定義了兩個接口,這是Java8默認(rèn)接口實現(xiàn)的寫法俏扩。這兩個接口是給客戶端調(diào)用的糜工,用來【刪除觀察者】【增加觀察者】,還提供一個方法录淡,此方法需要被【具體主題角色】重寫捌木,用來通知各個【觀察者】。

public class NewsSubject implements Subject{
    public void update() {
        for (People people : peopleList) {
            News news = new News();
            news.setContent("今日在大街上嫉戚,有人躲在草叢中襲擊路人刨裆,還大喊“德瑪西亞萬歲”");
            news.setTitle("德瑪西亞出現(xiàn)了");
            people.update(news);
        }
    }
}

具體主題角色,重寫了【抽象主題角色】的方法彬檀,循環(huán)列表崔拥,通知各個【觀察者】。

public interface People {
    void update(News news);
}

抽象觀察者角色凤覆,定義了一個接口链瓦,【具體觀察者角色】需要重寫這個方法。

下面就是【具體觀察者角色】了:

public class PeopleA implements People {
    @Override
    public void update(News news) {
        System.out.println("這個新聞?wù)婧每?);
    }
}
public class PeopleB implements People {
    @Override
    public void update(News news) {
        System.out.println("這個新聞?wù)鏌o語");
    }
}
public class PeopleC implements People {
    @Override
    public void update(News news) {
        System.out.println("這個新聞?wù)娑?);
    }
}

客戶端:

public class Main {
    public static void main(String[] args) {
        Subject subject = new NewsSubject();
        subject.add(new PeopleA());
        subject.add(new PeopleB());
        subject.add(new PeopleC());
        subject.update();
    }
}

運行:


image.png

我們學(xué)習(xí)設(shè)計模式盯桦,必須知道設(shè)計模式的優(yōu)缺點慈俯,那么觀察者設(shè)計模式的優(yōu)缺點是什么呢?

優(yōu)點:

  • 【主題】和【觀察者】通過抽象拥峦,建立了一個松耦合的關(guān)系贴膘,【主題】只知道當(dāng)前有哪些【觀察者】,并且發(fā)送通知略号,但是不知道【觀察者】具體會執(zhí)行怎樣的動作刑峡。這也很好理解,比如 微信公眾號推送了一個消息過來玄柠,它不知道你會采取如何的動作突梦,是 微笑的打開,還是憤怒的打開羽利,或者是直接把消息刪了宫患,又或者把手機(jī)扔到洗衣機(jī)洗刷刷。

  • 符合開閉原則这弧,如果需要新增一個【觀察者】娃闲,只需要寫一個類去實現(xiàn)【抽象觀察者角色】即可,不需要改動原來的代碼匾浪。

缺點:

  • 客戶端必須知道所有的【觀察者】皇帮,并且進(jìn)行【增加觀察者】和【刪除觀察者】的操作。

  • 如果有很多【觀察者】蛋辈,那么所有的【觀察者】收到通知属拾,可能需要花費很久時間。

當(dāng)然以上優(yōu)缺點,是最直觀的捌年,可以很容易理解,并且體會到的挂洛。其他優(yōu)缺點礼预,可以自行百度。

Lambda

在介紹這種寫法之前虏劲,有必要介紹下函數(shù)式接口托酸,函數(shù)式接口的概念由來已久,一般來說只定義了一個虛方法的接口就叫函數(shù)式接口柒巫,在Java8中励堡,由于Lambda表達(dá)式的出現(xiàn),讓函數(shù)式接口大放異彩堡掏。

我們僅僅需要修改客戶端的代碼就可以:

    public static void main(String[] args) {
        Subject subject = new NewsSubject();
        subject.add(a -> System.out.println("已閱這新聞"));
        subject.add(a -> System.out.println("假的吧"));
        subject.add(a -> System.out.println("昨天就看過了"));
        subject.update();
    }

運行結(jié)果:


image.png

利用Lambda表達(dá)式和函數(shù)式接口应结,可以省去【具體觀察者角色】的定義,但是個人認(rèn)為泉唁,這并非屬于嚴(yán)格意義上的觀察者模式鹅龄,而且弊端很明顯:

  • 客戶端需要知道觀察者的具體實現(xiàn)。
  • 如果觀察者的具體實現(xiàn)比較復(fù)雜亭畜,可能代碼并沒有那么清晰扮休。

所以這種寫法,具有一定的局限性拴鸵。

借用大神的一句話

設(shè)計模式的出現(xiàn)玷坠,是為了彌補語言的缺陷。

正是由于語言的升級劲藐,讓某些設(shè)計模式發(fā)生了一定的變化八堡,除了觀察者模式,還有模板方法模式聘芜、責(zé)任鏈模式等秕重,都由于 Lambda表達(dá)式的出現(xiàn),而出現(xiàn)了一些變化厉膀。

JDK

在Java中溶耘,本身就提供了一個接口:Observer,一個子類:Observable服鹅,其中Observer表示【觀察者】凳兵,Observable表示【主題】,可以利用這兩個子類和接口來實現(xiàn)觀察者模式:

public class NewsObservable extends Observable {
    public void update() {
       setChanged();
       notifyObservers();
    }
}
public class People1 implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        System.out.println("小編真無聊");
    }
}
public class People2 implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        System.out.println("開局一張圖企软,內(nèi)容全靠編");
    }
}

客戶端:

  public static void main(String[] args) {
        NewsObservable newsObservable = new NewsObservable();
        newsObservable.addObserver(new People1());
        newsObservable.addObserver(new People2());
        newsObservable.update();
    }

運行結(jié)果:


image.png

在這里庐扫,我不打算詳細(xì)介紹這種實現(xiàn)方式,因為從Java9開始,Java已經(jīng)不推薦這種寫法了形庭,而推薦用消息隊列來實現(xiàn)铅辞。是不是很開心,找到一個借口不去研究Observable萨醒,Observer 這兩個東西了斟珊。

Spring中的事件編程模型

Spring中的事件編程模型就是觀察者模式的實現(xiàn),SpringBoot就利用了Spring的事件編程模型來完成一些操作富纸,這里暫時不表囤踩。

在Spring中定義了一個ApplicationListener接口,從名字就知道它是一個監(jiān)聽器晓褪,是監(jiān)聽Application的事件的堵漱,那么Application又是什么,就是ApplicationContext涣仿,ApplicationContext內(nèi)置了幾個事件勤庐,其中比較容易理解的是:

  • ContextRefreshedEvent
  • ContextStartedEvent
  • ContextStoppedEvent
  • ContextClosedEvent
    從名稱上來看,就知道這幾個事件是什么時候被觸發(fā)的了好港。

下面我演示下具體的用法埃元,比如我想監(jiān)聽ContextRefreshedEvent事件,如果事件發(fā)生了媚狰,就打印一句話岛杀。

@Component
public class MyListener implements ApplicationListener{
    @Override
    public void onApplicationEvent(ApplicationEvent applicationEvent) {
        if(applicationEvent  instanceof ContextRefreshedEvent){
            System.out.println("刷新了");
        }
    }
}
@Configuration
@ComponentScan
public class AppConfig {
}
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
    }

運行結(jié)果:


image.png

當(dāng)時學(xué)習(xí)Spring,看到Spring提供了各式各樣的接口來讓程序員們對Spring進(jìn)行擴(kuò)展崭孤,并且沒有任何侵入性类嗤,我不得不佩服Spring的開發(fā)者們。這里也是辨宠,我們可以看到在客戶端找不到任何關(guān)于“訂閱事件”的影子遗锣。

這種實現(xiàn)方式不是太好,可以看到我們在方法內(nèi)部做了一個判斷:接收到的事件是否為ContextRefreshedEvent嗤形。

偉大的Spring還提供了泛型的ApplicationListener精偿,我們可以通過泛型的ApplicationListener來完善上面的代碼:

@Component
public class MyListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("刷新了");
    }
}

我們還可以利用Spring中的事件編程模型來自定義事件,并且發(fā)布事件:

首先赋兵,我們需要定義一個事件笔咽,來實現(xiàn)ApplicationEvent接口,代表這是一個Application事件霹期,其實上面所說的四個內(nèi)置的事件也實現(xiàn)了ApplicationEvent接口:

public class MyEvent extends ApplicationEvent {
    public MyEvent(Object source) {
        super(source);
    }
}

還需要定義一個監(jiān)聽器叶组,當(dāng)然,在這里需要監(jiān)聽MyEvent事件:

@Component
public class MyListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent event) {
        System.out.println("我訂閱的事件已經(jīng)到達(dá)");
    }
}

現(xiàn)在有了事件历造,也有了監(jiān)聽器甩十,是不是還少了發(fā)布者船庇,不然誰去發(fā)布事件呢?

@Component
public class MyEventPublish implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    public void publish(Object obj) {
        this.publisher.publishEvent(obj);
    }
}

發(fā)布者侣监,需要實現(xiàn)ApplicationEventPublisherAware 接口鸭轮,重寫publish方法,顧名思義橄霉,這就是發(fā)布方法窃爷,那么方法的參數(shù)obj是干嘛的呢,作為發(fā)布者酪劫,應(yīng)該需要知道我要發(fā)布什么事件吞鸭,以及事件來源(是誰觸發(fā)的)把寺董,這個obj就是用來存放這樣的數(shù)據(jù)的覆糟,當(dāng)然,這個參數(shù)需要我們手動傳入進(jìn)去遮咖。setApplicationEventPublisher是Spring內(nèi)部主動調(diào)用的滩字,可以簡單的理解為初始化發(fā)布者。

現(xiàn)在就剩最后一個角色了御吞,監(jiān)聽器有了麦箍,發(fā)布者有了,事件也有了陶珠,對挟裂,沒錯,還少一個觸發(fā)者揍诽,畢竟要有觸發(fā)者去觸發(fā)事件熬魅亍:

@Component
public class Service {

    @Autowired
    private  MyEventPublish publish;

    public void publish() {
        publish.publish(new MyEvent(this));
    }
}

其中publish方法就是給客戶端調(diào)用的,用來觸發(fā)事件暑脆,可以很清楚的看到傳入了new MyEvent(this)渠啤,這樣發(fā)布者就可以知道我要觸發(fā)什么事件和是誰觸發(fā)了事件。

當(dāng)然添吗,還需要把一切交給Spring管理:

@Configuration
@ComponentScan
public class AppConfig {
}

客戶端:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
        context.getBean(Service.class).publish();;
    }

運行結(jié)果:


image.png

這一篇博客比較簡單沥曹,只是簡單的應(yīng)用,但是只有會了應(yīng)用碟联,才能談源碼妓美。

這篇博客到這里就結(jié)束了慧脱,謝謝大家饱亮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市两残,隨后出現(xiàn)的幾起案子裤纹,更是在濱河造成了極大的恐慌委刘,老刑警劉巖丧没,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锡移,居然都是意外死亡呕童,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門淆珊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夺饲,“玉大人,你說我怎么就攤上這事施符⊥” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵戳吝,是天一觀的道長浩销。 經(jīng)常有香客問我,道長听哭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任陆盘,我火速辦了婚禮普筹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隘马。我一直安慰自己太防,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布酸员。 她就那樣靜靜地躺著蜒车,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沸呐。 梳的紋絲不亂的頭發(fā)上醇王,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機(jī)與錄音崭添,去河邊找鬼寓娩。 笑死,一個胖子當(dāng)著我的面吹牛呼渣,可吹牛的內(nèi)容都是我干的棘伴。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屁置,長吁一口氣:“原來是場噩夢啊……” “哼焊夸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蓝角,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阱穗,失蹤者是張志新(化名)和其女友劉穎饭冬,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揪阶,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡昌抠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鲁僚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炊苫。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖冰沙,靈堂內(nèi)的尸體忽然破棺而出侨艾,到底是詐尸還是另有隱情,我是刑警寧澤拓挥,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布唠梨,位于F島的核電站,受9級特大地震影響撞叽,放射性物質(zhì)發(fā)生泄漏姻成。R本人自食惡果不足惜插龄,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一愿棋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧均牢,春花似錦糠雨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至垮庐,卻和暖如春松邪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哨查。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工逗抑, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寒亥。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓邮府,卻偏偏與公主長得像,于是被迫代替她去往敵國和親溉奕。 傳聞我的和親對象是個殘疾皇子褂傀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355