歡迎光臨
每天分享高質量文章

Android 組件化之路

作者:WuRichard

鏈接:https://www.jianshu.com/p/84978f992e61

首先先分清楚兩個概念:

模塊化

模塊化編程是將一個程式按照功能拆分成相互獨立的若干模塊,它強調將程式的功能分離成獨立的、可替換的模塊。每個模塊內只有與其相關功能的內容。

模塊化編程和結構化編程與面向物件編程是密切相關的,它們的目的都是將大型軟體程式劃分成一個個更小的部分。模塊化編程的粒度會更“粗”一些。在Java9中也在編譯器層面提供了模塊化的支持:Java Platform Module System 。

組件是一個類似的概念,但通常指更高的級別;組件是整個系統的一部分,而模塊是單個程式的一部分。“模塊”一詞因語言而有很大差異;在Python中,它非常小,每個檔案都是一個模塊,而在Java 9中,它是非常大的,其中模塊是包的集合,包又是檔案的集合。

在面向物件編程中,通常使用接口作為模塊間通信的橋梁,也就是基於接口的編程。

組件化

組件化開發是軟體工程的一個分支,它強調對給定軟體系統中廣泛可用的功能進行分割。基於可重用的目的將一個大的軟體系統拆分成多個獨立的組件,減少系統耦合度。

組件化開發中認為組件作為系統的一部分,是可獨立運行的服務,維基百科中舉了一個例子:在web服務中,有一種面向服務的架構設計–service-oriented architectures (SOA),這種架構設計從業務角度出發,利用企業現有的各種軟體體系,重新整合併構建起一套新的軟體架構。這套軟體架構可以隨著業務的變化,隨時靈活地結合現有服務,組成一個新的軟體。增加應用系統的靈活性。

組件可以產生或者消費事件,也可以應用於事件驅動架構。

  • 組件之間通過接口進行通信

  • 組件是可替換的,如果後續組件滿足初始組件的需求(通過接口表示),則組件可以替換另一個組件(在設計時或運行時),因此可以用更新的版本或替代的版本替換組件,而不會破壞系統的運行。

  • 一個判斷可替換組件的經驗法則是:如果組件B至少提供了A提供的組件,並且使用的組件不超過A使用的組件,那麼組件B可以立即替換組件A

  • 當組件直接與用戶交互時,應該考慮基於組件的可用性測試。

  • 組件需要是完全文件化、全面測試、具有全面的輸入效度檢查的。

模塊化 or 組件化

不管是模塊化還是組件化,都不是一個新的設計思想,它們最早都是在20世紀60年代就已經被提出了,但是早期的移動應用由於相對簡單,本身邏輯功能也不多,所以在移動端的應用反而沒那麼廣泛。(雖然Java最開始的模塊化是針對在移動和嵌入式設備上的應用設計的)。

從上面的概述來看其實組件化跟模塊化沒有明顯的區別;一個登錄功能可以是一個模塊也可以是一個組件,一個日期選擇控制元件可以是一個模塊,也可以是一個組件,因為不管是模塊化還是組件化,它們都有一個共同的標的:將一個大的軟體系統細化成一個個模塊或者組件,都是為了重用和解耦。因此沒有一個明確的界線去區分它們。

網上很多文章對於組件和模塊的定義也是不盡相同的,一些人認為組件的粒度更細,它只是具備單一功能與業務無關的組件,比如一個日曆選擇控制元件就認為是一個組件。而模塊他們認為就是業務模塊,顧名思義,就是按業務劃分而成的模塊。而另一部分人則相反。

在維基百科對模塊化的解釋中有這麼一句:

A component is a similar concept, but typically refers to a higher level; a component is a piece of a whole system, while a module is a piece of an individual program

也就是認為組件粒度較模塊要更大,所以本文對組件和模塊做出以下定義:

  • 組件:側重於業務,可編譯成單獨的app,一般只負責單一業務,具備自身的生命周期(通常包含Android四大組件的一個或多個,所以稱之為組件也更加貼切)

  • 模塊:側重於功能,與業務無關,比如自定義控制元件、網絡請求庫、圖片加載庫等

而從Android Studio推出之後,我們在開發專案時也會有意識的將一些可重用的代碼邏輯抽離成一個個的Module,這也就是模塊化開發的雛形。當然,組件化開發也不是就盡善盡美的,下麵列舉了它的一些優缺點:

優點一個複雜的系統可以由一個個組件集合而成,甚至於不同的組合可以構建出不同的系統。每個組件有獨立的版本,可獨立編譯、打包,大大提高了系統的靈活性以及開發人員的開發效率。應用的更新可以精細到組件,組件的升級替換不會影響到其它組件,也不會受其它組件的限制。

基於組件化架構設計的應用比傳統的“單片”設計可重用性高得多,因為這些組件可以在其他專案中重用,而且開發人員無需瞭解整個應用,可以只專註於分配給他們的較小的任務,提高開發效率。

缺點組件化的實施對開發人員和團隊管理人員提出了更高水平的要求,專案管理難度更大。組件間如何進行通信也是需要慎重考慮的。萬事開頭難,在對一個專案進行組件化分解時就好像庖丁解牛一般,你需要瞭解專案的“肌理筋骨”,才知道從何處下“刀”,才能更輕易的去分解專案,這就要求架構師對於專案的整體需求瞭如指掌。

下麵就來談談我的組件化之路。。。

首先我負責的專案類似於一個遠程控制應用,它與服務器建立Socket連接,接收服務器發送過來的指令,針對這些指令對當前Android設備執行關機、安裝應用等操作。應用本身也會收集一些設備信息如應用運行日誌,使用時長等,在某個指定的時間點上傳至服務器。理想的組件間依賴關係是這樣的:

組件化架構.jpg

其中基礎模塊不能脫離主工程獨立運行,組件之間不能直接依賴,組件間通信方式可以是接口也可以是事件總線。團隊中的開發人員只需要關註自身負責的組件(在開發樣式下各組件會轉化為可單獨運行的App,說白了就是在build.gradle檔案中將apply plugin: ‘com.android.library’改為apply plugin: ‘com.android.application’,網上有很多相關資料,在此就不贅述了)。

現在來了個開發需求需要改動組件Component1內部的邏輯,團隊中的小A是負責該組件的開發人員,在接到需求後,小A啪啪啪一頓猛如虎的操作完成需求後,對該組件進行單元測試,檢查組件輸入輸出,測試通過後提交代碼,審核通過後構建平臺構建、打包、發佈,整個過程完全沒有“驚動”其他組件,Perfect!

然而現實是殘酷的。

由於組件間不可能完全不通信,所以現實情況組件之間的依賴關係有可能是這樣的:

現實情況依賴關係.jpg

對比上圖,組件之間顯得更加“親密無間”了,而且這還不是糟糕的情況,當組件越來越多,各種相互依賴,循壞依賴的問題會讓你痛不欲生。

因為組件之間不可避免的存在需要通信的情況,比如 Component1需要呼叫Component2的方法一般情況下我們都是直接通過類名或物件取用的方式去呼叫相應的方法。但是這種通信方式正是導致組件之間高度耦合的罪魁禍首,所以必須杜絕這種通信方式。

那麼問題來了,怎麼做到既能讓組件間通信又高度解耦呢?這就需要用到文章開頭提到的面向接口編程思想和依賴註入(或者叫依賴查找)技術。舉個?:

組件A中的Foo1類依賴組件BFoo2類中的bar方法,一種比較low的實現方式是:

//ComponentA
class Foo1 {
    private Foo2 mFoo2;
    public void main() {
        mFoo2 = new Foo2();
        mFoo2.bar();
    }
}

//ComponentB
class Foo2 {
    public void bar() {
        //nop
    }
}

這種實現方式違反了控制反轉設計原則,耦合度高,假如這時需求變更了,需要使用組件C的Foo3類中的bar()方法去替換原來的實現,那這下樂子就大了。

而通過面向接口編程以及依賴註入技術我們能很好的遵循控制反轉設計原則:

//Common Component
interface IBar {
    void bar()
}

//ComponentA
class Foo1 {
    private IBar mBar;

    public void main() {
        if (mBar !null) {
            mBar.bar();
        }
    }

    public void setBar(IBar bar) {
        mBar = bar;
    }
}

//ComponentB
class Foo2 implements IBar {

    @Override
    public void bar() {
        //nop
    }
}

//ComponentC
class Foo3 implements IBar {

    @Override
    public void bar() {
        //nop
    }
}

這就是經典的實現了控制反轉的示例代碼,Foo1類只知道自己需要一個實現了IBar接口的實體,然後呼叫接口的bar()方法,至於是誰去實現的這個接口,不好意思,它壓根不關心。

雖然你Foo1類是舒服了,把依賴關係交給外部去解決了,但是總要有人去負責這部分的工作吧。這時候依賴註入容器(IOC容器)就登場了,如果對web開發有所瞭解的同學肯定不會感到陌生,Spring就是一個IOC容器,這個容器把依賴查找,類實體化(其實就是根據類的路徑名稱通過反射進行實體化)這些臟活累活攬在身上,這樣既實現了控制反轉又極大提高了應用的靈活性和可維護性。

正因為依賴註入能有效地降低代碼之間的耦合度,所以基於依賴註入實現的組件化框架(路由框架)也就應運而生了,目前主流的Android組件化框架有ARouter、CC、DDComponentForAndroid、ActivityRouter等等,我自己也使用Kotlin基於kapt技術實現了一個路由框架KRouter。

雖然相關的框架有很多,但是它們實現原理不外乎兩種:

  • 一種是將分佈在各個組件的類按照一定的規則在內部生成映射表,這個映射表的資料結構通常是一個Map,Key是一個字串,Value是一個類或者是類的路徑名稱(用於通過反射進行類的實體化)。通俗來說就是類的查找,這種實現方式要求呼叫方和被呼叫方都持有接口類,通常這些共同持有的接口類會被定義在一個Common基礎模塊中,而且在運行時這些相互通信的組件必須打包到同一個APK中。這種實現方式導致無法真正實現代碼隔離(需要通信的兩個組件仍然是存在依賴關係的),基於這種原理實現的組件化架構“自約束能力”很弱,因為無法約束開發人員通過直接取用的方式進行通信的行為,雖然一開始設計人員想的很美好,但是開發人員在實現時做出來的產品卻不是那樣,因為“自約束能力”弱的架構設計是通過“編碼規範”、“測試驅動”甚至是“人員熟練度”來保證開發人員實現的代碼符合設計人員的設計初衷,而且這種架構也無法保證後續接手維護專案的開發人員能夠貫徹原本的設計思想,隨著時間推移,專案往越來越糟糕的方向演進(解決這個問題最好的方案就是從編譯器層面進行約束,也就是把問題攔截在編碼階段,然而Java9才支持模塊化開發,Android目前還處於支持部分Java8的特性的階段,路還很長)。

  • 另一種方案是基於事件總線的方式實現組件之間的通信,不再是面向接口編程,而是面向通信協議編程,可以理解為組件間的呼叫類似http請求。這些框架會在內部建立跨行程通信的連接(也就是事件總線),這條事件總線負責分發路由請求以及傳回執行結果。這種實現方式的好處是真正可以實現代碼隔離,組件可以運行在獨立的行程中,但是只支持基本型別引數的轉發。實現跨行程通信有很多方案,比如Android原生的四大組件、Socket、FileObserver、MemoryFile、基於AIDL的Messager等等,使用Android原生的好處是安全性方面的工作由Android幫我們完成了,而使用Socket則需要自己實現加密Socket。

第一種方案適合小型的專案,因為這些專案通常都是單行程的,雖然這樣設計的架構“自約束能力”弱,但是目前大多數Android專案團隊開發人數也不會太多,所以管理難度較小,而第二種實現方案則更適合跨行程組件化的專案(組件一般運行在獨立的行程中甚至一個組件就是一個APP)。

在我看來Android的組件化是存在3個階段的,第一個是從單工程專案過度到多模塊的階段;第二個是從多模塊過度到多組件的階段;第三個就是多組件獨立行程的階段。而目前大多數應用其實都是在第二個階段或者介於第二和第三個階段之間,所以對於這樣的專案,選擇一個既支持類查找方式,又支持事件總線的組件化框架是最合適的(這也是一開始設計KRouter想要達到的效果,雖然目前暫時不支持跨行程組件。

在專案實施組件化過程中,其實真正耗費時間、精力的不是編碼,而是一開始組件的劃分以及組件單元測試的代碼的編寫。有可能因為一開始對業務的不熟悉,導致後期開發時發現組件劃分的不夠準確,需要加以調整;或者是對接口抽象的不夠好,導致維護時頻繁修改接口;還有可能在編寫單元測試時覺得枯燥乏味而選擇放棄。我們不能因為遇到這些困難就半途而廢,或者是質疑自己的架構設計能力,沒有哪一個架構設計是放之四海皆準的,有可能一個專案的架構設計放在另一個專案中就顯得不那麼合適了。

所以好的架構設計還需要設計人員“因地制宜”的對一個比較通用的架構骨架進行查漏補缺,最後使其與實際專案更加契合。

祝大家都能成為一個優秀的架構設計師。

已同步到看一看
赞(0)

分享創造快樂