Java設(shè)計模式之觀察者模式
觀察者模式是極其重要的一個設(shè)計模式,也是我?guī)啄觊_發(fā)過程中使用最多的設(shè)計模式,本文首先概述觀察者模式的基本概念和Demo實現(xiàn),接著是觀察者模式在Java和Spring中的應(yīng)用,最后是對觀察者模式的應(yīng)用場景和優(yōu)缺點進行總結(jié)。
一、概念理解
觀察者模式:定義對象之間的一種一對多的依賴關(guān)系,使得每當(dāng)一個對象的狀態(tài)發(fā)生變化時,其相關(guān)的依賴對象都可以得到通知并被自動更新。主要用于多個不同的對象對一個對象的某個方法會做出不同的反應(yīng)!
概念啥意思呢?也就是說,如果使用觀察者模式在A的業(yè)務(wù)邏輯中調(diào)用B的業(yè)務(wù)邏輯,即使B的業(yè)務(wù)邏輯報錯了,仍然不影響A的執(zhí)行。
比如,在我最近公司開發(fā)商城系統(tǒng)的過程中,提交訂單成功以后要刪除購物車中的信息,如果我先寫訂單提交邏輯,接著寫刪除購物車邏輯,這樣當(dāng)然沒有什么問題,但是這樣程序的健壯性太差了。
應(yīng)該將該業(yè)務(wù)分成兩步,一是處理訂單成功處理邏輯,二是刪除購物車中的信息。即使刪除購物車報錯了,提交訂單邏輯仍然不影響。
那應(yīng)該怎么做才能讓他們互不影響呢?需要在購物車對象中要有一個方法用于刪除購物車,還要有一個對象A用于注入(add)購物車對象和通知(notify)購物車執(zhí)行它的方法。
在執(zhí)行時先調(diào)用對象A的add方法將購物車對象添加到對象A中,在訂單提交成功以后,調(diào)用對象A的通知notify購物車方法執(zhí)行清除購物車邏輯。
在觀察者模式中,購物車就稱為觀察者,對象A就稱為目標(biāo)對象。在面向接口編程原則下,觀察者模式應(yīng)該包括四個角色:
1、目標(biāo)接口(subject) :它是一個抽象類,也是所有目標(biāo)對象的父類。它用一個列表記錄當(dāng)前目標(biāo)對象有哪些觀察者對象,并提供增加、刪除觀察者對象和通知觀察者對象的方法聲明。
2、具體目標(biāo)類:可以有多個不同的具體目標(biāo)類,它們同時繼承Subject類。一個目標(biāo)對象就是某個具體目標(biāo)類的對象,一個具體目標(biāo)類負責(zé)定義它自身的事務(wù)邏輯,并在狀態(tài)改變時通知它的所有觀察者對象。
3、觀察者接口(Listener) 它也是一個抽象類,是所有觀察者對象的父類;它為所有的觀察者對象都定義了一個名為update(notify)的方法。當(dāng)目標(biāo)對象的狀態(tài)改變時,它就是通過調(diào)用它的所有觀察者對象的update(notify)方法來通知它們的。
4、具體觀察者類,可以有多個不同的具體觀察者類,它們同時繼承Listener類。一個觀察者對象就是某個具體觀察者類的對象。每個具體觀察者類都要重定義Listener類中定義的update(notify)方法,在該方法中實現(xiàn)它自己的任務(wù)邏輯,當(dāng)它被通知的時候(目標(biāo)對象調(diào)用它的update(notify)方法)就執(zhí)行自己特有的任務(wù)。在我們的例子中是購物車觀察者,當(dāng)然還能有別的,如日志觀察者。
我們基于四個角色實現(xiàn)demo。
二、案例實現(xiàn)
目標(biāo)接口:包括注冊、移除、通知監(jiān)聽者的方法聲明。
/** * 這是被觀察的對象 * 目標(biāo)類 * @author tcy * @Date 17-09-2022 */ public interface SubjectAbstract<T> { //注冊監(jiān)聽者 public void registerListener(T t); //移除監(jiān)聽者 public void removeListener(T t); //通知監(jiān)聽者 public void notifyListener(); }
目標(biāo)接口實現(xiàn):里面需要一個listenerList數(shù)組存儲所有的觀察者,需要定義add和remove觀察者的方法,需要給出notify方法通知所有的觀察者對象。
/** * * 具體目標(biāo)類 * @author tcy * @Date 17-09-2022 */ public class SubjectImpl implements SubjectAbstract<ListenerAbstract> { //監(jiān)聽者的注冊列表 private List<ListenerAbstract> listenerList = new ArrayList<>(); @Override public void registerListener(ListenerAbstract myListener) { listenerList.add(myListener); } @Override public void removeListener(ListenerAbstract myListener) { listenerList.remove(myListener); } @Override public void notifyListener() { for (ListenerAbstract myListener : listenerList) { System.out.println("收到推送事件,開始調(diào)用異步邏輯..."); myListener.onEvent(); } } }
觀察者接口:聲明響應(yīng)方法
/** * * 觀察者-接口 * @author tcy * @Date 17-09-2022 */ public interface ListenerAbstract { void onEvent(); }
觀察者接口:實現(xiàn)響應(yīng)方法,處理清除購物車的邏輯。
/** * 具體觀察者類 購物車 * @author tcy * @Date 17-09-2022 */ public class ListenerMyShopCart implements ListenerAbstract { @Override public void onEvent() { //...省略購物車處理邏輯 System.out.println("刪除購物車中的信息..."); } }
我們使用Client模擬提交訂單操作。
/** * 先使用具體目標(biāo)對象的registerListener方法添加具體觀察者對象, * 然后調(diào)用其notify方法通知觀察者 * @author tcy * @Date 17-09-2022 */ public class Client { public static void main(String[] args) { System.out.println("訂單成功處理邏輯..."); //創(chuàng)建目標(biāo)對象 SubjectImpl subject=new SubjectImpl(); //具體觀察者注冊入 目標(biāo)對象 ListenerMyShopCart shopCart=new ListenerMyShopCart(); //向觀察者中注冊listener subject.registerListener(shopCart); //發(fā)布事件,通知觀察者 subject.notifyListener(); } }
這樣就實現(xiàn)了訂單的處理邏輯和購物車的邏輯解耦,即使購物車邏輯報錯也不會影響訂單處理邏輯。
既然觀察者模式是很常用的模式,而且抽象觀察者和抽象目標(biāo)類方法聲明都是固定的,作為高級語言Java,Java設(shè)計者干脆內(nèi)置兩個接口,開發(fā)者直接實現(xiàn)接口就能使用觀察者模式。
三、Java中的觀察者模式
在 Java 中,通過 java.util.Observable 類和 java.util.Observer 接口定義觀察者模式,只要實現(xiàn)它們的子類就可以編寫觀察者模式實例。
Observable 類是抽象目標(biāo)類,它有一個 Vector 向量,用于保存所有要通知的觀察者對象,下面來介紹它最重要的 3 個方法。
- void addObserver(Observer o) 方法:用于將新的觀察者對象添加到向量中。
- void notifyObservers(Object arg) 方法:調(diào)用向量中的所有觀察者對象的 update() 方法,通知它們數(shù)據(jù)發(fā)生改變。通常越晚加入向量的觀察者越先得到通知。
- void setChange() 方法:用來設(shè)置一個 boolean 類型的內(nèi)部標(biāo)志位,注明目標(biāo)對象發(fā)生了變化。當(dāng)它為真時,notifyObservers() 才會通知觀察者。
Observer 接口是抽象觀察者,它監(jiān)視目標(biāo)對象的變化,當(dāng)目標(biāo)對象發(fā)生變化時,觀察者得到通知,并調(diào)用 void update(Observable o,Object arg) 方法,進行相應(yīng)的工作。
我們基于Java的兩個接口,改造我們的案例。
具體目標(biāo)類:
/** * 具體目標(biāo)類 * @author tcy * @Date 19-09-2022 */ public class SubjectObservable extends Observable { public void notifyListener() { super.setChanged(); System.out.println("收到推送的消息..."); super.notifyObservers(); //通知觀察者購物車事件 } }
具體觀察者類:
/** * 觀察者實現(xiàn)類 * @author tcy * @Date 19-09-2022 */ public class ShopCartObserver implements Observer { @Override public void update(Observable o, Object arg) { System.out.println("清除購物車..."); } }
依舊是Client模擬訂單處理邏輯。
/** * @author tcy * @Date 19-09-2022 */ public class Client { public static void main(String[] args) { System.out.println("訂單提交成功..."); SubjectObservable observable = new SubjectObservable(); Observer shopCartObserver = new ShopCartObserver(); //購物車 observable.addObserver(shopCartObserver); observable.notifyListener(); } }
這樣也能實現(xiàn)觀察者邏輯,但Java中的觀察者模式有一定的局限性。
Observable是個類,而不是一個接口,沒有實現(xiàn)Serializable,所以,不能序列化和它的子類,而且他是線程不安全的,無法保證觀察者的執(zhí)行順序。在JDK9之后已經(jīng)啟用了。
寫Java的恐怕沒有不用Spring的了,作為優(yōu)秀的開源框架,Spring中也有觀察者模式的大量應(yīng)用,而且Spring是在java的基礎(chǔ)之上改造的,很好的規(guī)避了Java觀察者模式的不足之處。
四、Spring如何使用觀察者模式
在第一章節(jié)典型的觀察者模式中包含四個角色:目標(biāo)類、目標(biāo)類實現(xiàn)、觀察者、觀察者實現(xiàn)類。而在Spring下的觀察者模式略有不同,Spring對其做了部分改造。
事件:
Spring中定義最頂層的事件ApplicationEvent,這個接口最終還是繼承了EventObject接口。
只是在基礎(chǔ)之上增加了構(gòu)造和獲取當(dāng)前時間戳的方法,Spring所有的事件都要實現(xiàn)這個接口,比如Spring中內(nèi)置的ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent...看名字大概就知道這些事件用于哪些地方,分別是容器刷新后、開始時、停止時...
目標(biāo)類接口:
Spirng中的ApplicationEventMulticaster接口就是實例中目標(biāo)類,我們可以對比我們的目標(biāo)接口和ApplicationEventMulticaster接口,長的非常像。
觀察者接口:
觀察者ApplicationListener用于監(jiān)聽事件,只有一個方法onApplicationEvent事件發(fā)生后該事件執(zhí)行。與我們樣例中的抽象觀察者并無太大的不同。
目標(biāo)類實現(xiàn):
在我們案例中目標(biāo)類的職責(zé)直接在一個類中實現(xiàn),注冊監(jiān)聽器、廣播事件(調(diào)用監(jiān)聽器方法)。
在Spring中兩個實現(xiàn)類分別拆分開來,Spring啟動過程中會調(diào)用registerListeners()方法,看名字我們大概就已經(jīng)知道是注冊所有的監(jiān)聽器,該方法完成原目標(biāo)類的注冊監(jiān)聽器職責(zé)。
在Spring中事件源ApplicationContext用于廣播事件,用戶不必再顯示的調(diào)用監(jiān)聽器的方法,交給Spring調(diào)用,該方法完成原目標(biāo)類的廣播事件職責(zé)。
我們基于Spring的觀察者模式繼續(xù)改造我們的案例。
購物車事件:
/** * 購物車事件 * @author tcy * @Date 19-09-2022 */ @Component public class EventShopCart extends ApplicationEvent { private String orderId; public EventShopCart(Object source, String orderId) { super(source); this.orderId=orderId; } public EventShopCart() { super(1); } }
發(fā)布者(模擬Spring調(diào)用監(jiān)聽器的方法,實際開發(fā)不需要寫):
/** * 發(fā)布者 * @author tcy * @Date 19-09-2022 */ @Component public class MyPublisher implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * 發(fā)布事件 * 監(jiān)聽該事件的監(jiān)聽者都可以獲取消息 * * @param myEvent */ public void workEvent(EventShopCart myEvent) { //該方法會調(diào)用監(jiān)聽器實現(xiàn)的方法 applicationContext.publishEvent(myEvent); } }
監(jiān)聽者:
/** * 監(jiān)聽者 * @author tcy * @Date 19-09-2022 */ @Component public class ListenerShopCart implements ApplicationListener<EventShopCart> { @Override public void onApplicationEvent(EventShopCart myEvent) { System.out.println("清除購物車成功..."); } }
Client模擬調(diào)用:
/** * @author tcy * @Date 19-09-2022 */ public class Client { public static void main(String[] args) { ApplicationContext ac =new AnnotationConfigApplicationContext("cn.sky1998.behavior.observer.spring"); System.out.println("訂單提交成功..."); MyPublisher bean = ac.getBean(MyPublisher.class); EventShopCart myEvent = ac.getBean(EventShopCart.class); bean.workEvent(myEvent); } }
通過Spring實現(xiàn)觀察者模式比我們手動寫簡單的多。
使用Spring實現(xiàn)觀察者模式時,觀察者接口、目標(biāo)接口、目標(biāo)實現(xiàn),我們都不需要管,只負責(zé)繼承ApplicationEvent類定義我們自己的事件,并實現(xiàn)ApplicationListener<自定義事件>接口實現(xiàn)我們的觀察者,并在對應(yīng)的業(yè)務(wù)中調(diào)用applicationContext.publishEvent(new ShopCartEvent(cmOrderItemList)),即實現(xiàn)了觀察者模式。
讀者可以拉取完整代碼本地學(xué)習(xí),實現(xiàn)代碼均測試通過上傳到碼云,本地源碼下載。
五、總結(jié)
Spring使用觀察者模式我在很久之前就使用過,但是并不清楚為什么要這樣寫,學(xué)了觀察者模式以后,寫起來變得通透多了。
雖然觀察者模式的概念是:一對多的依賴關(guān)系,但不一定觀察者有多個才能使用,我們的例子都是使用的一個觀察者。
它很好的降低了目標(biāo)與觀察者之間的耦合關(guān)系,目標(biāo)與觀察者建立一套觸發(fā)機制,也讓他成為了最常見的設(shè)計模式。
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
相關(guān)文章
Windows中在IDEA上安裝和使用JetBrains Mono字體的教程
這篇文章主要介紹了Windows IDEA上安裝和使用JetBrains Mono字體的教程,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03java數(shù)據(jù)結(jié)構(gòu)與算法之簡單選擇排序詳解
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)與算法之簡單選擇排序,結(jié)合實例形式分析了選擇排序的原理、實現(xiàn)方法與相關(guān)操作技巧,需要的朋友可以參考下2017-05-05Java throw Exception實現(xiàn)異常轉(zhuǎn)換
這篇文章主要介紹了Java throw Exception實現(xiàn)異常轉(zhuǎn)換,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04關(guān)于Lombok @Data注解:簡化Java代碼的魔法棒
Lombok庫通過@Data注解自動生成常見的樣板代碼如getter、setter、toString等,極大減少代碼量,提高開發(fā)效率,@Data注解集成了@ToString、@EqualsAndHashCode、@Getter、@Setter、@RequiredArgsConstructor等注解的功能2024-10-10Java運行時數(shù)據(jù)區(qū)域(內(nèi)存劃分)的深入講解
聽說Java運行時環(huán)境的內(nèi)存劃分是挺進BAT的必經(jīng)之路,這篇文章主要給大家介紹了關(guān)于Java運行時數(shù)據(jù)區(qū)域(內(nèi)存劃分)的相關(guān)資料,需要的朋友可以參考下2021-06-06使用mybatis-plus分頁出現(xiàn)兩個Limit的問題解決
在使用MyBatis-Plus進行分頁查詢時,可能會遇到查詢SQL中出現(xiàn)兩個limit語句的問題,這通常是由于在多個模塊中重復(fù)引入了MyBatis-Plus的分頁插件所導(dǎo)致的,下面就來介紹一下如何解決,感興趣的可以了解一下2024-10-10