兩萬(wàn)字盤(pán)點(diǎn)那些被玩爛了的設(shè)計(jì)模式
大家好,我是三友~~
之前有小伙伴私信我說(shuō)看源碼的時(shí)候感覺(jué)源碼很難,不知道該怎么看,其實(shí)這有部分原因是因?yàn)闆](méi)有弄懂一些源碼實(shí)現(xiàn)的套路,也就是設(shè)計(jì)模式,所以本文我就總結(jié)了9種在源碼中非常常見(jiàn)的設(shè)計(jì)模式,并列舉了很多源碼的實(shí)現(xiàn)例子,希望對(duì)你看源碼和日常工作中有所幫助。
單例模式
單例模式是指一個(gè)類(lèi)在一個(gè)進(jìn)程中只有一個(gè)實(shí)例對(duì)象(但也不一定,比如Spring中的Bean的單例是指在一個(gè)容器中是單例的)
單例模式創(chuàng)建分為餓漢式和懶漢式,總共大概有8種寫(xiě)法。但是在開(kāi)源項(xiàng)目中使用最多的主要有兩種寫(xiě)法:
1、靜態(tài)常量
靜態(tài)常量方式屬于餓漢式,以靜態(tài)變量的方式聲明對(duì)象。這種單例模式在Spring中使用的比較多,舉個(gè)例子,在Spring中對(duì)于Bean的名稱(chēng)生成有個(gè)類(lèi)AnnotationBeanNameGenerator就是單例的。
AnnotationBeanNameGenerator
2、雙重檢查機(jī)制
除了上面一種,還有一種雙重檢查機(jī)制在開(kāi)源項(xiàng)目中也使用的比較多,而且在面試中也比較喜歡問(wèn)。雙重檢查機(jī)制方式屬于懶漢式,代碼如下:
之所以這種方式叫雙重檢查機(jī)制,主要是在創(chuàng)建對(duì)象的時(shí)候進(jìn)行了兩次INSTANCE == null的判斷。
疑問(wèn)講解
這里解釋一下雙重檢查機(jī)制的三個(gè)疑問(wèn):
- 外層判斷null的作用
- 內(nèi)層判斷null的作用
- 變量使用volatile關(guān)鍵字修飾的作用
外層判斷null的作用:其實(shí)就是為了減少進(jìn)入同步代碼塊的次數(shù),提高效率。你想一下,其實(shí)去了外層的判斷其實(shí)是可以的,但是每次獲取對(duì)象都需要進(jìn)入同步代碼塊,實(shí)在是沒(méi)有必要。
內(nèi)層判斷null的作用:防止多次創(chuàng)建對(duì)象。假設(shè)AB同時(shí)走到同步代碼塊,A先搶到鎖,進(jìn)入代碼,創(chuàng)建了對(duì)象,釋放鎖,此時(shí)B進(jìn)入代碼塊,如果沒(méi)有判斷null,那么就會(huì)直接再次創(chuàng)建對(duì)象,那么就不是單例的了,所以需要進(jìn)行判斷null,防止重復(fù)創(chuàng)建單例對(duì)象。
volatile關(guān)鍵字的作用:防止重排序。因?yàn)閯?chuàng)建對(duì)象的過(guò)程不是原子,大概會(huì)分為三個(gè)步驟
- 第一步:分配內(nèi)存空間給Singleton這個(gè)對(duì)象
- 第二步:初始化對(duì)象
- 第三步:將INSTANCE變量指向Singleton這個(gè)對(duì)象內(nèi)存地址
假設(shè)沒(méi)有使用volatile關(guān)鍵字發(fā)生了重排序,第二步和第三步執(zhí)行過(guò)程被調(diào)換了,也就是先將INSTANCE變量指向Singleton這個(gè)對(duì)象內(nèi)存地址,再初始化對(duì)象。這樣在發(fā)生并發(fā)的情況下,另一個(gè)線程經(jīng)過(guò)第一個(gè)if非空判斷時(shí),發(fā)現(xiàn)已經(jīng)為不為空,就直接返回了這個(gè)對(duì)象,但是此時(shí)這個(gè)對(duì)象還未初始化,內(nèi)部的屬性可能都是空值,一旦被使用的話,就很有可能出現(xiàn)空指針這些問(wèn)題。
雙重檢查機(jī)制在dubbo中的應(yīng)用
在dubbo的spi機(jī)制中獲取對(duì)象的時(shí)候有這樣一段代碼:
雖然這段代碼跟上面的單例的寫(xiě)法有點(diǎn)不同,但是不難看出其實(shí)是使用了雙重檢查機(jī)制來(lái)創(chuàng)建對(duì)象,保證對(duì)象單例。
建造者模式
將一個(gè)復(fù)雜對(duì)象的構(gòu)造與它的表示分離,使同樣的構(gòu)建過(guò)程可以創(chuàng)建不同的表示,這樣的設(shè)計(jì)模式被稱(chēng)為建造者模式。它是將一個(gè)復(fù)雜的對(duì)象分解為多個(gè)簡(jiǎn)單的對(duì)象,然后一步一步構(gòu)建而成。
上面的意思看起來(lái)很繞,其實(shí)在實(shí)際開(kāi)發(fā)中,其實(shí)建造者模式使用的還是比較多的,比如有時(shí)在創(chuàng)建一個(gè)pojo對(duì)象時(shí),就可以使用建造者模式來(lái)創(chuàng)建:
上面這段代碼就是通過(guò)建造者模式構(gòu)建了一個(gè)PersonDTO對(duì)象,所以建造者模式又被稱(chēng)為Budiler模式。
這種模式在創(chuàng)建對(duì)象的時(shí)候看起來(lái)比較優(yōu)雅,當(dāng)構(gòu)造參數(shù)比較多的時(shí)候,適合使用建造者模式。
接下來(lái)就來(lái)看看建造者模式在開(kāi)源項(xiàng)目中是如何運(yùn)用的
1、在Spring中的運(yùn)用
我們都知道,Spring在創(chuàng)建Bean之前,會(huì)將每個(gè)Bean的聲明封裝成對(duì)應(yīng)的一個(gè)BeanDefinition,而B(niǎo)eanDefinition會(huì)封裝很多屬性,所以Spring為了更加優(yōu)雅地創(chuàng)建BeanDefinition,就提供了BeanDefinitionBuilder這個(gè)建造者類(lèi)。
BeanDefinitionBuilder
2、在Guava中的運(yùn)用
在項(xiàng)目中,如果我們需要使用本地緩存,會(huì)使用本地緩存的實(shí)現(xiàn)的框架來(lái)創(chuàng)建一個(gè),比如在使用Guava來(lái)創(chuàng)建本地緩存時(shí),就會(huì)這么寫(xiě)
這其實(shí)也就是建造者模式。
建造者模式不僅在開(kāi)源項(xiàng)目中有所使用,在JDK源碼中也有使用到,比如StringBuilder類(lèi)。
最后上面說(shuō)的建造者模式其實(shí)算是在Java中一種簡(jiǎn)化的方式,如果想了解一下傳統(tǒng)的建造者模式,可以看一下這篇文章
https://m.runoob.com/design-pattern/builder-pattern.html?ivk_sa=1024320u
工廠模式
工廠模式在開(kāi)源項(xiàng)目中也使用的非常多,具體的實(shí)現(xiàn)大概可以細(xì)分為三種:
- 簡(jiǎn)單工廠模式
- 工廠方法模式
- 抽象工廠模式
簡(jiǎn)單工廠模式
簡(jiǎn)單工廠模式,就跟名字一樣,的確很簡(jiǎn)單。比如說(shuō),現(xiàn)在有個(gè)動(dòng)物接口Animal,具體的實(shí)現(xiàn)有貓Cat、狗Dog等等,而每個(gè)具體的動(dòng)物對(duì)象創(chuàng)建過(guò)程很復(fù)雜,有各種各樣地步驟,此時(shí)就可以使用簡(jiǎn)單工廠來(lái)封裝對(duì)象的創(chuàng)建過(guò)程,調(diào)用者不需要關(guān)心對(duì)象是如何具體創(chuàng)建的。
當(dāng)需要使用這些對(duì)象,調(diào)用者就可以直接通過(guò)簡(jiǎn)單工廠創(chuàng)建就行。
需要注意的是,一般來(lái)說(shuō)如果每個(gè)動(dòng)物對(duì)象的創(chuàng)建只需要簡(jiǎn)單地new一下就行了,那么其實(shí)就無(wú)需使用工廠模式,工廠模式適合對(duì)象創(chuàng)建過(guò)程復(fù)雜的場(chǎng)景。
工廠方法模式
上面說(shuō)的簡(jiǎn)單工廠模式看起來(lái)沒(méi)啥問(wèn)題,但是還是違反了七大設(shè)計(jì)原則的OCP原則,也就是開(kāi)閉原則。所謂的開(kāi)閉原則就是對(duì)修改關(guān)閉,對(duì)擴(kuò)展開(kāi)放。
什么叫對(duì)修改關(guān)閉?就是盡可能不修改的意思。就拿上面的例子來(lái)說(shuō),如果現(xiàn)在新增了一種動(dòng)物兔子,那么createAnimal方法就得修改,增加一種類(lèi)型的判斷,那么就此時(shí)就出現(xiàn)了修改代碼的行為,也就違反了對(duì)修改關(guān)閉的原則。
所以解決簡(jiǎn)單工廠模式違反開(kāi)閉原則的問(wèn)題,就可以使用工廠方法模式來(lái)解決。
這種方式就是工廠方法模式。他將動(dòng)物工廠提取成一個(gè)接口AnimalFactory,具體每個(gè)動(dòng)物都各自實(shí)現(xiàn)這個(gè)接口,每種動(dòng)物都有各自的創(chuàng)建工廠,如果調(diào)用者需要?jiǎng)?chuàng)建動(dòng)物,就可以通過(guò)各自的工廠來(lái)實(shí)現(xiàn)。
此時(shí)假設(shè)需要新增一個(gè)動(dòng)物兔子,那么只需要實(shí)現(xiàn)AnimalFactory接口就行,對(duì)于原來(lái)的貓和狗的實(shí)現(xiàn),其實(shí)代碼是不需要修改的,遵守了對(duì)修改關(guān)閉的原則,同時(shí)由于是對(duì)擴(kuò)展開(kāi)放,實(shí)現(xiàn)接口就是擴(kuò)展的意思,那么也就符合擴(kuò)展開(kāi)放的原則。
抽象工廠模式
工廠方法模式其實(shí)是創(chuàng)建一個(gè)產(chǎn)品的工廠,比如上面的例子中,AnimalFactory其實(shí)只創(chuàng)建動(dòng)物這一個(gè)產(chǎn)品。而抽象工廠模式特點(diǎn)就是創(chuàng)建一系列產(chǎn)品,比如說(shuō),不同的動(dòng)物吃的東西是不一樣的,那么就可以加入食物這個(gè)產(chǎn)品,通過(guò)抽象工廠模式來(lái)實(shí)現(xiàn)。
在動(dòng)物工廠中,新增了創(chuàng)建食物的接口,小狗小貓的工廠去實(shí)現(xiàn)這個(gè)接口,創(chuàng)建狗糧和貓糧,這里就不去寫(xiě)了。
1、工廠模式在Mybatis的運(yùn)用
在Mybatis中,當(dāng)需要調(diào)用Mapper接口執(zhí)行sql的時(shí)候,需要先獲取到SqlSession,通過(guò)SqlSession再獲取到Mapper接口的動(dòng)態(tài)代理對(duì)象,而SqlSession的構(gòu)造過(guò)程比較復(fù)雜,所以就提供了SqlSessionFactory工廠類(lèi)來(lái)封裝SqlSession的創(chuàng)建過(guò)程。
SqlSessionFactory及默認(rèn)實(shí)現(xiàn)DefaultSqlSessionFactory
對(duì)于使用者來(lái)說(shuō),只需要通過(guò)SqlSessionFactory來(lái)獲取到SqlSession,而無(wú)需關(guān)心SqlSession是如何創(chuàng)建的。
2、工廠模式在Spring中的運(yùn)用
我們知道Spring中的Bean是通過(guò)BeanFactory創(chuàng)建的。
BeanFactory就是Bean生成的工廠。一個(gè)Spring Bean在生成過(guò)程中會(huì)經(jīng)歷復(fù)雜的一個(gè)生命周期,而這些生命周期對(duì)于使用者來(lái)說(shuō)是無(wú)需關(guān)心的,所以就可以將Bean創(chuàng)建過(guò)程的邏輯給封裝起來(lái),提取出一個(gè)Bean的工廠。
策略模式
策略模式也比較常見(jiàn),就比如說(shuō)在Spring源碼中就有很多地方都使用到了策略模式。
在講策略模式是什么之前先來(lái)舉個(gè)例子,這個(gè)例子我在之前的《寫(xiě)出漂亮代碼的45個(gè)小技巧》文章提到過(guò)。
假設(shè)現(xiàn)在有一個(gè)需求,需要將消息推送到不同的平臺(tái)。
最簡(jiǎn)單的做法其實(shí)就是使用if else來(lái)做判斷就行了。
根據(jù)不同的平臺(tái)類(lèi)型進(jìn)行判斷,調(diào)用對(duì)應(yīng)的api發(fā)送消息。
雖然這樣能實(shí)現(xiàn)功能,但是跟上面的提到的簡(jiǎn)單工廠的問(wèn)題是一樣的,同樣違反了開(kāi)閉原則。當(dāng)需要增加一種平臺(tái)類(lèi)型,比如郵件通知,那么就得修改notifyMessage的方法,再次進(jìn)行else if的判斷,然后調(diào)用發(fā)送郵件的郵件發(fā)送消息。
此時(shí)就可以使用策略模式來(lái)優(yōu)化了。
首先設(shè)計(jì)一個(gè)策略接口:
短信通知實(shí)現(xiàn):
app通知實(shí)現(xiàn):
最后notifyMessage的實(shí)現(xiàn)只需要要循環(huán)調(diào)用所有的MessageNotifier的support方法,一旦support方法返回true,說(shuō)明當(dāng)前MessageNotifier支持該類(lèi)的消息發(fā)送,最后再調(diào)用notify發(fā)送消息就可以了。
那么如果現(xiàn)在需要支持通過(guò)郵件通知,只需要實(shí)現(xiàn)MessageNotifier接口,注入到Spring容器就行,其余的代碼根本不需要有任何變動(dòng)。
到這其實(shí)可以更好的理解策略模式了。就拿上面舉的例子來(lái)說(shuō),短信通知,app通知等其實(shí)都是發(fā)送消息一種策略,而策略模式就是需要將這些策略進(jìn)行封裝,抽取共性,使這些策略之間相互替換。
策略模式在SpringMVC中的運(yùn)用
1、對(duì)接口方法參數(shù)的處理
比如說(shuō),我們經(jīng)常在寫(xiě)接口的時(shí)候,會(huì)使用到了@PathVariable、@RequestParam、@RequestBody等注解,一旦我們使用了注解,SpringMVC會(huì)處理注解,從請(qǐng)求中獲取到參數(shù),然后再調(diào)用接口傳遞過(guò)來(lái),而這個(gè)過(guò)程,就使用到了策略模式。
對(duì)于這類(lèi)參數(shù)的解析,SpringMVC提供了一個(gè)策略接口HandlerMethodArgumentResolver
HandlerMethodArgumentResolver
這個(gè)接口的定義就跟我們上面定義的差不多,不同的參數(shù)處理只需要實(shí)現(xiàn)這個(gè)解決就行,比如上面提到的幾個(gè)注解,都有對(duì)應(yīng)的實(shí)現(xiàn)。
比如處理@RequestParam注解的RequestParamMethodArgumentResolver的實(shí)現(xiàn)。
RequestParamMethodArgumentResolver
當(dāng)然還有其它很多的實(shí)現(xiàn),如果想知道各種注解處理的過(guò)程,只需要找到對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)就行了。
2、對(duì)接口返回值的處理
同樣,SpringMVC對(duì)于返回值的處理也是基于策略模式來(lái)實(shí)現(xiàn)的。
HandlerMethodReturnValueHandler
HandlerMethodReturnValueHandler接口定義跟上面都是同一種套路。
比如說(shuō),常見(jiàn)的對(duì)于@ResponseBody注解處理的實(shí)現(xiàn)RequestResponseBodyMethodProcessor。
ResponseBody注解處理的實(shí)現(xiàn)RequestResponseBodyMethodProcessor
同樣,HandlerMethodReturnValueHandler的實(shí)現(xiàn)也有很多,這里就不再舉例了。
策略模式在Spring的運(yùn)用遠(yuǎn)不止這兩處,就比如我在《三萬(wàn)字盤(pán)點(diǎn)Spring/Boot的那些常用擴(kuò)展點(diǎn)》文章提到過(guò)對(duì)于配置文件的加載PropertySourceLoader也是策略模式的運(yùn)用。
模板方法模式
模板方法模式是指,在父類(lèi)中定義一個(gè)操作中的框架,而操作步驟的具體實(shí)現(xiàn)交由子類(lèi)做。其核心思想就是,對(duì)于功能實(shí)現(xiàn)的順序步驟是一定的,但是具體每一步如何實(shí)現(xiàn)交由子類(lèi)決定。
比如說(shuō),對(duì)于旅游來(lái)說(shuō),一般有以下幾個(gè)步驟:
- 做攻略,選擇目的地
- 收拾行李
- 乘坐交通工具去目的地
- 玩耍、拍照
- 乘坐交通工具去返回
但是對(duì)于去哪,收拾什么東西都,乘坐什么交通工具,都是由具體某個(gè)旅行來(lái)決定。
那么對(duì)于旅游這個(gè)過(guò)程使用模板方法模式翻譯成代碼如下:
對(duì)于某次旅行來(lái)說(shuō),只需要重寫(xiě)每個(gè)步驟該做的事就行,比如說(shuō)這次可以選擇去杭州西湖,下次可以去長(zhǎng)城,但是對(duì)于旅行過(guò)程來(lái)說(shuō)是不變了,對(duì)于調(diào)用者來(lái)說(shuō),只需要調(diào)用暴露的travel方法就行。
可能這說(shuō)的還是比較抽象,我再舉兩個(gè)模板方法模式在源碼中實(shí)現(xiàn)的例子。
模板方法模式在源碼中的使用
1、模板方法模式在HashMap中的使用
HashMap我們都很熟悉,可以通過(guò)put方法存元素,并且在元素添加成功之后,會(huì)調(diào)用一下afterNodeInsertion方法。
而afterNodeInsertion其實(shí)是在HashMap中是空實(shí)現(xiàn),什么事都沒(méi)干。
afterNodeInsertion
這其實(shí)就是模板方法模式。HashMap定義了一個(gè)流程,那就是當(dāng)元素成功添加之后會(huì)調(diào)用afterNodeInsertion,子類(lèi)如果需要在元素添加之后做什么事,那么重寫(xiě)afterNodeInsertion就行。
正巧,JDK中的LinkedHashMap重寫(xiě)了這個(gè)方法。
而這段代碼主要干的一件事就是可能會(huì)移除最老的元素,至于到底會(huì)不會(huì)移除,得看if是否成立。
添加元素移除最老的元素,基于這種特性其實(shí)可以實(shí)現(xiàn)LRU算法,比如Mybatis的LruCache就是基于LinkedHashMap實(shí)現(xiàn)的,有興趣的可以扒扒源碼,這里就不再展開(kāi)講了。
2、模板方法模式在Spring中的運(yùn)用
我們都知道,在Spring中,ApplicationContext在使用之前需要調(diào)用一下refresh方法,而refresh方法就定義了整個(gè)容器刷新的執(zhí)行流程代碼。
refresh方法部分截圖
在整個(gè)刷新過(guò)程有一個(gè)onRefresh方法
onRefresh方法
而onRefresh方法默認(rèn)是沒(méi)有做任何事,并且在注釋上有清楚兩個(gè)單詞Template method,翻譯過(guò)來(lái)就是模板方法的意思,所以onRefresh就是一個(gè)模板方法,并且方法內(nèi)部的注釋也表明了,這個(gè)方法是為了子類(lèi)提供的。
在Web環(huán)境下,子類(lèi)會(huì)重寫(xiě)這個(gè)方法,然后創(chuàng)建一個(gè)Web服務(wù)器。
3、模板方法模式在Mybatis中的使用
在Mybatis中,是使用Executor執(zhí)行Sql的。
Executor
而Mybatis一級(jí)緩存就在Executor的抽象實(shí)現(xiàn)中BaseExecutor實(shí)現(xiàn)的。如圖所示,紅圈就是一級(jí)緩存
BaseExecutor
比如在查詢(xún)的時(shí)候,如果一級(jí)緩存有,那么就處理緩存的數(shù)據(jù),沒(méi)有的話就調(diào)用queryFromDatabase從數(shù)據(jù)庫(kù)查
queryFromDatabase會(huì)調(diào)用doQuery方法從數(shù)據(jù)庫(kù)查數(shù)據(jù),然后放入一級(jí)緩存中。
而doQuery是個(gè)抽象方法
所以doQuery其實(shí)就是一個(gè)模板方法,需要子類(lèi)真正實(shí)現(xiàn)從數(shù)據(jù)庫(kù)中查詢(xún)數(shù)據(jù),所以這里就使用了模板方法模式。
責(zé)任鏈模式
在責(zé)任鏈模式里,很多對(duì)象由每一個(gè)對(duì)象對(duì)其下家的引用而連接起來(lái)形成一條鏈。請(qǐng)求在這個(gè)鏈上傳遞,由該鏈上的某一個(gè)對(duì)象或者某幾個(gè)對(duì)象決定處理此請(qǐng)求,每個(gè)對(duì)象在整個(gè)處理過(guò)程中值扮演一個(gè)小小的角色。
舉個(gè)例子,現(xiàn)在有個(gè)請(qǐng)假的審批流程,根據(jù)請(qǐng)假的人的級(jí)別審批到的領(lǐng)導(dǎo)不同,比如有有組長(zhǎng)、主管、HR、分管經(jīng)理等等。
先需要定義一個(gè)處理抽象類(lèi),抽象類(lèi)有個(gè)下一個(gè)處理對(duì)象的引用,提供了抽象處理方法,還有一個(gè)對(duì)下一個(gè)處理對(duì)象的調(diào)用方法。
幾種審批人的實(shí)現(xiàn)
有了這幾個(gè)實(shí)現(xiàn)之后,接下來(lái)就需要對(duì)對(duì)象進(jìn)行組裝,組成一個(gè)鏈條,比如在Spring中就可以這么玩。
之后對(duì)于調(diào)用方而言,只需要獲取到鏈條,開(kāi)始處理就行。
一旦后面出現(xiàn)需要增加或者減少審批人,只需要調(diào)整鏈條中的節(jié)點(diǎn)就行,對(duì)于調(diào)用者來(lái)說(shuō)是無(wú)感知的。
責(zé)任鏈模式在開(kāi)源項(xiàng)目中的使用
1、在SpringMVC中的使用
在SpringMVC中,可以通過(guò)使用HandlerInterceptor對(duì)每個(gè)請(qǐng)求進(jìn)行攔截。
HandlerInterceptor
而HandlerInterceptor其實(shí)就使用到了責(zé)任鏈模式,但是這種責(zé)任鏈模式的寫(xiě)法跟上面舉的例子寫(xiě)法不太一樣。
對(duì)于HandlerInterceptor的調(diào)用是在HandlerExecutionChain中完成的。
HandlerExecutionChain
比如說(shuō),對(duì)于請(qǐng)求處理前的攔截,就在是這樣調(diào)用的。
其實(shí)就是循環(huán)遍歷每個(gè)HandlerInterceptor,調(diào)用preHandle方法。
2、在Sentinel中的使用
Sentinel是阿里開(kāi)源的一個(gè)流量治理組件,而Sentinel核心邏輯的執(zhí)行其實(shí)就是一條責(zé)任鏈。
在Sentinel中,有個(gè)核心抽象類(lèi)AbstractLinkedProcessorSlot
AbstractLinkedProcessorSlot
這個(gè)組件內(nèi)部也維護(hù)了下一個(gè)節(jié)點(diǎn)對(duì)象,這個(gè)類(lèi)扮演的角色跟例子中的ApprovalHandler類(lèi)是一樣的,寫(xiě)法也比較相似。這個(gè)組件有很多實(shí)現(xiàn)
比如有比較核心的幾個(gè)實(shí)現(xiàn)
- DegradeSlot:熔斷降級(jí)的實(shí)現(xiàn)
- FlowSlot:流量控制的實(shí)現(xiàn)
- StatisticSlot:統(tǒng)計(jì)的實(shí)現(xiàn),比如統(tǒng)計(jì)請(qǐng)求成功的次數(shù)、異常次數(shù),為限流提供數(shù)據(jù)來(lái)源
- SystemSlot:根據(jù)系統(tǒng)規(guī)則來(lái)進(jìn)行流量控制
整個(gè)鏈條的組裝的實(shí)現(xiàn)是由DefaultSlotChainBuilder實(shí)現(xiàn)的
DefaultSlotChainBuilder
并且內(nèi)部是使用了SPI機(jī)制來(lái)加載每個(gè)處理節(jié)點(diǎn)
所以,如果你想自定一些處理邏輯,就可以基于SPI機(jī)制來(lái)擴(kuò)展。
除了上面的例子,比如Gateway網(wǎng)關(guān)、Dubbo、MyBatis等等框架中都有責(zé)任鏈模式的身影,所以責(zé)任鏈模式使用的還是比較多的。
代理模式
代理模式也是開(kāi)源項(xiàng)目中很常見(jiàn)的使用的一種設(shè)計(jì)模式,這種模式可以在不改變?cè)写a的情況下增加功能。
舉個(gè)例子,比如現(xiàn)在有個(gè)PersonService接口和它的實(shí)現(xiàn)類(lèi)PersonServiceImpl
這個(gè)類(lèi)剛開(kāi)始運(yùn)行的好好的,但是突然之前不知道咋回事了,有報(bào)錯(cuò),需要追尋入?yún)ⅲ源藭r(shí)就可以這么寫(xiě)。
這么寫(xiě),就修改了代碼,萬(wàn)一以后不需要打印日志了呢,豈不是又要修改代碼,不符和之前說(shuō)的開(kāi)閉原則,那么怎么寫(xiě)呢?可以這么玩。
可以實(shí)現(xiàn)一個(gè)代理類(lèi)PersonServiceProxy,對(duì)PersonServiceImpl進(jìn)行代理,這個(gè)代理類(lèi)干的事就是打印日志,最后調(diào)用PersonServiceImpl進(jìn)行人員信息的保存,這就是代理模式。
當(dāng)需要打印日志就使用PersonServiceProxy,不需要打印日志就使用PersonServiceImpl,這樣就行了,不需要改原有代碼的實(shí)現(xiàn)。
講到了代理模式,就不得不提一下Spring AOP,Spring AOP其實(shí)跟靜態(tài)代理很像,最終其實(shí)也是調(diào)用目標(biāo)對(duì)象的方法,只不過(guò)是動(dòng)態(tài)生成的,這里就不展開(kāi)講解了。
代理模式在Mybtais中的使用
前面在說(shuō)模板方法模式的時(shí)候,舉了一個(gè)BaseExecutor使用到了模板方法模式的例子,并且在BaseExecutor這里面還完成了一級(jí)緩存的操作。
其實(shí)不光是一級(jí)緩存是通過(guò)Executor實(shí)現(xiàn)的,二級(jí)緩存其實(shí)也是,只不過(guò)不在BaseExecutor里面實(shí)現(xiàn),而是在CachingExecutor中實(shí)現(xiàn)的。
CachingExecutor
CachingExecutor中內(nèi)部有一個(gè)Executor類(lèi)型的屬性delegate,delegate單詞的意思就是代理的意思,所以CachingExecutor顯然就是一個(gè)代理類(lèi),這里就使用到了代理模式。
CachingExecutor的實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單,先從二級(jí)緩存查,查不到就通過(guò)被代理的對(duì)象查找數(shù)據(jù),而被代理的Executor在Mybatis中默認(rèn)使用的是SimpleExecutor實(shí)現(xiàn),SimpleExecutor繼承自BaseExecutor。
這里思考一下二級(jí)緩存為什么不像一級(jí)緩存一樣直接寫(xiě)到BaseExecutor中?
這里我猜測(cè)一下是為了減少耦合。
我們知道Mybatis的一級(jí)緩存默認(rèn)是開(kāi)啟的,一級(jí)緩存寫(xiě)在BaseExecutor中的話,那么只要是繼承了BaseExecutor,就擁有了一級(jí)緩存的能力。
但二級(jí)緩存默認(rèn)是不開(kāi)啟的,如果寫(xiě)在BaseExecutor中,講道理也是可以的,但不符和單一職責(zé)的原則,類(lèi)的功能過(guò)多,同時(shí)會(huì)耦合很多判斷代碼,比如開(kāi)啟二級(jí)緩存走什么邏輯,不開(kāi)啟二級(jí)緩存走什么邏輯。而使用代理模式很好的解決了這一問(wèn)題,只需要在創(chuàng)建的Executor的時(shí)候判斷是否開(kāi)啟二級(jí)緩存,開(kāi)啟的話就用CachingExecutor代理一下,不開(kāi)啟的話老老實(shí)實(shí)返回未被代理的對(duì)象就行,默認(rèn)是SimpleExecutor。
如圖所示,是構(gòu)建Executor對(duì)象的源碼,一旦開(kāi)啟了二級(jí)緩存,就會(huì)將前面創(chuàng)建的Executor進(jìn)行代理,構(gòu)建一個(gè)CachingExecutor返回。
適配器模式
適配器模式使得原本由于接口不兼容而不能一起工作的哪些類(lèi)可以一起工作,將一個(gè)類(lèi)的接口轉(zhuǎn)換成客戶(hù)希望的另一個(gè)接口。
舉個(gè)生活中的例子,比如手機(jī)充電器接口類(lèi)型有USB TypeC接口和Micro USB接口等。現(xiàn)在需要給一個(gè)Micro USB接口的手機(jī)充電,但是現(xiàn)在只有USB TypeC接口的充電器,這怎么辦呢?
其實(shí)一般可以弄個(gè)一個(gè)USB TypeC轉(zhuǎn)Micro USB接口的轉(zhuǎn)接頭,這樣就可以給Micro USB接口手機(jī)充電了,代碼如下
USBTypeC接口充電
MicroUSB接口
適配實(shí)現(xiàn),最后是調(diào)用USBTypeC接口來(lái)充電
方然除了上面這種寫(xiě)法,還有一種繼承的寫(xiě)法。
這兩種寫(xiě)法主要是繼承和組合(聚合)的區(qū)別。
這樣就可以通過(guò)適配器(轉(zhuǎn)接頭)就可以實(shí)現(xiàn)USBTypeC給MicroUSB接口充電。
適配器模式在日志中的使用
在日常開(kāi)發(fā)中,日志是必不可少的,可以幫助我們快速快速定位問(wèn)題,但是日志框架比較多,比如Slf4j、Log4j等等,一般同一系統(tǒng)都使用一種日志框架。
但是像Mybatis這種框架來(lái)說(shuō),它本身在運(yùn)行的過(guò)程中也需要產(chǎn)生日志,但是Mybatis框架在設(shè)計(jì)的時(shí)候,無(wú)法知道項(xiàng)目中具體使用的是什么日志框架,所以只能適配各種日志框架,項(xiàng)目中使用什么框架,Mybatis就使用什么框架。
為此Mybatis提供一個(gè)Log接口
而不同的日志框架,只需要適配這個(gè)接口就可以了
Slf4jLoggerImpl
就拿Slf4j的實(shí)現(xiàn)來(lái)看,內(nèi)部依賴(lài)了一個(gè)Slf4j框架中的Logger對(duì)象,最后所有日志的打印都是通過(guò)Slf4j框架中的Logger對(duì)象來(lái)實(shí)現(xiàn)的。
此外,Mybatis還提供了如下的一些實(shí)現(xiàn)
這樣,Mybatis在需要打印日志的時(shí)候,只需要從Mybatis自己的LogFactory中獲取到Log對(duì)象就行,至于最終獲取到的是什么Log實(shí)現(xiàn),由最終項(xiàng)目中使用日志框架來(lái)決定。
觀察者模式
當(dāng)對(duì)象間存在一對(duì)多關(guān)系時(shí),則使用觀察者模式(Observer Pattern)。比如,當(dāng)一個(gè)對(duì)象被修改時(shí),則會(huì)自動(dòng)通知依賴(lài)它的對(duì)象。
這是什么意思呢,舉個(gè)例子來(lái)說(shuō),假設(shè)發(fā)生了火災(zāi),可能需要打119、救人,那么就可以基于觀察者模式來(lái)實(shí)現(xiàn),打119、救人的操作只需要觀察火災(zāi)的發(fā)生,一旦發(fā)生,就觸發(fā)相應(yīng)的邏輯。
觀察者的核心優(yōu)點(diǎn)就是觀察者和被觀察者是解耦合的。就拿上面的例子來(lái)說(shuō),火災(zāi)事件(被觀察者)根本不關(guān)系有幾個(gè)監(jiān)聽(tīng)器(觀察者),當(dāng)以后需要有變動(dòng),只需要擴(kuò)展監(jiān)聽(tīng)器就行,對(duì)于事件的發(fā)布者和其它監(jiān)聽(tīng)器是無(wú)需做任何改變的。
觀察者模式實(shí)現(xiàn)起來(lái)比較復(fù)雜,這里我舉一下Spring事件的例子來(lái)說(shuō)明一下。
觀察者模式在Spring事件中的運(yùn)用
Spring事件,就是Spring基于觀察者模式實(shí)現(xiàn)的一套API,如果有不知道不知道Spring事件的小伙伴,可以看看《三萬(wàn)字盤(pán)點(diǎn)Spring/Boot的那些常用擴(kuò)展點(diǎn)》這篇文章,里面有對(duì)Spring事件的詳細(xì)介紹,這里就不對(duì)使用進(jìn)行介紹了。
Spring事件的實(shí)現(xiàn)比較簡(jiǎn)單,其實(shí)就是當(dāng)Bean在生成完成之后,會(huì)將所有的ApplicationListener接口實(shí)現(xiàn)(監(jiān)聽(tīng)器)添加到ApplicationEventMulticaster中。
ApplicationEventMulticaster可以理解為一個(gè)調(diào)度中心的作用,可以將事件通知給監(jiān)聽(tīng)器,觸發(fā)監(jiān)聽(tīng)器的執(zhí)行。
ApplicationEventMulticaster可以理解為一個(gè)總線
retrieverCache中存儲(chǔ)了事件類(lèi)型和對(duì)應(yīng)監(jiān)聽(tīng)器的緩存。當(dāng)發(fā)布事件的時(shí)候,會(huì)通過(guò)事件的類(lèi)型找到對(duì)應(yīng)的監(jiān)聽(tīng)器,然后循環(huán)調(diào)用監(jiān)聽(tīng)器。
所以,Spring的觀察者模式實(shí)現(xiàn)的其實(shí)也不復(fù)雜。
總結(jié)
本文通過(guò)對(duì)設(shè)計(jì)模式的講解加源碼舉例的方式介紹了9種在代碼設(shè)計(jì)中常用的設(shè)計(jì)模式:
- 單例模式
- 建造者模式
- 工廠模式
- 策略模式
- 模板方法模式
- 責(zé)任鏈模式
- 代理模式
- 適配器模式
- 觀察者模式
其實(shí)這些設(shè)計(jì)模式不僅在源碼中常見(jiàn)在平時(shí)工作中也是可以經(jīng)常使用到的。
設(shè)計(jì)模式其實(shí)還是一種思想,或者是套路性的東西,至于設(shè)計(jì)模式具體怎么用、如何用、代碼如何寫(xiě)還得依靠具體的場(chǎng)景來(lái)進(jìn)行靈活的判斷。
最后,本文又是前前后后花了一周多的時(shí)間完成,如果對(duì)你有點(diǎn)幫助,還請(qǐng)幫忙點(diǎn)贊、在看、轉(zhuǎn)發(fā)、非常感謝。