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

        共 5887字,需瀏覽 12分鐘

         ·

        2021-04-11 23:51

        大家好,我是狼王,一個愛打球的程序員

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

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

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

        重構前的代碼結構

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

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

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

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

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

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


        重構

        重構是什么?

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

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

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

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

        而且高層次的重構依然是由無數(shù)個小目標構成,而不是長時間、大規(guī)模地去實現(xiàn)。

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

        重構的重點

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

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

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

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

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

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

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

        SPI

        什么是SPI?

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

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

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

        下面就是SPI的機制過程

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

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

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

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

        SPI使用介紹

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

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

        SPI使用場景

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

        以下是比較常見的例子:

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

        SPI簡單例子

        先定義接口類

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

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

        文件搜索實現(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ù)庫搜索實現(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

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

        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
        數(shù)據(jù)庫搜索 hello world

        SPI原理解析

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

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

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

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

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

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

        總結

        優(yōu)點

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

        缺點

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

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


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

        樂于輸出干貨的Java技術公眾號:狼王編程。公眾號內有大量的技術文章、海量視頻資源、精美腦圖,不妨來關注一下!回復資料領取大量學習資源和免費書籍!

        轉發(fā)朋友圈是對我最大的支持!

         覺得有點東西就點一下“贊和在看”吧!感謝大家的支持了!

        瀏覽 73
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 色五月婷婷av | 日本日批视频 | 免费啪啪网址 | 日本wwwwxxxx泡妞下课 | 黄片网址 |