如果說 正常的重構(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)
好家伙愤炸,所有的第三方存儲都是寫在一個(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):
是不是清楚多了,之后哪怕某個(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ǔ)的例子:
一些 基本的原則 我覺得還是需要了解的
- 盡量避免過多過長的創(chuàng)建Java對象
- 盡量使用局部變量
- 盡量使用StringBuilder和StringBuffer進(jìn)行字符串連接
- 盡量減少對變量的重復(fù)計(jì)算
- 盡量在finally塊中釋放資源
- 盡量緩存經(jīng)常使用的對象
- 不使用的對象及時(shí)設(shè)置為null
- 盡量考慮使用靜態(tài)方法
- 盡量在合適的場合使用單例
- 盡量使用final修飾符
下面是關(guān)于類和方法優(yōu)化:
- 重復(fù)代碼的提取
- 冗長方法的分割
- 嵌套條件分支或者循環(huán)遞歸的優(yōu)化
- 提取類或繼承體系中的常量
- 提取繼承體系中重復(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ī)制過程
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舱殿,一般需要遵循如下約定:
- 當(dāng)服務(wù)提供者提供了接口的一種具體實(shí)現(xiàn)后,在jar包的 META-INF/services 目錄下創(chuàng)建一個(gè)以接口全限定名`為命名的文件,內(nèi)容為實(shí)現(xiàn)類的全限定名;
- 接口實(shí)現(xiàn)類所在的jar包放在主程序的 classpath 中;
- 主程序通過 java.util.ServiceLoder 動(dòng)態(tài)裝載實(shí)現(xiàn)模塊樟氢,它通過掃描 META-INF/services 目錄下的配置文件找到實(shí)現(xiàn)類的全限定名埠啃,把類加載到JVM;
- SPI的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法;
SPI使用場景
概括地說,適用于:調(diào)用者根據(jù)實(shí)際使用需要碴开,啟用、擴(kuò)展潦牛、或者替換框架的實(shí)現(xiàn)策略
以下是比較常見的例子:
- 數(shù)據(jù)庫驅(qū)動(dòng)加載接口實(shí)現(xiàn)類的加載 JDBC加載不同類型數(shù)據(jù)庫的驅(qū)動(dòng)
- 日志門面接口實(shí)現(xiàn)類加載 SLF4J加載不同提供商的日志實(shí)現(xiàn)類
- Spring Spring中大量使用了SPI,比如:對servlet3.0規(guī)范對ServletContainerInitializer的實(shí)現(xiàn)、自動(dòng)類型轉(zhuǎn)換Type Conversion SPI(Converter SPI朴爬、Formatter SPI)等
- 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)的流程如下:
- 應(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)迭代器功能)
- 應(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)
- 不能按需加載痊硕。雖然 ServiceLoader 做了延遲載入押框,但是基本只能通過遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化一遍盒揉。如果你并不想用某些實(shí)現(xiàn)類兑徘,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了挂脑,這就造成了浪費(fèi)。
- 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活谴分,只能通過 Iterator 形式獲取镀脂,不能根據(jù)某個(gè)參數(shù)來獲取對應(yīng)的實(shí)現(xiàn)類忘伞。
- 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的氓奈。
- 加載不到實(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)吧胞皱!