Android 元件化之路

HanDrush發表於2019-04-04

首先先分清楚兩個概念:

模組化

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

模組化程式設計和結構化程式設計與物件導向程式設計是密切相關的,它們的目的都是將大型軟體程式劃分成一個個更小的部分。模組化程式設計的粒度會更“粗”一些。在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裝置執行關機、安裝應用等操作。應用本身也會收集一些裝置資訊如應用執行日誌,使用時長等,在某個指定的時間點上傳至伺服器。理想的元件間依賴關係是這樣的:

Android 元件化之路
元件化架構.jpg

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

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

然而現實是殘酷的。。

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

Android 元件化之路
現實情況依賴關係.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想要達到的效果,雖然目前暫時不支援跨程式元件。。。)

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

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

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

後續會持續更新Android專題知識,大家覺得不錯可以點個贊在關注下,以後還會分享更多文章!

相關文章