淺談項目重構之路——模塊化

忙了一個多月,一直沒時間寫文章。終于把項目重構完了,借此機會淺談一下對Android架構的見解。筆者將會把重構分為三個部分講解。
本文為全局架構,主要設計模塊化架構開發。
上一篇為概述篇
下一篇為組件化+MVP篇

[如有解釋錯誤的地方,歡迎評論區指正探討]


模塊化能解決什么問題

先來看一下筆者項目的舊版架構:

全局架構-舊.png

是不是很眼熟這樣的架構?整個應用即為一個工程,所有業務之間不存在編譯隔離,所以可以互相引用。對于早期小型的App而言,這樣的架構清晰簡單,同時也便于快速開發。
不過隨著業務的積攢,整個App變得臃腫,這樣的架構不僅容易出現模塊耦合問題,同時容易造成開發混亂,改一處地方卻涉及到多個模塊。

在上一篇文章中,筆者也有提到為什么需要重構,并提出使用模塊化進行重構,那么我們來看看,使用模塊化能解決什么問題:

  • 解決由于模塊邊界定義不清而導致的耦合問題
  • 統一規定模塊之間通信方式,去除過分使用EventBus而臃腫的event包
  • 隔離各個模塊代碼,利于并行開發測試
  • 可單獨編譯打包某一模塊,提升開發效率
  • 模塊實現可復用,快速集成影子App
  • 開發時,可以進行單業務編譯,避免全量編譯耗時過長

這些問題都是從筆者的項目中反應出來的,也正是解決代碼劣化的關鍵。

什么是模塊化

講了那么久模塊化,那么到底什么是模塊化?網上對于模塊化的解釋有很多,基本上每個人的解釋都不太一樣,往往模塊化組件化總被混淆在一起。
這大概是因為組件和模塊在英文翻譯里都被叫為module,而在AS中lib模塊都被定義為module。所以這些module都容易被混淆在一起。

組件

這里提到的組件,翻譯成module并不準確,他其實是一個通用的Lib,只不過組件在AS中的實現,多數以module的形式實現。在Android App中,組件應該是構成業務模塊或業務功能的基本單位

舉個例子,筆者項目中存在類似朋友圈一樣的業務,那么必不可少的就需要一個圖片上傳組件Uploader
這里的Uploader不管是功能上還是業務上都無法繼續拆分,所以Uploader組件而并非Uploader模塊,朋友圈才可以稱之為模塊。

對于組件化,其實也是本次重構方案的關鍵之一,不過筆者將其歸為局部架構里的內容,所以在這里只簡單介紹一下概念,不展開過多描述。

模塊

對于模塊,這才是真正意義上的module。模塊由多個組件甚至多個模塊構成,并通過特定的邏輯講這些組件連接起來實現一定的業務。

還是以剛才的朋友圈為例子,朋友圈將網絡組件,上傳組件,日志組件,圖片組件通過特定的邏輯構成其特定的業務。對于微信朋友圈,其內部可能還有他特有的廣告模塊,Gps模塊等等,所以說模塊也可能由多個小模塊構成。

模塊具有可拆分性,正如朋友圈,我們可以將其拆分成多個組件。
模塊一般與業務相關聯
一個健康的模塊應該具有可復用性,要做這點,必然要和其他模塊保持獨立。
聽上去可服用性和業務相關聯,似乎互相矛盾,其實不然,如果這里的可復用性沒有組件的復用性那么強,強調的是與其他模塊保持獨立,假如兩個app都有朋友圈業務,那么大可以復用該模塊,改改ui即可。

區別

通過上面的介紹,其實也就大致了解了什么模塊,什么是組件。

簡而言之: 模塊 = 組件A + 組件B + …… 組件B

其實歸根結底,只要目的確定,把臃腫的工程,拆分為更小的部分,解耦各種復雜的邏輯,便于代碼管理。管他叫什么模塊還是什么。

技術難點

為了實現模塊化,并使各個模塊達成上述特性,筆者將整個過程劃分為三個問題,也是三個技術難點。

  1. 隔離模塊邊界
  2. 模塊間的跳轉
  3. 模塊間的通信

接下來,將一一解答這些問題。

隔離模塊邊界

對于以前的App而言,為了避免耦合問題,采取以包為分界,同時筆者的團隊制定了一系列代碼規范,然而,在趕工的情況下,并不是所有人都能遵守這套規范,尤其是剛進來并不熟悉團隊的新伙伴。因此,要想從根本上隔離代碼,解決耦合問題,在編譯上約束權限是最佳的方法

那么如何做到編譯時的約束呢?很顯然,這就需要將原本以包為分界的模塊抽出來以AS中的module形式隔離。
同時制定規則,模塊與模塊直接不允許同時直接產生依賴關系
對于多個模塊通用的組件,應該采取先前提及的組件化,同樣以module的形式隔離。這一塊將在下一篇文章中敘述。

規則

對于模塊的劃分,需要制定一定的規則,如果劃分粒度過小,那么會導致項目Module冗余,如果粒度過大,那么又會出現耦合問題,與初衷相悖。錯誤的劃分,將導致項目結構復雜。
因此,對于筆者的項目而言,指定這幾個規則來劃分:

  • 業務之間是否強關聯?強關聯應該合并
  • 共用的功能是否可組件化?可組件化應該拆分
  • 業務是否復雜?復雜應該拆分
  • 空殼模塊能否與其他空殼合并

舉個例子可能比較好懂,以微信為例,在首頁底部有四個tab:

微信首頁.jpg

可能你會這么想,底部四個tab就對應四個大模塊。
如果這么劃分的話,那么又該如何處理朋友圈,搖一搖等功能呢?都歸于發現模塊還是單獨開一個模塊呢?
顯然,如果都將朋友圈和搖一搖都歸為一個模塊,那么這個模塊將過度復雜,這兩者沒有明顯的業務關系,歸于一個模塊,很容易因為跳轉或信息通信產生耦合問題
如果單獨開一個模塊,那么顯然發現模塊將成為一個空殼,而四個tab,就對應了四個空殼,這就造成了Module冗余

那么應該如何處理好呢?
針對筆者定制的規則,我們一一考慮:

  1. 朋友圈與搖一搖之間業務并不是強關聯
  2. 這里并無復用功能
  3. 單純四個tab的業務并不復雜。朋友圈與搖一搖業務復雜
  4. 四個tab其實都可以作為空殼模塊,僅作為承載體

綜合考慮,我們應該合并四個空殼tab,拆分朋友圈與搖一搖。
所以項目結構如下:


微信項目結構.png

對于不同項目,實際情況可能比這里更復雜,這就需要對業務足夠了解,具有一定經驗了。目前筆者團隊劃分模塊時需要各業務Leader商討決定。

隔離好各個模塊,就應該來考慮模塊間跳轉,通信的問題了。雖然我們將不存在強關聯的模塊隔離開,但模塊之間終究需要通信與跳轉,這由應該如何處理呢?

模塊間跳轉

在我們隔離完模塊后,跳轉的問題也就出來了。因為編譯隔離,我們也就無法直接引用,不能通過的顯示方式跳轉。

隱性跳轉

既然顯示跳轉不行,自然而然的我們就想到隱式跳轉:

Intent intent=new Intent("action");   
startActivity(intent);  

不過使用隱式跳轉存在幾個問題:

  1. 每個模塊各自管理各自的AndroidManifest.xml,這就容易出現action重復的問題。
  2. 過多的Activity被導出,容易引發安全問題
  3. 可配置性較差Manifest限制于xml格式,書寫麻煩,配置復雜,可以自定義的東西也較少。
  4. 代碼寫起來繁瑣,出錯時難以定位問題
  5. 直接通過Intent的方式跳轉,跳轉過程開發者無法干預,一些面向切面的事情難以實施,比方說登錄、埋點這種非常通用的邏輯,在每個子頁面中判斷又很不合理,畢竟activity已經實例化了

顯然我們不可能采用難以管理的隱式跳轉。

路由跳轉

既然隱式跳轉不行,那我們只能另尋他法。
這里我們參考了路由器工作原理:

路由跳轉.png

很顯然,我們需要在路由器中維護一個路由表,也就是Activityurl的映射,在我們發出一個跳轉請求時,就由路由器去路由表中尋找映射并跳轉。

這樣的操作就類似于我們在瀏覽器中輸入www.baidu.com,我們本地完全不與百度產生依賴關系,卻可以跳轉訪問百度。百度是如何實現,是好是壞,我們完全不懂擔心,這樣的流程很適合我們的實現模塊化。

那么如何實現呢?
顯然路由器的核心是維護路由表,我們需要做的就是把每個Activity到路由器里。從原理上來看并不難實現,關鍵是如何做到好用易用。

這里筆者并沒有自己重復造輪子,而是選擇了阿里開源的框架ARouterARouter處理實現基本的路由功能外,還兼備攔截器降級策略等功能。
ARouter在實現維護路由表功能時,借助Annotation Processor來實現。這樣我們在使用時便十分方便,也不會在代碼中插入生硬的邏輯。
簡單的看一下使用:

添加注解

// 在支持路由的頁面上添加注解
@Route(path = "/baidu/index")
public class BaiDuActivity extend Activity {
}

執行跳轉

//  實現簡單的跳轉
ARouter.getInstance().build("/baidu/index").navigation();

是不是很簡單?這樣我們就仿造出了跳轉www.baidu.com的操作了。

簡單看看ARouter的工作流程,其實跟我們前面的講的原理差不多,需要提一下的是,ARouter在使用注解處理器的同時還使用了反射,經過測試,這里的反射很好的解決了模塊之間的耦合問題同時并不會出性能問題。

arouter.png

解決完跳轉問題,還有通信的問題要解決,比如朋友圈模塊需要使用用戶模塊的用戶信息。那么又該如何解決呢?

模塊間通信

在模塊獨立之后,模塊之間沒辦法直接耦合,所以原先的通信方式(setListenerstartActivityForResult)便失效了。所以,模塊化的一個關鍵便是如何實現與其他模塊保持獨立,又建立良好的通信方式。
我們需要尋找一種新的方案。

廣播

作為四大組件之一,我們借助Broadcast實現模塊之間的通訊,不過我們也知道,廣播作為一個重量級的通訊工具,并不適合頻繁通信,同時廣播僅支持基本數據類型可序列化對象,傳遞大數據時還有限制,可見局限性很大,并不適用。

EventBus

作為一個輕量級的通訊框架, EventBus解決了廣播存在的那些問題,同時十分靈活,**不依賴于上下文v,任何地方都可以進行通訊。重構之前的項目也有很多地方利用EventBus來進行通訊,確實實現了松耦合
不過EventBus也存在他的弊端:

  • 大量的通訊Event沉淀在Common層
  • 基于發布訂閱模式,注定無法主動獲取數據

這些弊端,讓我們最后放棄使用EventBus作為模塊之間的通訊工具,不過同一模塊內的通訊依舊可以選擇EventBus

協議通信

我們一開始參照了RPC機制, 也就是通過restful這樣的形式去進行通信。


協議通信.png

通過訪問定制的協議,經由路由器訪問相關的服務獲取數據,這種方式十分靈活,具備很強的解耦能力,但也有不可忽視的代價——高度文檔化
想必大家都有體驗過,我們開發時總是需要去翻閱后臺給我們的接口文檔,這樣的事情我們不想在本地通信時再次發生,不僅維護文檔困難,開發效率低下,也非常容易出錯。
我們希望協議的檢測能夠讓編譯器幫我們分擔,寫錯了編譯器會報錯,然而協議通信是依賴于文檔的,eg:www.baidu.com/getsomethings/id=xxx&passw=xxx,編譯器無法識別這樣的手寫是否符合協議,需要運行時才能發現錯誤。

說了好幾種常見的通訊方式都不行,那到底應該怎么做呢?

接口協議通信

既然協議通信不好用,那么有沒有辦法解決他高度依賴文檔問題。
方法是有的,就是改文檔化接口化。如果將原本由文檔規定的協議,交給接口來規定,那么編譯器就可以幫我們檢測協議是否正確了。
這也就是接口協議通信的原理。

簡單的看一下流程:


接口協議通信.png

和上面提到的協議通信很相似,多了Provider這一層次:

  1. ModuleB 向 Router 注冊 ProviderB 接口服務
  2. ModuleA 向 Router 請求 ProviderB 接口服務
  3. Router 返回 ProviderB 接口服務

這樣邊解決了原本的高度文檔化的問題,同時保持原來的靈活性和解耦能力。
剛好ARouter具備這樣的功能,于是我們也采用了ARouter的實現方案。使用起來是這樣的:

首先在公共組件(路由組件)中 聲明接口,其他組件通過接口來調用服務

public interface IProviderB extends IProvider {
    String getUserName();
}

然后在具體模塊中實現接口,并注冊

@Route(path = "/moduleb/providerb", name = "測試服務")
public class ProviderB implements IProviderB {

    @Override
    String getUserName(){
    }
}

在其他模塊中通過路由去尋找相關服務

IProviderB provider = (IPoviderB) ARouter.getInstance().build("/moduleb/providerb").navigation();

是不是同樣和很簡單?而且因為都使用了ARouter,所以調用操作與跳轉的操作很像,也就便于代碼的閱讀。

至此,我們就解決了模塊化的三個關鍵性問題。

再思考

解決完上述的上的三個關鍵性問題后,一個基于ARouter的模塊化架構也就誕生了,不過還存在一些問題。

app module

在我們隔離完業務模塊后,該如何處理app module呢?
在上面的微信的例子中,我們將app module作為home界面的載體,裝載了主界面的幾個空殼。那么app module就只做這樣的功能了嗎?
并不,app module作為特殊的一個模塊,鏈接著所以模塊的生命周期,也就包括了模塊的初始化與銷毀。
同時app module作為一個中介,可以實現一些簡單的模塊間通訊。

缺點

那么是否實現模塊化之后就高枕無憂了呢?并不。
模塊化很好的解決了模塊之間的耦合問題,同時便于進行單業務拆分編譯。但是也暴露幾個問題:

  1. 因為模塊數量的增加,全量編譯時間變長
    雖然我們平時開發時做到了單業務編譯,加快了編譯速度,但是最終打包合并的時候需要全量編譯,事實證明全量編譯的時間將隨著模塊數量的增加而增加。不過,這點還能接受。

  2. 模塊的劃分有時糾結不清
    當對模塊進行解耦時,即便大體上的業務劃分已經清晰,但因為業務間各種微妙的關系,細節上仍會遇到糾纏不清的情況。那么這個時候就會出現糾結于這個模塊到底該不該細分的問題。
    我們能做到的只是盡量讓他更加"面向對象",同時避免隨意拼湊和單純為了類型解耦而解耦的情況。

  3. 模塊劃分粒度容易過細,導致模塊數劇增
    這是筆者項目中實際遇到的問題,對于部分業務,功能比較零散,如果劃分多一個模塊或組件,這個模塊或組件又只有這個業務在使用。如果不劃分,又容易與這個業務里的其他功能耦合。
    引用微信模塊化的例子,對于Gallery模塊,內部還有存儲,編輯等小功能,如果直接與Gallery揉合,那么很容易就產生耦合問題,為此微信團隊提出了自己的解決方案,構建pins工程

    image.png

    這一塊筆者就不再闡述,可以跳轉微信的文章進行學習。

總結

對于中大型App而言,往往都積累了一些年份,很多時候,全局架構都停留最初的狀態,各個業務相互交叉耦合,這樣其實并不利于整個App的發展。代碼只會逐漸劣化,到最后發現拓展新業務時,需要大規模修改舊業務,那就為時已晚了。
所以,一個良好的項目周期,需要適時推動一些重構計劃,提高代碼質量,而并不是只停留在業務代碼層次。
看一下采用模塊化之后的項目架構,對比一下文章開頭的架構:

全局架構-新.png

模塊化的架構不僅解決了模塊耦合問題,同時也調高了整個App的拓展性與維護性。這樣的重構,何樂而不為?


最后希望筆者分享的一點經驗能對大家提高代碼有些幫助,如有錯誤的地方,歡迎指正探討。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372