1. 呦呦,這些代碼有點(diǎn)臭,重構(gòu)大法帶你秀(SPI接口化),skr~

        共 5749字,需瀏覽 12分鐘

         ·

        2021-08-10 18:25

        如果說(shuō) 正常的重構(gòu)是為了消除代碼的壞味道, 那么高層次的重構(gòu)就是消除架構(gòu)的壞味道

        最近由于需要將公司基礎(chǔ)架構(gòu)的組件進(jìn)行各種兼容,適配以及二開(kāi),所以很多時(shí)候就需要對(duì)組件進(jìn)行重構(gòu),大家是不是在拿到公司老項(xiàng)目老代碼,又需要二開(kāi)或者重構(gòu)的時(shí)候,會(huì)頭很大,無(wú)從下手,我之前也一直是這樣的狀態(tài),不過(guò)在慢慢熟悉了一些重構(gòu)的思想和方法之后,就能稍微的得心應(yīng)手一些,下面我就開(kāi)始講下重構(gòu),然后會(huì)著重講下重構(gòu)中的SPI接口化。

        先給大家看看最近通過(guò)使用SPI接口化,重構(gòu)的一個(gè)組件-分布式存儲(chǔ)。

        重構(gòu)前的代碼結(jié)構(gòu)

        好家伙,所有的第三方存儲(chǔ)都是寫(xiě)在一個(gè)模塊中的,各種阿里云,騰訊云,華為云等等,這樣的代碼架構(gòu)在前期可能在不需要經(jīng)常擴(kuò)展,二開(kāi)的時(shí)候,還是能用的。

        但是當(dāng)某個(gè)新需求來(lái)的時(shí)候,比如我遇到的:需要支持多個(gè)云的多個(gè)賬號(hào)上傳下載功能,這個(gè)是因?yàn)樵诓煌脑粕?,不同賬號(hào)的權(quán)限,安全認(rèn)證等都是不太一樣的,所以在某一刻,這個(gè)需求就被提出來(lái)了,也就是你想上傳到哪個(gè)云的哪個(gè)賬號(hào)都可以。

        然后拿到這個(gè)代碼,看了下這樣的架構(gòu),可能在這樣的基礎(chǔ)上完成需求也是沒(méi)有問(wèn)題的,但是擴(kuò)展很麻煩,而且代碼會(huì)越來(lái)越繁重,架構(gòu)會(huì)越來(lái)越復(fù)雜,不清晰。

        所以我索性趁著這個(gè)機(jī)會(huì),就重構(gòu)一把,和其他同事也商量了下,決定分模塊,SPI化,好處就是根據(jù)你想使用的引入對(duì)應(yīng)的依賴,讓代碼架構(gòu)更加清晰,后續(xù)更加容易擴(kuò)展了!下面就是重構(gòu)后的大體架構(gòu):

        是不是清楚多了,之后哪怕某個(gè)云存儲(chǔ)需要增加新功能,或者需要兼容更多的云也是比較容易的了。

        好了,下面就讓我們開(kāi)始講講重構(gòu)大法~


        重構(gòu)

        重構(gòu)是什么?

        重構(gòu)(Refactoring)就是通過(guò)調(diào)整程序代碼改善軟件的質(zhì)量、性能,使其程序的設(shè)計(jì)模式和架構(gòu)更趨合理,提高軟件的擴(kuò)展性和維護(hù)性。

        重構(gòu)最重要的思想就是讓普通程序員也能寫(xiě)出優(yōu)秀的程序。

        把優(yōu)化代碼質(zhì)量的過(guò)程拆解成一個(gè)個(gè)小的步驟,這樣重構(gòu)一個(gè)項(xiàng)目的巨大工作量就變成比如修改變量名、提取函數(shù)、抽取接口等等簡(jiǎn)單的工作目標(biāo)。

        作為一個(gè)普通的程序員就可以通過(guò)實(shí)現(xiàn)這些易完成的工作目標(biāo)來(lái)提升自己的編碼能力,加深自己的項(xiàng)目認(rèn)識(shí),從而為最高層次的重構(gòu)打下基礎(chǔ)。

        而且高層次的重構(gòu)依然是由無(wú)數(shù)個(gè)小目標(biāo)構(gòu)成,而不是長(zhǎng)時(shí)間、大規(guī)模地去實(shí)現(xiàn)。

        重構(gòu)本質(zhì)是極限編程的一部分,完整地實(shí)現(xiàn)極限編程才能最大化地發(fā)揮重構(gòu)的價(jià)值。而極限編程本身就提倡擁抱變化,增強(qiáng)適應(yīng)性,因此分解極限編程中的功能去適應(yīng)項(xiàng)目的需求、適應(yīng)團(tuán)隊(duì)的現(xiàn)狀才是最好的操作模式。

        重構(gòu)的重點(diǎn)

        重復(fù)代碼,過(guò)長(zhǎng)函數(shù),過(guò)大的類,過(guò)長(zhǎng)參數(shù)列,發(fā)散式變化,霰彈式修改,依戀情結(jié),數(shù)據(jù)泥團(tuán),基本類型偏執(zhí),平行繼承體系,冗余類等

        下面舉一些常用的或者比較基礎(chǔ)的例子:

        一些基本的原則我覺(jué)得還是需要了解的

        1. 盡量避免過(guò)多過(guò)長(zhǎng)的創(chuàng)建Java對(duì)象
        2. 盡量使用局部變量
        3. 盡量使用StringBuilder和StringBuffer進(jìn)行字符串連接
        4. 盡量減少對(duì)變量的重復(fù)計(jì)算
        5. 盡量在finally塊中釋放資源
        6. 盡量緩存經(jīng)常使用的對(duì)象
        7. 不使用的對(duì)象及時(shí)設(shè)置為null
        8. 盡量考慮使用靜態(tài)方法
        9. 盡量在合適的場(chǎng)合使用單例
        10. 盡量使用final修飾符

        下面是關(guān)于類和方法優(yōu)化:

        1. 重復(fù)代碼的提取
        2. 冗長(zhǎng)方法的分割
        3. 嵌套條件分支或者循環(huán)遞歸的優(yōu)化
        4. 提取類或繼承體系中的常量
        5. 提取繼承體系中重復(fù)的屬性與方法到父類

        這里先簡(jiǎn)單介紹這些比較常規(guī)的重構(gòu)思想和原則,方法,畢竟今天的主角是SPI,下面有請(qǐng)SPI登場(chǎng)!

        SPI

        什么是SPI?

        SPI全稱Service Provider Interface,是Java提供的一套用來(lái)被第三方實(shí)現(xiàn)或者擴(kuò)展的API,它可以用來(lái)啟用框架擴(kuò)展和替換組件。

        它是一種服務(wù)發(fā)現(xiàn)機(jī)制,它通過(guò)在ClassPath路徑下的META-INF/services文件夾查找文件,自動(dòng)加載文件里所定義的類。

        這一機(jī)制為很多框架擴(kuò)展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機(jī)制。

        下面就是SPI的機(jī)制過(guò)程

        SPI實(shí)際上是基于接口的編程+策略模式+配置文件組合實(shí)現(xiàn)的動(dòng)態(tài)加載機(jī)制。

        系統(tǒng)設(shè)計(jì)的各個(gè)抽象,往往有很多不同的實(shí)現(xiàn)方案,在面向的對(duì)象的設(shè)計(jì)里,一般推薦模塊之間基于接口編程,模塊之間不對(duì)實(shí)現(xiàn)類進(jìn)行硬編碼。

        一旦代碼里涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實(shí)現(xiàn),就需要修改代碼。為了實(shí)現(xiàn)在模塊裝配的時(shí)候能不在程序里動(dòng)態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機(jī)制。

        SPI就是提供這樣的一個(gè)機(jī)制:為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)的機(jī)制。有點(diǎn)類似IOC的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要。所以SPI的核心思想就是解耦。

        SPI使用介紹

        要使用Java SPI,一般需要遵循如下約定:

        1. 當(dāng)服務(wù)提供者提供了接口的一種具體實(shí)現(xiàn)后,在jar包的META-INF/services目錄下創(chuàng)建一個(gè)以接口全限定名`為命名的文件,內(nèi)容為實(shí)現(xiàn)類的全限定名;
        2. 接口實(shí)現(xiàn)類所在的jar包放在主程序的classpath中;
        3. 主程序通過(guò)java.util.ServiceLoder動(dòng)態(tài)裝載實(shí)現(xiàn)模塊,它通過(guò)掃描META-INF/services目錄下的配置文件找到實(shí)現(xiàn)類的全限定名,把類加載到JVM;
        4. SPI的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法;

        SPI使用場(chǎng)景

        概括地說(shuō),適用于:調(diào)用者根據(jù)實(shí)際使用需要,啟用、擴(kuò)展、或者替換框架的實(shí)現(xiàn)策略

        以下是比較常見(jiàn)的例子:

        1. 數(shù)據(jù)庫(kù)驅(qū)動(dòng)加載接口實(shí)現(xiàn)類的加載 JDBC加載不同類型數(shù)據(jù)庫(kù)的驅(qū)動(dòng)
        2. 日志門(mén)面接口實(shí)現(xiàn)類加載 SLF4J加載不同提供商的日志實(shí)現(xiàn)類
        3. Spring Spring中大量使用了SPI,比如:對(duì)servlet3.0規(guī)范對(duì)ServletContainerInitializer的實(shí)現(xiàn)、自動(dòng)類型轉(zhuǎn)換Type Conversion SPI(Converter SPI、Formatter SPI)等
        4. Dubbo Dubbo中也大量使用SPI的方式實(shí)現(xiàn)框架的擴(kuò)展, 不過(guò)它對(duì)Java提供的原生SPI做了封裝,允許用戶擴(kuò)展實(shí)現(xiàn)Filter接口

        SPI簡(jiǎn)單例子

        先定義接口類

        package com.test.spi.learn;
        import java.util.List;

        public interface Search {
            public List<String> searchDoc(String keyword);   
        }

        文件搜索實(shí)現(xiàn)

        package com.test.spi.learn;
        import java.util.List;

        public class FileSearch implements Search{
            @Override
            public List<String> searchDoc(String keyword) {
                System.out.println("文件搜索 "+keyword);
                return null;
            }
        }

        數(shù)據(jù)庫(kù)搜索實(shí)現(xiàn)

        package com.test.spi.learn;
        import java.util.List;

        public class DBSearch implements Search{
            @Override
            public List<String> searchDoc(String keyword) {
                System.out.println("數(shù)據(jù)庫(kù)搜索 "+keyword);
                return null;
            }
        }

        接下來(lái)可以在resources下新建META-INF/services/目錄,然后新建接口全限定名的文件:com.test.spi.learn.Search

        里面加上我們需要用到的實(shí)現(xiàn)類

        com.test.spi.learn.FileSearch
        com.test.spi.learn.DBSearch

        然后寫(xiě)一個(gè)測(cè)試方法

        package com.test.spi.learn;
        import java.util.Iterator;
        import java.util.ServiceLoader;

        public class TestCase {
            public static void main(String[] args) {
                ServiceLoader<Search> s = ServiceLoader.load(Search.class);
                Iterator<Search> iterator = s.iterator();
                while (iterator.hasNext()) {
                   Search search =  iterator.next();
                   search.searchDoc("hello world");
                }
            }
        }

        可以看到輸出結(jié)果:

        文件搜索 hello world
        數(shù)據(jù)庫(kù)搜索 hello world

        SPI原理解析

        通過(guò)查看ServiceLoader的源碼,梳理了一下,實(shí)現(xiàn)的流程如下:

        1. 應(yīng)用程序調(diào)用ServiceLoader.load方法 ServiceLoader.load方法內(nèi)先創(chuàng)建一個(gè)新的ServiceLoader,并實(shí)例化該類中的成員變量,包括以下:

        loader(ClassLoader類型,類加載器) acc(AccessControlContext類型,訪問(wèn)控制器) providers(LinkedHashMap<String,S>類型,用于緩存加載成功的類) lookupIterator(實(shí)現(xiàn)迭代器功能)

        1. 應(yīng)用程序通過(guò)迭代器接口獲取對(duì)象實(shí)例 ServiceLoader先判斷成員變量providers對(duì)象中(LinkedHashMap<String,S>類型)是否有緩存實(shí)例對(duì)象,

        如果有緩存,直接返回。如果沒(méi)有緩存,執(zhí)行類的裝載,實(shí)現(xiàn)如下:

        (1) 讀取META-INF/services/下的配置文件,獲得所有能被實(shí)例化的類的名稱,值得注意的是,ServiceLoader可以跨越j(luò)ar包獲取META-INF下的配置文件
        (2) 通過(guò)反射方法Class.forName()加載類對(duì)象,并用instance()方法將類實(shí)例化。
        (3) 把實(shí)例化后的類緩存到providers對(duì)象中,(LinkedHashMap<String,S>類型) 然后返回實(shí)例對(duì)象。

        總結(jié)

        優(yōu)點(diǎn)

        使用SPI機(jī)制的優(yōu)勢(shì)是實(shí)現(xiàn)解耦,使得接口的定義與具體業(yè)務(wù)實(shí)現(xiàn)分離,而不是耦合在一起。應(yīng)用進(jìn)程可以根據(jù)實(shí)際業(yè)務(wù)情況啟用或替換具體組件。

        缺點(diǎn)

        1. 不能按需加載。雖然ServiceLoader做了延遲載入,但是基本只能通過(guò)遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化一遍。如果你并不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
        2. 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過(guò) Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來(lái)獲取對(duì)應(yīng)的實(shí)現(xiàn)類。
        3. 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。
        4. 加載不到實(shí)現(xiàn)類時(shí)拋出并不是真正原因的異常,錯(cuò)誤很難定位。

        看到上面這么多的缺點(diǎn),你肯定會(huì)想,有這些弊端為什么還要使用呢,沒(méi)錯(cuò),在重構(gòu)的過(guò)程中,SPI接口化是一個(gè)非常有用的方式,當(dāng)你需要擴(kuò)展的時(shí)候,適配的時(shí)候,越早的使用你就會(huì)受利越早,在一個(gè)合適的時(shí)間,恰當(dāng)?shù)臋C(jī)會(huì)的時(shí)候,就鼓起勇氣,重構(gòu)吧!


        好了。今天就說(shuō)到這了,我還會(huì)不斷分享自己的所學(xué)所想,希望我們一起走在成功的道路上!

        瀏覽 49
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 六十路性爱视频 | 成人免费在线观看毛片 | 亚洲v不卡ww在线 | 国产精品成人AV电影 | 亚洲高清欧美 |