javascript
三种方式实现观察者模式 及 Spring中的事件编程模型
觀察者模式可以說(shuō)是眾多設(shè)計(jì)模式中,最容易理解的設(shè)計(jì)模式之一了,觀察者模式在Spring中也隨處可見(jiàn),面試的時(shí)候,面試官可能會(huì)問(wèn),嘿,你既然讀過(guò)Spring源碼,那你說(shuō)說(shuō)Spring中運(yùn)用的設(shè)計(jì)模式吧,你可以自信的告訴他,Spring中的ApplicationListener就運(yùn)用了觀察者模式。
讓我們一步一步來(lái),首先我們要知道到底什么是觀察者模式,用Java是如何實(shí)現(xiàn)的,在這里,我將會(huì)用三種方式來(lái)實(shí)現(xiàn)觀察者模式。
什么是觀察者模式
在現(xiàn)實(shí)生活中,觀察者模式處處可見(jiàn),比如
-
看新聞,只要新聞開(kāi)始播放了,就會(huì)把新聞推送給訂閱了新聞的用戶,在這里,新聞就是【被觀察者】,而用戶就是【觀察者】。
-
微信公眾號(hào),如果一個(gè)用戶訂閱了某個(gè)公眾號(hào),那么便會(huì)收到公眾號(hào)發(fā)來(lái)的消息,那么,公眾號(hào)就是【被觀察者】,而用戶就是【觀察者】。
-
熱水器,假設(shè)熱水器由三部分組成,熱水器,警報(bào)器,顯示器,熱水器僅僅負(fù)責(zé)燒水,當(dāng)水溫到達(dá)設(shè)定的溫度后,通知警報(bào)器,警報(bào)器發(fā)出警報(bào),顯示器也需要訂閱熱水器的燒水事件,從而獲得水溫,并顯示。熱水器就是【被觀察者】,警報(bào)器,顯示器就是【觀察者】。
在這里,可以看到,【觀察者】已經(jīng)失去自主的權(quán)利,只能被動(dòng)的接收來(lái)自【被觀察者】的事件,無(wú)法主動(dòng)觀察。【觀察者】成為了“受”,而【被觀察者】成為了“攻”。【被觀察者】只是通知【觀察者】,不關(guān)心【觀察者】收到通知后,會(huì)執(zhí)行怎樣的動(dòng)作。
而在設(shè)計(jì)模式中,又把【被觀察者】稱為【主題】。
在觀察者設(shè)計(jì)模式中,一般有四個(gè)角色:
- 抽象主題角色(Subject)
- 具體主題角色(ConcreteSubject)
- 抽象觀察者角色(Observer)
- 具體觀察者角色(ConcreteObserver)
其中,【主題】需要有一個(gè)列表字段,用來(lái)保存【觀察者】的引用,提供兩個(gè)方法(虛方法),即【刪除觀察者】【增加觀察者】,還需要提供一個(gè)給客戶端調(diào)用的方法,通知各個(gè)【觀察者】:你們關(guān)心(訂閱)的事件已經(jīng)推送給你們了。
下面,我就用三種方式來(lái)實(shí)現(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;} } 復(fù)制代碼此類不屬于觀察者模式必須的類,用來(lái)存放事件的信息。
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(); } 復(fù)制代碼抽象主題角色,在這個(gè)角色中,有一個(gè)字段peopleList,用來(lái)保存【觀察者】的引用,同時(shí)定義了兩個(gè)接口,這是Java8默認(rèn)接口實(shí)現(xiàn)的寫(xiě)法。這兩個(gè)接口是給客戶端調(diào)用的,用來(lái)【刪除觀察者】【增加觀察者】,還提供一個(gè)方法,此方法需要被【具體主題角色】重寫(xiě),用來(lái)通知各個(gè)【觀察者】。
public class NewsSubject implements Subject{public void update() {for (People people : peopleList) {News news = new News();news.setContent("今日在大街上,有人躲在草叢中襲擊路人,還大喊“德瑪西亞萬(wàn)歲”");news.setTitle("德瑪西亞出現(xiàn)了");people.update(news);}} } 復(fù)制代碼具體主題角色,重寫(xiě)了【抽象主題角色】的方法,循環(huán)列表,通知各個(gè)【觀察者】。
public interface People {void update(News news); } 復(fù)制代碼抽象觀察者角色,定義了一個(gè)接口,【具體觀察者角色】需要重寫(xiě)這個(gè)方法。
下面就是【具體觀察者角色】了:
public class PeopleA implements People {@Overridepublic void update(News news) {System.out.println("這個(gè)新聞?wù)婧每?#34;);} } 復(fù)制代碼public class PeopleB implements People {@Overridepublic void update(News news) {System.out.println("這個(gè)新聞?wù)鏌o(wú)語(yǔ)");} } 復(fù)制代碼public class PeopleC implements People {@Overridepublic void update(News news) {System.out.println("這個(gè)新聞?wù)娑?#34;);} } 復(fù)制代碼客戶端:
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();} } 復(fù)制代碼運(yùn)行:
我們學(xué)習(xí)設(shè)計(jì)模式,必須知道設(shè)計(jì)模式的優(yōu)缺點(diǎn),那么觀察者設(shè)計(jì)模式的優(yōu)缺點(diǎn)是什么呢?
優(yōu)點(diǎn):
-
【主題】和【觀察者】通過(guò)抽象,建立了一個(gè)松耦合的關(guān)系,【主題】只知道當(dāng)前有哪些【觀察者】,并且發(fā)送通知,但是不知道【觀察者】具體會(huì)執(zhí)行怎樣的動(dòng)作。這也很好理解,比如 微信公眾號(hào)推送了一個(gè)消息過(guò)來(lái),它不知道你會(huì)采取如何的動(dòng)作,是 微笑的打開(kāi),還是憤怒的打開(kāi),或者是直接把消息刪了,又或者把手機(jī)扔到洗衣機(jī)洗刷刷。
-
符合開(kāi)閉原則,如果需要新增一個(gè)【觀察者】,只需要寫(xiě)一個(gè)類去實(shí)現(xiàn)【抽象觀察者角色】即可,不需要改動(dòng)原來(lái)的代碼。
缺點(diǎn):
-
客戶端必須知道所有的【觀察者】,并且進(jìn)行【增加觀察者】和【刪除觀察者】的操作。
-
如果有很多【觀察者】,那么所有的【觀察者】收到通知,可能需要花費(fèi)很久時(shí)間。
當(dāng)然以上優(yōu)缺點(diǎn),是最直觀的,可以很容易理解,并且體會(huì)到的。其他優(yōu)缺點(diǎn),可以自行百度。
Lambda
在介紹這種寫(xiě)法之前,有必要介紹下***函數(shù)式接口***,函數(shù)式接口的概念由來(lái)已久,一般來(lái)說(shuō)只定義了一個(gè)虛方法的接口就叫函數(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("昨天就看過(guò)了"));subject.update();} 復(fù)制代碼運(yùn)行結(jié)果:
利用Lambda表達(dá)式和函數(shù)式接口,可以省去【具體觀察者角色】的定義,但是個(gè)人認(rèn)為,這并非屬于嚴(yán)格意義上的觀察者模式,而且弊端很明顯:
- 客戶端需要知道觀察者的具體實(shí)現(xiàn)。
- 如果觀察者的具體實(shí)現(xiàn)比較復(fù)雜,可能代碼并沒(méi)有那么清晰。
所以這種寫(xiě)法,具有一定的局限性。
借用大神的一句話
設(shè)計(jì)模式的出現(xiàn),是為了彌補(bǔ)語(yǔ)言的缺陷。
正是由于語(yǔ)言的升級(jí),讓某些設(shè)計(jì)模式發(fā)生了一定的變化,除了觀察者模式,還有模板方法模式、責(zé)任鏈模式等,都由于 Lambda表達(dá)式的出現(xiàn),而出現(xiàn)了一些變化。
JDK
在Java中,本身就提供了一個(gè)接口:Observer,一個(gè)子類:Observable,其中Observer表示【觀察者】,Observable表示【主題】,可以利用這兩個(gè)子類和接口來(lái)實(shí)現(xiàn)觀察者模式:
public class NewsObservable extends Observable {public void update() {setChanged();notifyObservers();} } 復(fù)制代碼public class People1 implements Observer {@Overridepublic void update(Observable o, Object arg) {System.out.println("小編真無(wú)聊");} } 復(fù)制代碼public class People2 implements Observer {@Overridepublic void update(Observable o, Object arg) {System.out.println("開(kāi)局一張圖,內(nèi)容全靠編");} } 復(fù)制代碼客戶端:
public static void main(String[] args) {NewsObservable newsObservable = new NewsObservable();newsObservable.addObserver(new People1());newsObservable.addObserver(new People2());newsObservable.update();} 復(fù)制代碼運(yùn)行結(jié)果:
在這里,我不打算詳細(xì)介紹這種實(shí)現(xiàn)方式,因?yàn)閺腏ava9開(kāi)始,Java已經(jīng)不推薦這種寫(xiě)法了,而推薦用消息隊(duì)列來(lái)實(shí)現(xiàn)。是不是很開(kāi)心,找到一個(gè)借口不去研究Observable,Observer 這兩個(gè)東西了。
Spring中的事件編程模型
Spring中的事件編程模型就是觀察者模式的實(shí)現(xiàn),SpringBoot就利用了Spring的事件編程模型來(lái)完成一些操作,這里暫時(shí)不表。
在Spring中定義了一個(gè)ApplicationListener接口,從名字就知道它是一個(gè)監(jiān)聽(tīng)器,是監(jiān)聽(tīng)Application的事件的,那么Application又是什么,就是ApplicationContext,ApplicationContext內(nèi)置了幾個(gè)事件,其中比較容易理解的是:
- ContextRefreshedEvent
- ContextStartedEvent
- ContextStoppedEvent
- ContextClosedEvent 從名稱上來(lái)看,就知道這幾個(gè)事件是什么時(shí)候被觸發(fā)的了。
下面我演示下具體的用法,比如我想監(jiān)聽(tīng)ContextRefreshedEvent事件,如果事件發(fā)生了,就打印一句話。
@Component public class MyListener implements ApplicationListener{@Overridepublic void onApplicationEvent(ApplicationEvent applicationEvent) {if(applicationEvent instanceof ContextRefreshedEvent){System.out.println("刷新了");}} } 復(fù)制代碼@Configuration @ComponentScan public class AppConfig { } 復(fù)制代碼 public static void main(String[] args) {AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);} 復(fù)制代碼運(yùn)行結(jié)果:
當(dāng)時(shí)學(xué)習(xí)Spring,看到Spring提供了各式各樣的接口來(lái)讓程序員們對(duì)Spring進(jìn)行擴(kuò)展,并且沒(méi)有任何侵入性,我不得不佩服Spring的開(kāi)發(fā)者們。這里也是,我們可以看到在客戶端找不到任何關(guān)于“訂閱事件”的影子。
這種實(shí)現(xiàn)方式不是太好,可以看到我們?cè)诜椒▋?nèi)部做了一個(gè)判斷:接收到的事件是否為ContextRefreshedEvent。
偉大的Spring還提供了泛型的ApplicationListener,我們可以通過(guò)泛型的ApplicationListener來(lái)完善上面的代碼:
@Component public class MyListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {System.out.println("刷新了");} } 復(fù)制代碼我們還可以利用Spring中的事件編程模型來(lái)自定義事件,并且發(fā)布事件:
首先,我們需要定義一個(gè)事件,來(lái)實(shí)現(xiàn)ApplicationEvent接口,代表這是一個(gè)Application事件,其實(shí)上面所說(shuō)的四個(gè)內(nèi)置的事件也實(shí)現(xiàn)了ApplicationEvent接口:
public class MyEvent extends ApplicationEvent {public MyEvent(Object source) {super(source);} } 復(fù)制代碼還需要定義一個(gè)監(jiān)聽(tīng)器,當(dāng)然,在這里需要監(jiān)聽(tīng)MyEvent事件:
@Component public class MyListener implements ApplicationListener<MyEvent> {@Overridepublic void onApplicationEvent(MyEvent event) {System.out.println("我訂閱的事件已經(jīng)到達(dá)");} } 復(fù)制代碼現(xiàn)在有了事件,也有了監(jiān)聽(tīng)器,是不是還少了發(fā)布者,不然誰(shuí)去發(fā)布事件呢?
@Component public class MyEventPublish implements ApplicationEventPublisherAware {private ApplicationEventPublisher publisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.publisher = applicationEventPublisher;}public void publish(Object obj) {this.publisher.publishEvent(obj);} } 復(fù)制代碼發(fā)布者,需要實(shí)現(xiàn)ApplicationEventPublisherAware 接口,重寫(xiě)publish方法,顧名思義,這就是發(fā)布方法,那么方法的參數(shù)obj是干嘛的呢,作為發(fā)布者,應(yīng)該需要知道我要發(fā)布什么事件,以及事件來(lái)源(是誰(shuí)觸發(fā)的)把,這個(gè)obj就是用來(lái)存放這樣的數(shù)據(jù)的,當(dāng)然,這個(gè)參數(shù)需要我們手動(dòng)傳入進(jìn)去。setApplicationEventPublisher是Spring內(nèi)部主動(dòng)調(diào)用的,可以簡(jiǎn)單的理解為初始化發(fā)布者。
現(xiàn)在就剩最后一個(gè)角色了,監(jiān)聽(tīng)器有了,發(fā)布者有了,事件也有了,對(duì),沒(méi)錯(cuò),還少一個(gè)觸發(fā)者,畢竟要有觸發(fā)者去觸發(fā)事件啊:
@Component public class Service {@Autowiredprivate MyEventPublish publish;public void publish() {publish.publish(new MyEvent(this));} } 復(fù)制代碼其中publish方法就是給客戶端調(diào)用的,用來(lái)觸發(fā)事件,可以很清楚的看到傳入了new MyEvent(this),這樣發(fā)布者就可以知道我要觸發(fā)什么事件和是誰(shuí)觸發(fā)了事件。
當(dāng)然,還需要把一切交給Spring管理:
@Configuration @ComponentScan public class AppConfig { } 復(fù)制代碼客戶端:
public static void main(String[] args) {AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);context.getBean(Service.class).publish();;} 復(fù)制代碼運(yùn)行結(jié)果:
這一篇博客比較簡(jiǎn)單,只是簡(jiǎn)單的應(yīng)用,但是只有會(huì)了應(yīng)用,才能談源碼。
這篇博客到這里就結(jié)束了,謝謝大家。
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的三种方式实现观察者模式 及 Spring中的事件编程模型的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一个“Internal”牵扯出的代码泄露
- 下一篇: 《人月神话》阅读笔记2