文章收錄在 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 中觀察者模式的四個角色
-
事件:ApplicationEvent 是所有事件物件的父類。ApplicationEvent 繼承自 jdk 的 EventObject, 所有的事件都需要繼承 ApplicationEvent, 並且通過 source 得到事件源。
Spring 也為我們提供了很多內建事件,
ContextRefreshedEvent
、ContextStartedEvent
、ContextStoppedEvent
、ContextClosedEvent
、RequestHandledEvent
。 - 事件監聽:ApplicationListener,也就是觀察者,繼承自 jdk 的 EventListener,該類中只有一個方法 onApplicationEvent。當監聽的事件發生後該方法會被執行。
-
事件源:ApplicationContext,
ApplicationContext
是 Spring 中的核心容器,在事件監聽中 ApplicationContext 可以作為事件的釋出者,也就是事件源。因為 ApplicationContext 繼承自 ApplicationEventPublisher。在ApplicationEventPublisher
中定義了事件釋出的方法:publishEvent(Object event)
- 事件管理: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萬條了,就通知觀察者重新寫到一個新的檔案。
大佬可能覺這麼實現有點費勁,不用設計模式也好,或者用訊息佇列也好,其實都只是一種手段,選擇適合自己業務的,開心就好。