淺談項(xiàng)目重構(gòu)之路——模塊化

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

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


模塊化能解決什么問題

先來看一下筆者項(xiàng)目的舊版架構(gòu):

全局架構(gòu)-舊.png

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

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

  • 解決由于模塊邊界定義不清而導(dǎo)致的耦合問題
  • 統(tǒng)一規(guī)定模塊之間通信方式,去除過分使用EventBus而臃腫的event包
  • 隔離各個模塊代碼,利于并行開發(fā)測試
  • 可單獨(dú)編譯打包某一模塊,提升開發(fā)效率
  • 模塊實(shí)現(xiàn)可復(fù)用,快速集成影子App
  • 開發(fā)時(shí),可以進(jìn)行單業(yè)務(wù)編譯,避免全量編譯耗時(shí)過長

這些問題都是從筆者的項(xiàng)目中反應(yīng)出來的,也正是解決代碼劣化的關(guān)鍵。

什么是模塊化

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

組件

這里提到的組件,翻譯成module并不準(zhǔn)確,他其實(shí)是一個通用的Lib,只不過組件在AS中的實(shí)現(xiàn),多數(shù)以module的形式實(shí)現(xiàn)。在Android App中,組件應(yīng)該是構(gòu)成業(yè)務(wù)模塊或業(yè)務(wù)功能的基本單位。

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

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

模塊

對于模塊,這才是真正意義上的module。模塊由多個組件甚至多個模塊構(gòu)成,并通過特定的邏輯講這些組件連接起來實(shí)現(xiàn)一定的業(yè)務(wù)。

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

模塊具有可拆分性,正如朋友圈,我們可以將其拆分成多個組件。
模塊一般與業(yè)務(wù)相關(guān)聯(lián)。
一個健康的模塊應(yīng)該具有可復(fù)用性,要做這點(diǎn),必然要和其他模塊保持獨(dú)立。
聽上去可服用性和業(yè)務(wù)相關(guān)聯(lián),似乎互相矛盾,其實(shí)不然,如果這里的可復(fù)用性沒有組件的復(fù)用性那么強(qiáng),強(qiáng)調(diào)的是與其他模塊保持獨(dú)立,假如兩個app都有朋友圈業(yè)務(wù),那么大可以復(fù)用該模塊,改改ui即可。

區(qū)別

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

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

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

技術(shù)難點(diǎn)

為了實(shí)現(xiàn)模塊化,并使各個模塊達(dá)成上述特性,筆者將整個過程劃分為三個問題,也是三個技術(shù)難點(diǎn)。

  1. 隔離模塊邊界
  2. 模塊間的跳轉(zhuǎn)
  3. 模塊間的通信

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

隔離模塊邊界

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

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

規(guī)則

對于模塊的劃分,需要制定一定的規(guī)則,如果劃分粒度過小,那么會導(dǎo)致項(xiàng)目Module冗余,如果粒度過大,那么又會出現(xiàn)耦合問題,與初衷相悖。錯誤的劃分,將導(dǎo)致項(xiàng)目結(jié)構(gòu)復(fù)雜。
因此,對于筆者的項(xiàng)目而言,指定這幾個規(guī)則來劃分:

  • 業(yè)務(wù)之間是否強(qiáng)關(guān)聯(lián)?強(qiáng)關(guān)聯(lián)應(yīng)該合并
  • 共用的功能是否可組件化?可組件化應(yīng)該拆分
  • 業(yè)務(wù)是否復(fù)雜?復(fù)雜應(yīng)該拆分
  • 空殼模塊能否與其他空殼合并

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

微信首頁.jpg

可能你會這么想,底部四個tab就對應(yīng)四個大模塊。
如果這么劃分的話,那么又該如何處理朋友圈,搖一搖等功能呢?都?xì)w于發(fā)現(xiàn)模塊還是單獨(dú)開一個模塊呢?
顯然,如果都將朋友圈和搖一搖都?xì)w為一個模塊,那么這個模塊將過度復(fù)雜,這兩者沒有明顯的業(yè)務(wù)關(guān)系,歸于一個模塊,很容易因?yàn)樘D(zhuǎn)或信息通信產(chǎn)生耦合問題。
如果單獨(dú)開一個模塊,那么顯然發(fā)現(xiàn)模塊將成為一個空殼,而四個tab,就對應(yīng)了四個空殼,這就造成了Module冗余。

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

  1. 朋友圈與搖一搖之間業(yè)務(wù)并不是強(qiáng)關(guān)聯(lián)
  2. 這里并無復(fù)用功能
  3. 單純四個tab的業(yè)務(wù)并不復(fù)雜。朋友圈與搖一搖業(yè)務(wù)復(fù)雜
  4. 四個tab其實(shí)都可以作為空殼模塊,僅作為承載體

綜合考慮,我們應(yīng)該合并四個空殼tab,拆分朋友圈與搖一搖。
所以項(xiàng)目結(jié)構(gòu)如下:


微信項(xiàng)目結(jié)構(gòu).png

對于不同項(xiàng)目,實(shí)際情況可能比這里更復(fù)雜,這就需要對業(yè)務(wù)足夠了解,具有一定經(jīng)驗(yàn)了。目前筆者團(tuán)隊(duì)劃分模塊時(shí)需要各業(yè)務(wù)Leader商討決定。

隔離好各個模塊,就應(yīng)該來考慮模塊間跳轉(zhuǎn),通信的問題了。雖然我們將不存在強(qiáng)關(guān)聯(lián)的模塊隔離開,但模塊之間終究需要通信與跳轉(zhuǎn),這由應(yīng)該如何處理呢?

模塊間跳轉(zhuǎn)

在我們隔離完模塊后,跳轉(zhuǎn)的問題也就出來了。因?yàn)?strong>編譯隔離,我們也就無法直接引用,不能通過的顯示方式跳轉(zhuǎn)。

隱性跳轉(zhuǎn)

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

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

不過使用隱式跳轉(zhuǎn)存在幾個問題:

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

顯然我們不可能采用難以管理的隱式跳轉(zhuǎn)。

路由跳轉(zhuǎn)

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

路由跳轉(zhuǎn).png

很顯然,我們需要在路由器中維護(hù)一個路由表,也就是Activityurl的映射,在我們發(fā)出一個跳轉(zhuǎn)請求時(shí),就由路由器去路由表中尋找映射并跳轉(zhuǎn)。

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

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

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

添加注解

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

執(zhí)行跳轉(zhuǎn)

//  實(shí)現(xiàn)簡單的跳轉(zhuǎn)
ARouter.getInstance().build("/baidu/index").navigation();

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

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

arouter.png

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

模塊間通信

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

廣播

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

EventBus

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

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

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

協(xié)議通信

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


協(xié)議通信.png

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

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

接口協(xié)議通信

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

簡單的看一下流程:


接口協(xié)議通信.png

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

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

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

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

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

然后在具體模塊中實(shí)現(xiàn)接口,并注冊

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

    @Override
    String getUserName(){
    }
}

在其他模塊中通過路由去尋找相關(guān)服務(wù)

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

是不是同樣和很簡單?而且因?yàn)槎际褂昧薃Router,所以調(diào)用操作與跳轉(zhuǎn)的操作很像,也就便于代碼的閱讀。

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

再思考

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

app module

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

缺點(diǎn)

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

  1. 因?yàn)槟K數(shù)量的增加,全量編譯時(shí)間變長
    雖然我們平時(shí)開發(fā)時(shí)做到了單業(yè)務(wù)編譯,加快了編譯速度,但是最終打包合并的時(shí)候需要全量編譯,事實(shí)證明全量編譯的時(shí)間將隨著模塊數(shù)量的增加而增加。不過,這點(diǎn)還能接受。

  2. 模塊的劃分有時(shí)糾結(jié)不清
    當(dāng)對模塊進(jìn)行解耦時(shí),即便大體上的業(yè)務(wù)劃分已經(jīng)清晰,但因?yàn)闃I(yè)務(wù)間各種微妙的關(guān)系,細(xì)節(jié)上仍會遇到糾纏不清的情況。那么這個時(shí)候就會出現(xiàn)糾結(jié)于這個模塊到底該不該細(xì)分的問題。
    我們能做到的只是盡量讓他更加"面向?qū)ο?,同時(shí)避免隨意拼湊和單純?yōu)榱祟愋徒怦疃怦畹那闆r。

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

    image.png

    這一塊筆者就不再闡述,可以跳轉(zhuǎn)微信的文章進(jìn)行學(xué)習(xí)。

總結(jié)

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

全局架構(gòu)-新.png

模塊化的架構(gòu)不僅解決了模塊耦合問題,同時(shí)也調(diào)高了整個App的拓展性與維護(hù)性。這樣的重構(gòu),何樂而不為?


最后希望筆者分享的一點(diǎn)經(jīng)驗(yàn)?zāi)軐Υ蠹姨岣叽a有些幫助,如有錯誤的地方,歡迎指正探討。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容