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

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

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

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

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

這些代碼有點(diǎn)臭赠橙,重構(gòu)大法帶你秀(SPI接口化)

好家伙愤炸,所有的第三方存儲都是寫在一個(gè)模塊中的,各種 阿里云规个,騰訊云凤薛,華為云 等等,這樣的代碼架構(gòu)在前期可能在 不需要經(jīng)常擴(kuò)展缤苫,二開 的時(shí)候墅拭,還是能用的活玲。

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

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

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

這些代碼有點(diǎn)臭色瘩,重構(gòu)大法帶你秀(SPI接口化)

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

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

重構(gòu)

重構(gòu)是什么?

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

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

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

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

而且高層次的 重構(gòu) 依然是由無數(shù)個(gè)小目標(biāo)構(gòu)成,而不是長時(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ù)代碼,過長函數(shù),過大的類,過長參數(shù)列,發(fā)散式變化,霰彈式修改,依戀情結(jié),數(shù)據(jù)泥團(tuán),基本類型偏執(zhí),平行繼承體系,冗余類等

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

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

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

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

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

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

SPI

什么是SPI?

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

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

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

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

image.png

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

系統(tǒng)設(shè)計(jì)的各個(gè)抽象铅协,往往有很多不同的實(shí)現(xiàn)方案,在面向的對象的設(shè)計(jì)里亿胸,一般推薦模塊之間基于接口編程预皇,模塊之間不對實(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. 主程序通過 java.util.ServiceLoder 動(dòng)態(tài)裝載實(shí)現(xiàn)模塊樟氢,它通過掃描 META-INF/services 目錄下的配置文件找到實(shí)現(xiàn)類的全限定名埠啃,把類加載到JVM;
  4. SPI的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法;

SPI使用場景

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

以下是比較常見的例子:

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

SPI簡單例子

先定義接口類

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ù)庫搜索實(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ù)庫搜索 "+keyword);
        return null;
    }
}

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

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

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

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

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ù)庫搜索 hello world

SPI原理解析

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

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

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

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

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

(1) 讀取META-INF/services/下的配置文件束亏,獲得所有能被實(shí)例化的類的名稱阵具,值得注意的是,ServiceLoader可以跨越j(luò)ar包獲取META-INF下的配置文件

(2) 通過反射方法Class.forName()加載類對象阳液,并用instance()方法將類實(shí)例化趁舀。

(3) 把實(shí)例化后的類緩存到providers對象中,(LinkedHashMap<String,S>類型) 然后返回實(shí)例對象矮烹。

總結(jié)

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

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

缺點(diǎn)

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末九妈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宴树,更是在濱河造成了極大的恐慌晶疼,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锭吨,死亡現(xiàn)場離奇詭異寒匙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)考蕾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門辕翰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狈谊,“玉大人沟沙,你說我怎么就攤上這事壁榕。” “怎么了颊咬?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵牡辽,是天一觀的道長态辛。 經(jīng)常有香客問我,道長奏黑,這世上最難降的妖魔是什么熟史? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮碘菜,結(jié)果婚禮上限寞,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好缎岗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布传泊。 她就那樣靜靜地躺著,像睡著了一般拦盹。 火紅的嫁衣襯著肌膚如雪溪椎。 梳的紋絲不亂的頭發(fā)上恬口,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天沼侣,我揣著相機(jī)與錄音蛾洛,去河邊找鬼。 笑死轧膘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鳞滨。 我是一名探鬼主播椿浓,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼扳碍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了笋敞?” 一聲冷哼從身側(cè)響起夯巷,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喷兼,沒想到半個(gè)月后后雷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡勉抓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年藕筋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了梳码。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伍掀。...
    茶點(diǎn)故事閱讀 40,115評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翎承,死狀恐怖叨咖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情甸各,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布聘惦,位于F島的核電站善绎,受9級特大地震影響诫尽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牧嫉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一酣藻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辽剧,春花似錦怕轿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渡紫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惕澎,已是汗流浹背唧喉。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留董朝,地道東北人干跛。 一個(gè)月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像哥捕,于是被迫代替她去往敵國和親嘉熊。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評論 2 355

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