觀察者模式可以說是眾多設計模式中,最容易理解的設計模式之一了,觀察者模式在Spring中也隨處可見,面試的時候,面試官可能會問,嘿,你既然讀過Spring原始碼,那你說說Spring中運用的設計模式吧,你可以自信的告訴他,Spring中的ApplicationListener就運用了觀察者模式。
讓我們一步一步來,首先我們要知道到底什麼是觀察者模式,用Java是如何實現的,在這裡,我將會用三種方式來實現觀察者模式。
什麼是觀察者模式
在現實生活中,觀察者模式處處可見,比如
-
看新聞,只要新聞開始播放了,就會把新聞推送給訂閱了新聞的使用者,在這裡,新聞就是【被觀察者】,而使用者就是【觀察者】。
-
微信公眾號,如果一個使用者訂閱了某個公眾號,那麼便會收到公眾號發來的訊息,那麼,公眾號就是【被觀察者】,而使用者就是【觀察者】。
-
熱水器,假設熱水器由三部分組成,熱水器,警報器,顯示器,熱水器僅僅負責燒水,當水溫到達設定的溫度後,通知警報器,警報器發出警報,顯示器也需要訂閱熱水器的燒水事件,從而獲得水溫,並顯示。熱水器就是【被觀察者】,警報器,顯示器就是【觀察者】。
在這裡,可以看到,【觀察者】已經失去自主的權利,只能被動的接收來自【被觀察者】的事件,無法主動觀察。【觀察者】成為了“受”,而【被觀察者】成為了“攻”。【被觀察者】只是通知【觀察者】,不關心【觀察者】收到通知後,會執行怎樣的動作。
而在設計模式中,又把【被觀察者】稱為【主題】。
在觀察者設計模式中,一般有四個角色:
- 抽象主題角色(Subject)
- 具體主題角色(ConcreteSubject)
- 抽象觀察者角色(Observer)
- 具體觀察者角色(ConcreteObserver)
其中,【主題】需要有一個列表欄位,用來儲存【觀察者】的引用,提供兩個方法(虛方法),即【刪除觀察者】【增加觀察者】,還需要提供一個給客戶端呼叫的方法,通知各個【觀察者】:你們關心(訂閱)的事件已經推送給你們了。
下面,我就用三種方式來實現觀察者模式。
經典
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預設介面實現的寫法。這兩個介面是給客戶端呼叫的,用來【刪除觀察者】【增加觀察者】,還提供一個方法,此方法需要被【具體主題角色】重寫,用來通知各個【觀察者】。
public class NewsSubject implements Subject{
public void update() {
for (People people : peopleList) {
News news = new News();
news.setContent("今日在大街上,有人躲在草叢中襲擊路人,還大喊“德瑪西亞萬歲”");
news.setTitle("德瑪西亞出現了");
people.update(news);
}
}
}
複製程式碼
具體主題角色,重寫了【抽象主題角色】的方法,迴圈列表,通知各個【觀察者】。
public interface People {
void update(News news);
}
複製程式碼
抽象觀察者角色,定義了一個介面,【具體觀察者角色】需要重寫這個方法。
下面就是【具體觀察者角色】了:
public class PeopleA implements People {
@Override
public void update(News news) {
System.out.println("這個新聞真好看");
}
}
複製程式碼
public class PeopleB implements People {
@Override
public void update(News news) {
System.out.println("這個新聞真無語");
}
}
複製程式碼
public class PeopleC implements People {
@Override
public void update(News news) {
System.out.println("這個新聞真逗");
}
}
複製程式碼
客戶端:
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();
}
}
複製程式碼
執行:
我們學習設計模式,必須知道設計模式的優缺點,那麼觀察者設計模式的優缺點是什麼呢?
優點:
-
【主題】和【觀察者】通過抽象,建立了一個鬆耦合的關係,【主題】只知道當前有哪些【觀察者】,並且傳送通知,但是不知道【觀察者】具體會執行怎樣的動作。這也很好理解,比如 微信公眾號推送了一個訊息過來,它不知道你會採取如何的動作,是 微笑的開啟,還是憤怒的開啟,或者是直接把訊息刪了,又或者把手機扔到洗衣機洗刷刷。
-
符合開閉原則,如果需要新增一個【觀察者】,只需要寫一個類去實現【抽象觀察者角色】即可,不需要改動原來的程式碼。
缺點:
-
客戶端必須知道所有的【觀察者】,並且進行【增加觀察者】和【刪除觀察者】的操作。
-
如果有很多【觀察者】,那麼所有的【觀察者】收到通知,可能需要花費很久時間。
當然以上優缺點,是最直觀的,可以很容易理解,並且體會到的。其他優缺點,可以自行百度。
Lambda
在介紹這種寫法之前,有必要介紹下函式式介面,函式式介面的概念由來已久,一般來說只定義了一個虛方法的介面就叫函式式介面,在Java8中,由於Lambda表示式的出現,讓函式式介面大放異彩。
我們僅僅需要修改客戶端的程式碼就可以:
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();
}
複製程式碼
執行結果:
利用Lambda表示式和函式式介面,可以省去【具體觀察者角色】的定義,但是個人認為,這並非屬於嚴格意義上的觀察者模式,而且弊端很明顯:
- 客戶端需要知道觀察者的具體實現。
- 如果觀察者的具體實現比較複雜,可能程式碼並沒有那麼清晰。
所以這種寫法,具有一定的侷限性。
借用大神的一句話
設計模式的出現,是為了彌補語言的缺陷。
正是由於語言的升級,讓某些設計模式發生了一定的變化,除了觀察者模式,還有模板方法模式、責任鏈模式等,都由於 Lambda表示式的出現,而出現了一些變化。
JDK
在Java中,本身就提供了一個介面:Observer,一個子類:Observable,其中Observer表示【觀察者】,Observable表示【主題】,可以利用這兩個子類和介面來實現觀察者模式:
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("開局一張圖,內容全靠編");
}
}
複製程式碼
客戶端:
public static void main(String[] args) {
NewsObservable newsObservable = new NewsObservable();
newsObservable.addObserver(new People1());
newsObservable.addObserver(new People2());
newsObservable.update();
}
複製程式碼
執行結果:
在這裡,我不打算詳細介紹這種實現方式,因為從Java9開始,Java已經不推薦這種寫法了,而推薦用訊息佇列來實現。是不是很開心,找到一個藉口不去研究Observable,Observer 這兩個東西了。
Spring中的事件程式設計模型
Spring中的事件程式設計模型就是觀察者模式的實現,SpringBoot就利用了Spring的事件程式設計模型來完成一些操作,這裡暫時不表。
在Spring中定義了一個ApplicationListener介面,從名字就知道它是一個監聽器,是監聽Application的事件的,那麼Application又是什麼,就是ApplicationContext,ApplicationContext內建了幾個事件,其中比較容易理解的是:
- ContextRefreshedEvent
- ContextStartedEvent
- ContextStoppedEvent
- ContextClosedEvent
從名稱上來看,就知道這幾個事件是什麼時候被觸發的了。
下面我演示下具體的用法,比如我想監聽ContextRefreshedEvent事件,如果事件發生了,就列印一句話。
@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);
}
複製程式碼
執行結果:
當時學習Spring,看到Spring提供了各式各樣的介面來讓程式設計師們對Spring進行擴充套件,並且沒有任何侵入性,我不得不佩服Spring的開發者們。這裡也是,我們可以看到在客戶端找不到任何關於“訂閱事件”的影子。
這種實現方式不是太好,可以看到我們在方法內部做了一個判斷:接收到的事件是否為ContextRefreshedEvent。
偉大的Spring還提供了泛型的ApplicationListener,我們可以通過泛型的ApplicationListener來完善上面的程式碼:
@Component
public class MyListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("重新整理了");
}
}
複製程式碼
我們還可以利用Spring中的事件程式設計模型來自定義事件,並且釋出事件:
首先,我們需要定義一個事件,來實現ApplicationEvent介面,代表這是一個Application事件,其實上面所說的四個內建的事件也實現了ApplicationEvent介面:
public class MyEvent extends ApplicationEvent {
public MyEvent(Object source) {
super(source);
}
}
複製程式碼
還需要定義一個監聽器,當然,在這裡需要監聽MyEvent事件:
@Component
public class MyListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
System.out.println("我訂閱的事件已經到達");
}
}
複製程式碼
現在有了事件,也有了監聽器,是不是還少了釋出者,不然誰去釋出事件呢?
@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);
}
}
複製程式碼
釋出者,需要實現ApplicationEventPublisherAware 介面,重寫publish方法,顧名思義,這就是釋出方法,那麼方法的引數obj是幹嘛的呢,作為釋出者,應該需要知道我要釋出什麼事件,以及事件來源(是誰觸發的)把,這個obj就是用來存放這樣的資料的,當然,這個引數需要我們手動傳入進去。setApplicationEventPublisher是Spring內部主動呼叫的,可以簡單的理解為初始化釋出者。
現在就剩最後一個角色了,監聽器有了,釋出者有了,事件也有了,對,沒錯,還少一個觸發者,畢竟要有觸發者去觸發事件啊:
@Component
public class Service {
@Autowired
private MyEventPublish publish;
public void publish() {
publish.publish(new MyEvent(this));
}
}
複製程式碼
其中publish方法就是給客戶端呼叫的,用來觸發事件,可以很清楚的看到傳入了new MyEvent(this),這樣釋出者就可以知道我要觸發什麼事件和是誰觸發了事件。
當然,還需要把一切交給Spring管理:
@Configuration
@ComponentScan
public class AppConfig {
}
複製程式碼
客戶端:
public static void main(String[] args) {
AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(Service.class).publish();;
}
複製程式碼
執行結果:
這一篇部落格比較簡單,只是簡單的應用,但是隻有會了應用,才能談原始碼。
這篇部落格到這裡就結束了,謝謝大家。