JAVA設計模式之觀察者模式

前言

本系列文章參考《設計模式之禪》、菜鳥教程網(wǎng)以及網(wǎng)上的一些文章進行歸納總結(jié)弛矛,并結(jié)合自身開發(fā)應用杠园。設計模式的命名以《設計模式之禪》為準。

設計模式僅是一些開發(fā)者在日常開發(fā)中的編碼技巧的匯總并非固定不變筒狠,可根據(jù)項目業(yè)務實際情況進行擴展和應用猪狈,切不可被這個束縛。更不要為了使用而使用辩恼,設計模式是一把雙刃劍雇庙,過度的設計會導致代碼的可讀性下降,代碼的體積增加灶伊。

系列文章不會詳細介紹設計模式的《七大原則》疆前,也不會對設計模式進行分類。這樣只會增加學習和記憶的成本聘萨,也會導致使用時的思想固化竹椒,總在想這么設計是否符合xx原則,是否是xx設計模式米辐,xx模式是什么類型等等胸完,不是本系列文章的所希望看到的,目標只有一個翘贮,結(jié)合日常開發(fā)分享自身的應用赊窥,以提供一種代碼優(yōu)化的思路。

學習然后忘記狸页,也許是一種最好的方式锨能。

就像俗話說的那樣:天下本沒有路,走的人多了芍耘,就變成了路址遇。在我看來,設計模式也一樣斋竞,它并非是一種定律倔约,而是前輩們總結(jié)下來的經(jīng)驗,我們學習并結(jié)合實際加以利用窃页,而不是生搬硬套跺株。

定義

官腔:對象間的一種一對多的依賴關系复濒,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新乒省。

人話:可能概念不是很明白巧颈,但說道Spring或MQ中的發(fā)布訂閱模型,大家都很熟悉了袖扛。在不考慮分布式的情況下砸泛,一般一個發(fā)布者會對應多個訂閱者,發(fā)布者的信息會被多個訂閱者接受并處理蛆封。

擴展:

關于發(fā)布/訂閱模型觀察者模式的區(qū)別唇礁。

先說一下個人觀點:兩者本是同源,有小區(qū)別惨篱,但并非完全不同盏筐,發(fā)布訂閱模型相當與觀察者模式的升級版,也沒必要刻意區(qū)別并去記憶砸讳,純粹浪費時間琢融。

以下純屬個人爆論,請理性看待簿寂。

看了一下網(wǎng)上的幾篇文章漾抬,一句話總結(jié)一下,即發(fā)布/訂閱模型相對于觀察者模式常遂,多了一個任務調(diào)度中心纳令,且發(fā)布者和訂閱者直接沒有直接關系。

關于發(fā)布/定于模型基本都是基于MQ的設計模型來說的克胳,而對比觀察者的時候卻又是簡單的樣板代碼平绩!一個是設計概念,一個是完全基于書本概念的簡單demo代碼漠另。后者是看過觀察者模式概念馒过,并且完全不去思考變通,認為必須只存在酗钞,觀察者被觀察者兩個才算符合其模式。設想一下来累,在一個商城中砚作,用戶支付完成后,你需要推送消息給訂單系統(tǒng)更新狀態(tài)嘹锁,給庫存系統(tǒng)扣減庫存葫录,給優(yōu)惠券系統(tǒng)銷毀使用的優(yōu)惠券,給積分系統(tǒng)扣減積分领猾,給消息系統(tǒng)通知用戶購買成功等等米同。假設骇扇,消息系統(tǒng)壓力過大,超時處理了面粮,導致整個購買流程失敗回滾少孝,是不是得不償失?所以為了保證穩(wěn)定性熬苍,我們增加了一個消息隊列稍走,那么在消息系統(tǒng)和支付系統(tǒng)就不算是觀察者和被觀察者了嗎?

我所做的比對并非是要你接受兩種模型之間沒有區(qū)別柴底,而是想告訴在看這篇文章的各位婿脸,不要陷入定式思維。不要去糾結(jié)一個沒有任何意義的問題柄驻。無論是發(fā)布訂閱模型還是觀察者模式狐树,它們本就不存在,而僅僅是一種工具一種編碼技巧鸿脓。有去百度的時間抑钟,不如多想想怎么提升自己眼前代碼的擴展性和可讀性。

作者個人還是那個觀點答憔,設計模式并非是刻板的定理味赃,并非1+1=2這樣的固定的模式。而是一個你可以拿來任意操控的工具虐拓,用于提升代碼的兼容性和穩(wěn)定性心俗。

設計模式要在可讀性和擴展性之間進行權(quán)衡。不要張嘴就說蓉驹,你看我這地方用了策略模式城榛,那個地方是個單例,這里嵌套了一個迭代器模式态兴。而是在介紹自己代碼的時候說狠持,我這里這么做考慮到了以后的擴展,盡可能的抽離的公共的部分瞻润,并且加了適當?shù)淖⑨尨梗@樣寫的好處是xxxx!

編程是一門藝術,好比寫作绍撞,作家在創(chuàng)作的過程不會關心這里是否應該用比喻手法正勒,排比手法等等亂七八糟的所謂寫作手法,更多的是靈感傻铣,只有給學生做閱讀理解才會回答這些作者自己都不知道的寫作手法章贞。我相信各位也有體會,當有靈感的時候?qū)懘a就像停不下來一樣非洲,腦子里有千萬種方式去實現(xiàn)眼前這個渺小的功能鸭限。我希望設計模式對各位來說如同寫作手法一樣蜕径,用時不自知,留給后人去分析败京。

死記硬背設計模式不如不學兜喻,否者如同邯鄲學步。

以上純屬個人爆論喧枷!不喜輕噴虹统!

應用場景

樣例1

老樣子,還是來一個簡單的demo隧甚。

相信大家多少都看過或了解過警匪片车荔,里面一般都會有一個打入黑幫內(nèi)部的臥底警察,偷偷的給警方傳遞消息戚扳。

img

假設忧便,在燈塔國有一個斧頭幫,幫主人狠話不多帽借,我們稱其為"大黑"珠增。而燈塔國的警察已經(jīng)注意這個幫很久了,一直想找機會一窩端砍艾,但無奈大黑太狡猾蒂教,總能躲過圍剿。因此脆荷,這天新調(diào)來了一個警察局局長“大白”,關于黑幫凝垛,大白也很苦惱,經(jīng)過高層的商談蜓谋,大家一致決定安排一個臥底到斧頭幫梦皮,里應外合之下,一定可以一網(wǎng)打盡桃焕。于是大白開始物色人選剑肯,為了避開熟面孔,大白決定讓一個剛從警校畢業(yè)观堂,還未到警局報道的“小白”擔此重任让网,并為“小白”偽造了一個身份。小白需要從底層小混混開始做起师痕,盡可能的靠近管理層寂祥。那么如何取得大黑的信任呢?于是大白進行了又一次圍剿活動七兜,在活動中,小白拼死為大黑擋了一發(fā)子彈福扬。逃過一劫的大黑對小白贊賞有加腕铸,并視為左膀右臂惜犀,從此小白開始了3年之后又3年的亡命生涯。狠裹。虽界。

但故事仍沒完,大白不想把賭注都壓在一個人身上涛菠,因此又派了一個臥底“小新”莉御。但為了在暴露的時候不會供出另一個人,兩人之間并不知道彼此的存在俗冻。

故事說完了開始整代碼:

1.先定義觀察者
public interface Observer {

    /**
     * 通知給大白
     */
    void doAction();
}

2.構(gòu)建2個臥底
//小白
public class XiaoBai implements Observer{
    @Override
    public void doAction() {
        //通知大白
    }
}
//小新
public class XiaoXin implements Observer{
    @Override
    public void doAction() {
        //通知大白
    }
}

如果結(jié)構(gòu)完整的話礁叔,我需要再定一個“大白”類,但在觀察者模式中迄薄,描述的觀察者和被觀察者琅关,雖然主觀上,“大白”才是觀察者讥蔽,但他并沒有直接觀察涣易,而是委托給了兩個臥底。因此冶伞,“大黑”和兩個臥底之間屬于被觀察者和觀察者的概念新症。這是基于所謂的定義。

3.構(gòu)建被觀察者
public abstract class Subject {
    List<Observer> observers = new ArrayList<>();

    /**
     * 外出
     */
    public void goOut() {
        this.notifyOb();
    }

    /**
     * 新增觀察者
     */
    public void addObserver(Observer observer) {
      observers.add(observer);
    }

    /**
     * 通風報信
     */
    private void notifyOb() {
        //所有的臥底得知動作后偷偷的報信
        observers.forEach(Observer::doAction);
    }
}


public class DaHei extends Subject{

}

這個情景比較簡單响禽,因此定義了一個公共的被觀察對象徒爹,“大黑”沒有單獨的其他內(nèi)容。

4.調(diào)用
    public static void main(String[] args) {
        DaHei daHei = new DaHei();

        XiaoBai xiaoBai = new XiaoBai();
        daHei.addObserver(xiaoBai);
        XiaoXin xiaoXin = new XiaoXin();
        daHei.addObserver(xiaoXin);
        daHei.goOut();
    }

每次“大黑”外出金抡,小白和小新都會通風報信瀑焦,可謂最慘老大。梗肝。榛瓮。哈哈哈。

這個demo于網(wǎng)上的可能不太一樣巫击,網(wǎng)上的禀晓,比較完整一點,觀察者和被觀察者的頂層都是接口坝锰,中間層使用抽象類粹懒,用于提取公共的部分,實現(xiàn)層則是業(yè)務的定制操作顷级。

樣例2

spring自帶的異步事件監(jiān)聽器AOP都可以視為觀察者模式凫乖,應用大家都很熟悉了,這里就不在畫蛇添足。

但我們可以仿照異步事件去實現(xiàn)一個簡單的可實際應用的事件處理器帽芽。

通過事件的參數(shù)類型進行區(qū)別删掀,可控制一類事件監(jiān)聽一個服務。這里我們就以支付成功后的業(yè)務處理為例导街。

假設披泪,支付成功后,需要更新訂單狀態(tài)搬瑰,扣減庫存款票,銷毀優(yōu)惠券,發(fā)送短信等泽论。

1.定義觀察者
public interface LogicObserver {

    <T extends ObserverBaseBean> void  doAction(T t);
}

2.定義觀察者參數(shù)對象

ObserverBaseBean為觀察者參數(shù)統(tǒng)一的頂層類艾少,不同的業(yè)務定制各自的參數(shù)對象佩厚,并以此對觀察者進行分組姆钉。

@Data
public class ObserverBaseBean implements Serializable {
}

這里我們定義了2個參數(shù)對象。分別為:

支付成功后:用于統(tǒng)一處理支付成功后的其他業(yè)務操作

@EqualsAndHashCode(callSuper = true)
@Data
public class PayObserverBean extends ObserverBaseBean {
}

其他業(yè)務:用于測試抄瓦,區(qū)別與支付成功后的操作潮瓶。

@EqualsAndHashCode(callSuper = true)
@Data
public class OtherObserverBean extends ObserverBaseBean{
}
3.過渡抽象類

在業(yè)務觀察者和觀察者接口之間使用一個抽象類AbstractLogicObserver進行過渡,內(nèi)部定義一個getBean方法钙姊,用于返回當前業(yè)務的觀察者參數(shù)對象毯辅。

public abstract class AbstractLogicObserver implements LogicObserver {

    @PostConstruct
    public void init() {
        ObserverManager.addObservers(getBean().getClass(), this.getClass());
    }


    protected abstract ObserverBaseBean getBean();
}

此外,在init方法煞额,將當前的觀察者注入到ObserverManager中思恐,我愿稱其為調(diào)度中心。其中用到了@PostConstruct注解,不熟悉的小伙伴可自行了解一下膊毁。簡單的來說就是在bean中的屬性自動注入后胀莹,會執(zhí)行這個注解標注的方法,這個方法是無參的void方法婚温。

4.調(diào)度中心

ObserverManager:即所謂的調(diào)度中心就比較簡單了描焰,主要對觀察者按業(yè)務(這里通過不同業(yè)務的參數(shù)對象)分組保存,使用時遍歷即可栅螟。

@Slf4j
@SuppressWarnings("unused")
public class ObserverManager {
    private final static Map<Class<? extends ObserverBaseBean>, List<Class<? extends LogicObserver>>> observerMap = new HashMap<>(16);

    /**
     * 新增觀察者荆秦,并按參數(shù)類型進行分組
     */
    public static void addObservers(Class<? extends ObserverBaseBean> bean, Class<? extends LogicObserver> service) {
        List<Class<? extends LogicObserver>> observers = observerMap.get(bean);
        if (observers == null || observers.isEmpty()) {
            observers = new ArrayList<>();
            observers.add(service);
            observerMap.put(bean, observers);
        } else {
            observers.add(service);
        }
    }

    /**
     * 調(diào)用觀察者
     */
    public static <T extends ObserverBaseBean> void notifyOb(T t) {
        List<Class<? extends LogicObserver>> observers = observerMap.get(t.getClass());
        if (observers == null || observers.isEmpty()) {
            log.warn("未設置觀察者");
            return;
        }
        observers.forEach(ob -> {
            //依然靜態(tài)的獲取bean
            LogicObserver observer = SpringContextUtil.getBean(ob);
            observer.doAction(t);
        });
    }
}

5.業(yè)務的實際觀察對象

至于3個觀察者就不多說了,直接上代碼力图。

支付后消息:

@Component
public class PayAfterMsgObserver extends AbstractLogicObserver {


    @Override
    public <T extends ObserverBaseBean> void doAction(T t) {
        System.out.println("發(fā)送消息提醒");
    }

    @Override
    protected ObserverBaseBean getBean() {
        return new PayObserverBean();
    }
}

支付后扣減優(yōu)惠:

@Component
public class PayAfterCouponObserver extends AbstractLogicObserver{

    @Override
    protected ObserverBaseBean getBean() {
        return new PayObserverBean();
    }

    @Override
    public <T extends ObserverBaseBean> void doAction(T t) {
        System.out.println("扣減優(yōu)惠券");
    }
}

其他業(yè)務:

@Component
public class OtherObserver extends AbstractLogicObserver{

    @Override
    protected ObserverBaseBean getBean() {
        return new OtherObserverBean();
    }

    @Override
    public <T extends ObserverBaseBean> void doAction(T t) {
        //其他業(yè)務操作
        System.out.println("其他業(yè)務操作");
    }
}
6.業(yè)務對象

支付類:

@Service
public class PayService {

    public void payOrder() {
        //支付流程

        //支付完成后的處理
        ObserverManager.notifyOb(new PayObserverBean());
    }

    public void other() {
        //其他業(yè)務處理
        
        //處理之后
        ObserverManager.notifyOb(new OtherObserverBean());
    }
}
7.測試

需要使用springboot單元測試步绸,或通過springmvc的方式調(diào)用,必須在spring的內(nèi)部使用吃媒。不可通過main函數(shù)直接調(diào)用瓤介,否則ObserverManagerobserverMapSpringContextUtil中的ApplicationConext參數(shù)都將為null吕喘,原因為類加載器Classloader不同,感興趣的可以自行了解一下刑桑,后面有空也會單獨介紹classloader兽泄。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ObserverDemo {


    @Resource
    private PayService payService;

    @Test
    public void testPay() {
        payService.payOrder();

    }

    @Test
    public void testOther(){
        payService.other();
    }
}

一個簡單的事件處理器完成,不過是同步的漾月,若將調(diào)用的時候改為多線程,就可以實現(xiàn)異步并發(fā)執(zhí)行胃珍。樣例僅供參考梁肿,還有其他方式實現(xiàn)和優(yōu)化。

UMl圖

老樣子觅彰,從菜鳥教程搬運吩蔑。

觀察者模式使用三個類 Subject、Observer 和 Client填抬。Subject 對象帶有綁定觀察者到 Client 對象和從 Client 對象解綁觀察者的方法烛芬。我們創(chuàng)建 Subject 類、Observer 抽象類和擴展了抽象類 Observer 的實體類飒责。

ObserverPatternDemo赘娄,我們的演示類使用 Subject 和實體類對象來演示觀察者模式。

Uml

小結(jié)

這篇文章拖了很久宏蛉,其中有一個原因就是在main方法中測試時遣臼,結(jié)果和我預期的相差甚遠,顛覆了我的認知拾并,將一個自以為很熟悉的類加載器概念重新拉回視野揍堰。心疼的罵了自己一句。

img

同時在查看網(wǎng)上資料的時候嗅义,看到很多人在類比發(fā)布訂閱模型觀察者模式的區(qū)別屏歹,扣著字眼在那比較,真心覺得沒有意義之碗。

希望通過個人的講解蝙眶,能讓各位有所收貨。哪怕有一點靈感继控,也算是值得了械馆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市武通,隨后出現(xiàn)的幾起案子霹崎,更是在濱河造成了極大的恐慌,老刑警劉巖冶忱,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尾菇,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機派诬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門劳淆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人默赂,你說我怎么就攤上這事沛鸵。” “怎么了缆八?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵曲掰,是天一觀的道長。 經(jīng)常有香客問我奈辰,道長栏妖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任奖恰,我火速辦了婚禮吊趾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瑟啃。我一直安慰自己论泛,他們只是感情好,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布翰守。 她就那樣靜靜地躺著孵奶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蜡峰。 梳的紋絲不亂的頭發(fā)上了袁,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機與錄音湿颅,去河邊找鬼载绿。 笑死,一個胖子當著我的面吹牛油航,可吹牛的內(nèi)容都是我干的崭庸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼谊囚,長吁一口氣:“原來是場噩夢啊……” “哼怕享!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镰踏,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤函筋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后奠伪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跌帐,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡首懈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谨敛。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片究履。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖脸狸,靈堂內(nèi)的尸體忽然破棺而出最仑,到底是詐尸還是另有隱情,我是刑警寧澤炊甲,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布盯仪,位于F島的核電站,受9級特大地震影響蜜葱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜耀石,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一牵囤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧滞伟,春花似錦揭鳞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至亩钟,卻和暖如春乓梨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背清酥。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工扶镀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人焰轻。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓臭觉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辱志。 傳聞我的和親對象是個殘疾皇子蝠筑,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容