JAVA設(shè)計(jì)模式之觀察者模式

前言

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

設(shè)計(jì)模式僅是一些開發(fā)者在日常開發(fā)中的編碼技巧的匯總并非固定不變,可根據(jù)項(xiàng)目業(yè)務(wù)實(shí)際情況進(jìn)行擴(kuò)展和應(yīng)用,切不可被這個(gè)束縛。更不要為了使用而使用,設(shè)計(jì)模式是一把雙刃劍,過(guò)度的設(shè)計(jì)會(huì)導(dǎo)致代碼的可讀性下降,代碼的體積增加。

系列文章不會(huì)詳細(xì)介紹設(shè)計(jì)模式的《七大原則》,也不會(huì)對(duì)設(shè)計(jì)模式進(jìn)行分類。這樣只會(huì)增加學(xué)習(xí)和記憶的成本,也會(huì)導(dǎo)致使用時(shí)的思想固化,總在想這么設(shè)計(jì)是否符合xx原則,是否是xx設(shè)計(jì)模式,xx模式是什么類型等等,不是本系列文章的所希望看到的,目標(biāo)只有一個(gè),結(jié)合日常開發(fā)分享自身的應(yīng)用,以提供一種代碼優(yōu)化的思路。

學(xué)習(xí)然后忘記,也許是一種最好的方式。

就像俗話說(shuō)的那樣:天下本沒(méi)有路,走的人多了,就變成了路。在我看來(lái),設(shè)計(jì)模式也一樣,它并非是一種定律,而是前輩們總結(jié)下來(lái)的經(jīng)驗(yàn),我們學(xué)習(xí)并結(jié)合實(shí)際加以利用,而不是生搬硬套。

定義

官腔:對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都得到通知并被自動(dòng)更新。

人話:可能概念不是很明白,但說(shuō)道Spring或MQ中的發(fā)布訂閱模型,大家都很熟悉了。在不考慮分布式的情況下,一般一個(gè)發(fā)布者會(huì)對(duì)應(yīng)多個(gè)訂閱者,發(fā)布者的信息會(huì)被多個(gè)訂閱者接受并處理。

擴(kuò)展:

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

先說(shuō)一下個(gè)人觀點(diǎn):兩者本是同源,有小區(qū)別,但并非完全不同,發(fā)布訂閱模型相當(dāng)與觀察者模式的升級(jí)版,也沒(méi)必要刻意區(qū)別并去記憶,純粹浪費(fèi)時(shí)間。

以下純屬個(gè)人爆論,請(qǐng)理性看待。

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

關(guān)于發(fā)布/定于模型基本都是基于MQ的設(shè)計(jì)模型來(lái)說(shuō)的,而對(duì)比觀察者的時(shí)候卻又是簡(jiǎn)單的樣板代碼!一個(gè)是設(shè)計(jì)概念,一個(gè)是完全基于書本概念的簡(jiǎn)單demo代碼。后者是看過(guò)觀察者模式概念,并且完全不去思考變通,認(rèn)為必須只存在,觀察者被觀察者兩個(gè)才算符合其模式。設(shè)想一下,在一個(gè)商城中,用戶支付完成后,你需要推送消息給訂單系統(tǒng)更新狀態(tài),給庫(kù)存系統(tǒng)扣減庫(kù)存,給優(yōu)惠券系統(tǒng)銷毀使用的優(yōu)惠券,給積分系統(tǒng)扣減積分,給消息系統(tǒng)通知用戶購(gòu)買成功等等。假設(shè),消息系統(tǒng)壓力過(guò)大,超時(shí)處理了,導(dǎo)致整個(gè)購(gòu)買流程失敗回滾,是不是得不償失?所以為了保證穩(wěn)定性,我們?cè)黾恿艘粋€(gè)消息隊(duì)列,那么在消息系統(tǒng)和支付系統(tǒng)就不算是觀察者和被觀察者了嗎?

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

作者個(gè)人還是那個(gè)觀點(diǎn),設(shè)計(jì)模式并非是刻板的定理,并非1+1=2這樣的固定的模式。而是一個(gè)你可以拿來(lái)任意操控的工具,用于提升代碼的兼容性和穩(wěn)定性。

設(shè)計(jì)模式要在可讀性和擴(kuò)展性之間進(jìn)行權(quán)衡。不要張嘴就說(shuō),你看我這地方用了策略模式,那個(gè)地方是個(gè)單例,這里嵌套了一個(gè)迭代器模式。而是在介紹自己代碼的時(shí)候說(shuō),我這里這么做考慮到了以后的擴(kuò)展,盡可能的抽離的公共的部分,并且加了適當(dāng)?shù)淖⑨專@樣寫的好處是xxxx!

編程是一門藝術(shù),好比寫作,作家在創(chuàng)作的過(guò)程不會(huì)關(guān)心這里是否應(yīng)該用比喻手法,排比手法等等亂七八糟的所謂寫作手法,更多的是靈感,只有給學(xué)生做閱讀理解才會(huì)回答這些作者自己都不知道的寫作手法。我相信各位也有體會(huì),當(dāng)有靈感的時(shí)候?qū)懘a就像停不下來(lái)一樣,腦子里有千萬(wàn)種方式去實(shí)現(xiàn)眼前這個(gè)渺小的功能。我希望設(shè)計(jì)模式對(duì)各位來(lái)說(shuō)如同寫作手法一樣,用時(shí)不自知,留給后人去分析。

死記硬背設(shè)計(jì)模式不如不學(xué),否者如同邯鄲學(xué)步。

以上純屬個(gè)人爆論!不喜輕噴!

應(yīng)用場(chǎng)景

樣例1

老樣子,還是來(lái)一個(gè)簡(jiǎn)單的demo。

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

img

假設(shè),在燈塔國(guó)有一個(gè)斧頭幫,幫主人狠話不多,我們稱其為"大黑"。而燈塔國(guó)的警察已經(jīng)注意這個(gè)幫很久了,一直想找機(jī)會(huì)一窩端,但無(wú)奈大黑太狡猾,總能躲過(guò)圍剿。因此,這天新調(diào)來(lái)了一個(gè)警察局局長(zhǎng)“大白”,關(guān)于黑幫,大白也很苦惱,經(jīng)過(guò)高層的商談,大家一致決定安排一個(gè)臥底到斧頭幫,里應(yīng)外合之下,一定可以一網(wǎng)打盡。于是大白開始物色人選,為了避開熟面孔,大白決定讓一個(gè)剛從警校畢業(yè),還未到警局報(bào)道的“小白”擔(dān)此重任,并為“小白”偽造了一個(gè)身份。小白需要從底層小混混開始做起,盡可能的靠近管理層。那么如何取得大黑的信任呢?于是大白進(jìn)行了又一次圍剿活動(dòng),在活動(dòng)中,小白拼死為大黑擋了一發(fā)子彈。逃過(guò)一劫的大黑對(duì)小白贊賞有加,并視為左膀右臂,從此小白開始了3年之后又3年的亡命生涯。。。

但故事仍沒(méi)完,大白不想把賭注都?jí)涸谝粋€(gè)人身上,因此又派了一個(gè)臥底“小新”。但為了在暴露的時(shí)候不會(huì)供出另一個(gè)人,兩人之間并不知道彼此的存在。

故事說(shuō)完了開始整代碼:

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

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

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

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

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);
    }

    /**
     * 通風(fēng)報(bào)信
     */
    private void notifyOb() {
        //所有的臥底得知?jiǎng)幼骱笸低档膱?bào)信
        observers.forEach(Observer::doAction);
    }
}


public class DaHei extends Subject{

}

這個(gè)情景比較簡(jiǎn)單,因此定義了一個(gè)公共的被觀察對(duì)象,“大黑”沒(méi)有單獨(dú)的其他內(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();
    }

每次“大黑”外出,小白和小新都會(huì)通風(fēng)報(bào)信,可謂最慘老大。。。哈哈哈。

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

樣例2

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

但我們可以仿照異步事件去實(shí)現(xiàn)一個(gè)簡(jiǎn)單的可實(shí)際應(yīng)用的事件處理器。

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

假設(shè),支付成功后,需要更新訂單狀態(tài),扣減庫(kù)存,銷毀優(yōu)惠券,發(fā)送短信等。

1.定義觀察者
public interface LogicObserver {

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

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

ObserverBaseBean為觀察者參數(shù)統(tǒng)一的頂層類,不同的業(yè)務(wù)定制各自的參數(shù)對(duì)象,并以此對(duì)觀察者進(jìn)行分組。

@Data
public class ObserverBaseBean implements Serializable {
}

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

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

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

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

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

在業(yè)務(wù)觀察者和觀察者接口之間使用一個(gè)抽象類AbstractLogicObserver進(jìn)行過(guò)渡,內(nèi)部定義一個(gè)getBean方法,用于返回當(dāng)前業(yè)務(wù)的觀察者參數(shù)對(duì)象。

public abstract class AbstractLogicObserver implements LogicObserver {

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


    protected abstract ObserverBaseBean getBean();
}

此外,在init方法,將當(dāng)前的觀察者注入到ObserverManager中,我愿稱其為調(diào)度中心。其中用到了@PostConstruct注解,不熟悉的小伙伴可自行了解一下。簡(jiǎn)單的來(lái)說(shuō)就是在bean中的屬性自動(dòng)注入后,會(huì)執(zhí)行這個(gè)注解標(biāo)注的方法,這個(gè)方法是無(wú)參的void方法。

4.調(diào)度中心

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

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

    /**
     * 新增觀察者,并按參數(shù)類型進(jìn)行分組
     */
    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("未設(shè)置觀察者");
            return;
        }
        observers.forEach(ob -> {
            //依然靜態(tài)的獲取bean
            LogicObserver observer = SpringContextUtil.getBean(ob);
            observer.doAction(t);
        });
    }
}

5.業(yè)務(wù)的實(shí)際觀察對(duì)象

至于3個(gè)觀察者就不多說(shuō)了,直接上代碼。

支付后消息:

@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è)務(wù):

@Component
public class OtherObserver extends AbstractLogicObserver{

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

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

支付類:

@Service
public class PayService {

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

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

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

需要使用springboot單元測(cè)試,或通過(guò)springmvc的方式調(diào)用,必須在spring的內(nèi)部使用。不可通過(guò)main函數(shù)直接調(diào)用,否則ObserverManagerobserverMapSpringContextUtil中的ApplicationConext參數(shù)都將為null,原因?yàn)轭惣虞d器Classloader不同,感興趣的可以自行了解一下,后面有空也會(huì)單獨(dú)介紹classloader。

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


    @Resource
    private PayService payService;

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

    }

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

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

UMl圖

老樣子,從菜鳥教程搬運(yùn)。

觀察者模式使用三個(gè)類 Subject、Observer 和 Client。Subject 對(duì)象帶有綁定觀察者到 Client 對(duì)象和從 Client 對(duì)象解綁觀察者的方法。我們創(chuàng)建 Subject 類、Observer 抽象類和擴(kuò)展了抽象類 Observer 的實(shí)體類。

ObserverPatternDemo,我們的演示類使用 Subject 和實(shí)體類對(duì)象來(lái)演示觀察者模式。

Uml

小結(jié)

這篇文章拖了很久,其中有一個(gè)原因就是在main方法中測(cè)試時(shí),結(jié)果和我預(yù)期的相差甚遠(yuǎn),顛覆了我的認(rèn)知,將一個(gè)自以為很熟悉的類加載器概念重新拉回視野。心疼的罵了自己一句。

img

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

希望通過(guò)個(gè)人的講解,能讓各位有所收貨。哪怕有一點(diǎn)靈感,也算是值得了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容