呦呦,這些代碼有點臭,重構大法帶你秀(SPI接口化),skr~

如果說 正常的重構是爲了消除代碼的壞味道, 那麼高層次的重構就是消除架構的壞味道

最近由於需要將公司基礎架構的組件進行各種兼容,適配以及二開,所以很多時候就需要對組件進行重構,大家是不是在拿到公司老項目老代碼,又需要二開或者重構的時候,會頭很大,無從下手,我之前也一直是這樣的狀態,不過在慢慢熟悉了一些重構的思想和方法之後,就能稍微的得心應手一些,下面我就開始講下重構,然後會着重講下重構中的SPI接口化

先給大家看看最近通過使用SPI接口化重構的一個組件-分佈式存儲

重構前的代碼結構

好傢伙,所有的第三方存儲都是寫在一個模塊中的,各種阿里雲,騰訊雲,華爲雲等等,這樣的代碼架構在前期可能在不需要經常擴展,二開的時候,還是能用的。

但是當某個新需求來的時候,比如我遇到的:需要支持多個雲的多個賬號上傳下載功能,這個是因爲在不同的雲上,不同賬號的權限,安全認證等都是不太一樣的,所以在某一刻,這個需求就被提出來了,也就是你想上傳到哪個雲的哪個賬號都可以。

然後拿到這個代碼,看了下這樣的架構,可能在這樣的基礎上完成需求也是沒有問題的,但是擴展很麻煩,而且代碼會越來越繁重,架構會越來越複雜,不清晰

所以我索性趁着這個機會,就重構一把,和其他同事也商量了下,決定分模塊,SPI化,好處就是根據你想使用的引入對應的依賴,讓代碼架構更加清晰,後續更加容易擴展了!下面就是重構後的大體架構:

是不是清楚多了,之後哪怕某個雲存儲需要增加新功能,或者需要兼容更多的雲也是比較容易的了。

好了,下面就讓我們開始講講重構大法~


重構

重構是什麼?

重構(Refactoring)就是通過調整程序代碼改善軟件的質量、性能,使其程序的設計模式和架構更趨合理,提高軟件的擴展性和維護性。

重構最重要的思想就是讓普通程序員也能寫出優秀的程序。

把優化代碼質量的過程拆解成一個個小的步驟,這樣重構一個項目的巨大工作量就變成比如修改變量名、提取函數、抽取接口等等簡單的工作目標。

作爲一個普通的程序員就可以通過實現這些易完成的工作目標來提升自己的編碼能力,加深自己的項目認識,從而爲最高層次的重構打下基礎。

而且高層次的重構依然是由無數個小目標構成,而不是長時間、大規模地去實現。

重構本質是極限編程的一部分,完整地實現極限編程才能最大化地發揮重構的價值。而極限編程本身就提倡擁抱變化,增強適應性,因此分解極限編程中的功能去適應項目的需求、適應團隊的現狀纔是最好的操作模式

重構的重點

重複代碼,過長函數,過大的類,過長參數列,發散式變化,霰彈式修改,依戀情結,數據泥團,基本類型偏執,平行繼承體系,冗餘類等

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

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

  1. 儘量避免過多過長的創建Java對象
  2. 儘量使用局部變量
  3. 儘量使用StringBuilder和StringBuffer進行字符串連接
  4. 儘量減少對變量的重複計算
  5. 儘量在finally塊中釋放資源
  6. 儘量緩存經常使用的對象
  7. 不使用的對象及時設置爲null
  8. 儘量考慮使用靜態方法
  9. 儘量在合適的場合使用單例
  10. 儘量使用final修飾符

下面是關於類和方法優化:

  1. 重複代碼的提取
  2. 冗長方法的分割
  3. 嵌套條件分支或者循環遞歸的優化
  4. 提取類或繼承體系中的常量
  5. 提取繼承體系中重複的屬性與方法到父類

這裏先簡單介紹這些比較常規的重構思想和原則,方法,畢竟今天的主角是SPI,下面有請SPI登場!

SPI

什麼是SPI?

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啓用框架擴展和替換組件。

它是一種服務發現機制,它通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動加載文件裏所定義的類。

這一機制爲很多框架擴展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機制

下面就是SPI的機制過程

SPI實際上是基於接口的編程+策略模式+配置文件組合實現的動態加載機制

系統設計的各個抽象,往往有很多不同的實現方案,在面向的對象的設計裏,一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。

一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。爲了實現在模塊裝配的時候能不在程序裏動態指明,這就需要一種服務發現機制。

SPI就是提供這樣的一個機制:爲某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。

SPI使用介紹

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

  1. 當服務提供者提供了接口的一種具體實現後,在jar包的 META-INF/services目錄下創建一個以接口全限定名`爲命名的文件,內容爲實現類的全限定名;
  2. 接口實現類所在的jar包放在主程序的 classpath中;
  3. 主程序通過 java.util.ServiceLoder動態裝載實現模塊,它通過掃描 META-INF/services目錄下的配置文件找到實現類的全限定名,把類加載到JVM;
  4. SPI的實現類必須攜帶一個不帶參數的構造方法;

SPI使用場景

概括地說,適用於:調用者根據實際使用需要,啓用、擴展、或者替換框架的實現策略

以下是比較常見的例子:

  1. 數據庫驅動加載接口實現類的加載 JDBC加載不同類型數據庫的驅動
  2. 日誌門面接口實現類加載 SLF4J加載不同提供商的日誌實現類
  3. Spring Spring中大量使用了SPI,比如:對servlet3.0規範對ServletContainerInitializer的實現、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等
  4. Dubbo Dubbo中也大量使用SPI的方式實現框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實現Filter接口

SPI簡單例子

先定義接口類

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

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

文件搜索實現

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

數據庫搜索實現

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

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

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

裏面加上我們需要用到的實現類

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

然後寫一個測試方法

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

可以看到輸出結果:

文件搜索 hello world
數據庫搜索 hello world

SPI原理解析

通過查看ServiceLoader的源碼,梳理了一下,實現的流程如下:

  1. 應用程序調用ServiceLoader.load方法 ServiceLoader.load方法內先創建一個新的ServiceLoader,並實例化該類中的成員變量,包括以下:

loader(ClassLoader類型,類加載器) acc(AccessControlContext類型,訪問控制器) providers(LinkedHashMap<String,S>類型,用於緩存加載成功的類) lookupIterator(實現迭代器功能)

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

如果有緩存,直接返回。如果沒有緩存,執行類的裝載,實現如下:

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

總結

優點

使用SPI機制的優勢是實現解耦,使得接口的定義與具體業務實現分離,而不是耦合在一起。應用進程可以根據實際業務情況啓用或替換具體組件。

缺點

  1. 不能按需加載。雖然 ServiceLoader做了延遲載入,但是基本只能通過遍歷全部獲取,也就是接口的實現類得全部載入並實例化一遍。如果你並不想用某些實現類,或者某些類實例化很耗時,它也被載入並實例化了,這就造成了浪費。
  2. 獲取某個實現類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據某個參數來獲取對應的實現類。
  3. 多個併發多線程使用 ServiceLoader 類的實例是不安全的。
  4. 加載不到實現類時拋出並不是真正原因的異常,錯誤很難定位。

看到上面這麼多的缺點,你肯定會想,有這些弊端爲什麼還要使用呢,沒錯,在重構的過程中,SPI接口化是一個非常有用的方式,當你需要擴展的時候,適配的時候,越早的使用你就會受利越早,在一個合適的時間,恰當的機會的時候,就鼓起勇氣,重構吧!


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

本文分享自微信公衆號 - 武培軒(wupeixuan404)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章