基于組件化/模塊化的重構(gòu)探索實(shí)踐

背景

具體啥公司啥產(chǎn)品就先隱了,反正不是BAT或者一線大廠

當(dāng)前參與的項(xiàng)目歷史也很久遠(yuǎn),第一行代碼據(jù)說是寫于2014年的某一天,那時(shí)Android用的ide還是Eclipse、那時(shí)Android還沒有很好的架構(gòu)指導(dǎo)(mvp、mvvm)、那時(shí)Android最新的版本是5.0、那時(shí)Android的Material Design還沒流行……

隨著業(yè)務(wù)和產(chǎn)品發(fā)展,目前參與的項(xiàng)目apk有2~10個(gè)Android開發(fā)人員(注:開發(fā)人員數(shù)回浮動,不是因?yàn)殡x職,而是是因?yàn)楫?dāng)前項(xiàng)目團(tuán)隊(duì)在承接多個(gè)項(xiàng)目的并行開發(fā))在進(jìn)行迭代和維護(hù)。當(dāng)前技術(shù)部移動團(tuán)隊(duì)有30+開發(fā)人員,有多個(gè)不同的項(xiàng)目在并行開發(fā),但是卻沒有架構(gòu)組(底層碼農(nóng)管不了組織的事,只能埋頭敲代碼),沒有架構(gòu)組的最直接的問題是沒有一個(gè)組織來統(tǒng)一各個(gè)項(xiàng)目的技術(shù)選型和技術(shù)方案。

組件化/模塊化

  • 組件:基于可重用的目的,對功能進(jìn)行封裝,一個(gè)功能就是一個(gè)組件,例如網(wǎng)絡(luò)、IO、圖片加載等等這些都是組件
  • 模塊:基于業(yè)務(wù)獨(dú)立的目的,對一系列有內(nèi)聚性的業(yè)務(wù)進(jìn)行整理,將其與其他業(yè)務(wù)進(jìn)行切割、拆分,從主工程或原所在位置抽離為一個(gè)相互獨(dú)立的部分

由于模塊是獨(dú)立解耦、可重用的特性,在實(shí)施組件化/模塊化的過程中,我們需要解決三個(gè)主要問題:
1. 模塊通信——因?yàn)闃I(yè)務(wù)模塊是相互隔離的,它們完全不知道也無法感知其他業(yè)務(wù)模塊是否存在,所以需要一種盡最大可能的隔離、耦合度相對最低、代價(jià)相對最小的可行方案來實(shí)現(xiàn)通信
2. 模塊獨(dú)立運(yùn)行——在后續(xù)迭代維護(hù)的過程中,各個(gè)業(yè)務(wù)線的人員能夠職責(zé)更加清晰
3. 模塊靈活組合運(yùn)行——能夠適應(yīng)產(chǎn)品需求,靈活拆分組合打包上線

NOTE組件化/模塊化這一節(jié)將會以XModulable為例進(jìn)行解釋它是如何進(jìn)行組件化/模塊化:闡述和理解一個(gè)程序問題,最直接的方式是寫一個(gè)小的demo演示和show關(guān)鍵代碼。本文可能有些地方講的不夠詳細(xì),強(qiáng)烈建議拉下XModulable運(yùn)行看看。

XModulable架構(gòu)圖.png
XModulable工程結(jié)構(gòu).png

解決拋出的三個(gè)問題之前,先過下[XModulable]的工程結(jié)構(gòu)圖和架構(gòu)圖,上圖中的module對應(yīng)層級:

  • app殼層——依賴業(yè)務(wù)層,可靈活組合業(yè)務(wù)層模塊
  • 業(yè)務(wù)層——im、live和main,面向common層實(shí)現(xiàn)業(yè)務(wù)層服務(wù)接口,向common注冊和查詢業(yè)務(wù)模塊
  • common層——依賴基礎(chǔ)組件層;承接業(yè)務(wù)層,暴露業(yè)務(wù)層服務(wù)接口,同時(shí)為業(yè)務(wù)層提供模塊路由服務(wù)
  • basic層——basicRes和basicLib
    • basicRes——包含通用資源和各UI組件
    • basicLib——包含網(wǎng)路組件、圖片加載組件、各種工具等功能組件

1. 模塊通信

模塊化的通信(UI跳轉(zhuǎn)和數(shù)據(jù)傳遞),需要抓住幾個(gè)基本點(diǎn):隔離、解耦、代價(jià)小(易維護(hù))、傳遞復(fù)雜數(shù)據(jù)Fragment、ViewFile……)。實(shí)現(xiàn)獨(dú)立互不依賴模塊的通信,很容易能夠想到以下幾種方式:

  • Android傳統(tǒng)通信(比如aidl、廣播、自定義url……)
    • 無法避免高度耦合、以及隨著項(xiàng)目擴(kuò)張導(dǎo)致難以維護(hù)的問題
    • 還有另外一關(guān)鍵個(gè)問題就是只能進(jìn)行一些非常簡單的數(shù)據(jù)傳遞,像Fragment、ViewFile……這些數(shù)據(jù)(或者叫對象也行),完全無法通信傳遞,但是這些數(shù)據(jù)在實(shí)際的app中恰恰是組成一個(gè)app的關(guān)鍵節(jié)點(diǎn)。比如說app的主站中有一個(gè)MainActivity,它是一個(gè)ViewPager+TabLayout的結(jié)構(gòu),其中的每一個(gè)頁面都是來自于不同模塊的Fragment,這個(gè)時(shí)候我們的通信就完全無法滿足了。
  • 第三方通信(比如EventBusRxBus……)
    • 容易陷入茫茫的event通知和接收中,增加調(diào)試和維護(hù)的成本
    • 能夠傳遞一些復(fù)雜的數(shù)據(jù),通過event事件來攜帶其它數(shù)據(jù)對象,但是代碼耦合性相應(yīng)的會增加
  • 第三方路由庫(比如ARouter、OkDeepLink、[DeepLinkDispatch](htt ps://github.com/airbnb/DeepLinkDispatch)……)基本都能夠?qū)崿F(xiàn)隔離解耦代價(jià)小(易維護(hù))。至于數(shù)據(jù)傳遞的話默認(rèn)只支持一些簡單數(shù)據(jù),但是我們可以結(jié)合面向接口編程,公共層暴露接口,業(yè)務(wù)層面向公共層的接口去實(shí)現(xiàn)對應(yīng)的接口方法(UI跳轉(zhuǎn)、數(shù)據(jù)讀寫……),最后當(dāng)業(yè)務(wù)層使用的時(shí)候只需要通過路由到接口,就可以完成復(fù)雜數(shù)據(jù)的通信。以ARouter為例,可以在common層暴露業(yè)務(wù)模塊的服務(wù)接口(IProvider,ARouter提供的服務(wù)接口,只要實(shí)現(xiàn)了該接口的自定義服務(wù),ARouter都能進(jìn)行路由操作),然后交由對應(yīng)的業(yè)務(wù)模塊去實(shí)現(xiàn)common層對應(yīng)的服務(wù)接口,最后在業(yè)務(wù)模塊中使用ARouter進(jìn)行路由其他業(yè)務(wù)模塊暴露的服務(wù)接口來實(shí)現(xiàn)。

從上面的分析來看,路由+面向接口編程是實(shí)現(xiàn)組件化/模塊化的不二之選,但是這里又有一個(gè)問題——假設(shè)哪天抽風(fēng)想要更換路由庫或者可能某種特殊需求不同的業(yè)務(wù)模塊使用了不容的路由庫,那怎么辦呢?沒關(guān)系,我們這時(shí)候需要對路由庫做一層封裝,使業(yè)務(wù)模塊內(nèi)的路由都相互隔離,也就是一個(gè)業(yè)務(wù)模塊內(nèi)部的路由操作對其他業(yè)務(wù)模塊來說是一個(gè)黑箱操作。我的封裝思路是這樣的:加一個(gè)XModule(可以把它想象成一個(gè)容器)的概念,在common層暴露服務(wù)接口的同時(shí)暴露XModule(它的具體實(shí)現(xiàn)也是有對應(yīng)的業(yè)務(wù)模塊決定的),每一業(yè)務(wù)模塊都對應(yīng)一個(gè)XModule,用于承載common層暴露的服務(wù)接口,業(yè)務(wù)模塊之間的通信第一步必須先獲取XModule,然后再通過這個(gè)容器去拿到服務(wù)。

綜上所述,最終的組件化/模塊化采用的是封裝+路由+面向接口編程。以live業(yè)務(wù)模塊為例,從源碼的角度看下它們是實(shí)現(xiàn)這套思路的。在common層把live業(yè)務(wù)模塊想要暴露給其他業(yè)務(wù)模塊的服務(wù)LiveService進(jìn)行了暴露,同時(shí)在common層暴露了一個(gè)LiveModule(live業(yè)務(wù)模塊的服務(wù)容器,承載了LiveService),l,live業(yè)務(wù)模塊面向common層對應(yīng)的接口進(jìn)行實(shí)現(xiàn)(LiveModuleImplLiveServiceImpl)。這樣的話,上層業(yè)務(wù)就可以通過XModulable SDK獲取到LiveModule,然后通過LiveModule承載的服務(wù)進(jìn)行調(diào)用。

// common層live暴露的XModule(LiveModule)和服務(wù)接口(LiveService)

public abstract class LiveModule extends BaseModule {

    public abstract LiveService getLiveService();
}

public interface LiveService extends BaseService {
    Fragment createLiveEntranceFragment();

    void startLive();
}

// 業(yè)務(wù)模塊層——live針對common層暴露的實(shí)現(xiàn)LiveModuleImpl和LiveServiceImpl

@XModule(name = ModuleName.LIVE)
public class LiveModuleImpl extends LiveModule {
    @Autowired
    LiveService liveService;

    @Override
    public LiveService getLiveService() {
        return liveService;
    }
}

@Route(path = PathConstants.PATH_SERVICE_LIVE)
public class LiveServiceImpl implements LiveService {
    @Override
    public void init(Context context) {

    }

    @Override
    public Fragment createLiveEntranceFragment() {
        return new LiveEntranceFragment();
    }

    @Override
    public void startLive() {
        ARouter.getInstance().build(PathConstants.PATH_VIEW_LIVE).navigation();
    }
}

2. 模塊獨(dú)立運(yùn)行

業(yè)務(wù)模塊在Android Studio中其實(shí)就是一個(gè)module,從gradle的角度來說,module不是以application plugin方式運(yùn)行,就是以library plugin方式運(yùn)行,所以為了業(yè)務(wù)模塊也能夠獨(dú)立運(yùn)行,就需要控制gradle能夠在application plugin和library plugin兩種形式下切換,同時(shí)還要提供單獨(dú)運(yùn)行時(shí)的源碼。

首先在項(xiàng)目的build.gradle中創(chuàng)建業(yè)務(wù)模塊配置,isStandAlone表示業(yè)務(wù)模塊是否獨(dú)立運(yùn)行:

ext {
    applicationId = "com.xpleemoon.sample.modulable"

    // 通過更改isStandalone的值實(shí)現(xiàn)業(yè)務(wù)模塊是否獨(dú)立運(yùn)行,以及app殼工程對組件的靈活依賴
    modules = [
            main: [
                    isStandalone : false,
                    applicationId: "${applicationId}.main",
            ],
            im  : [
                    isStandalone : false,
                    applicationId: "${applicationId}.im",
            ],
            live: [
                    isStandalone : true,
                    applicationId: "${applicationId}.live"
            ],
    ]
}

然后設(shè)置對應(yīng)業(yè)務(wù)模塊的build.gradle:

def currentModule = rootProject.modules.live
// isStandalone的值決定了當(dāng)前業(yè)務(wù)模塊是否獨(dú)立運(yùn)行
if (currentModule.isStandalone) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
 省略...
    defaultConfig {
        if (currentModule.isStandalone) {
            // 當(dāng)前組件獨(dú)立運(yùn)行,需要設(shè)置applicationId
            applicationId currentModule.applicationId
        }
        省略...

        def moduleName = project.getName()
        // 業(yè)務(wù)組件資源前綴,避免沖突
        resourcePrefix "${moduleName}_"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [
                        // ARouter處理器所需參數(shù)
                        moduleName   : moduleName,
                        // XModulable處理器所需參數(shù)
                        XModule: moduleName
                ]
            }
        }

    }
省略...
    sourceSets {
        main {
            // 單獨(dú)運(yùn)行所需要配置的源碼文件
            if (currentModule.isStandalone) {
                manifest.srcFile 'src/standalone/AndroidManifest.xml'
                java.srcDirs = ['src/main/java/', 'src/standalone/java/']
                res.srcDirs = ['src/main/res', 'src/standalone/res']
            }
        }
    }
}
省略...

最后,在業(yè)務(wù)模塊中編寫build.gradle中sourceSets聲明單獨(dú)運(yùn)行所需要的額外源碼文件,比如ApplicationSplashActivityManifest

完成上面的過程后,就可以選擇對應(yīng)的業(yè)務(wù)模塊live運(yùn)行

選擇業(yè)務(wù)模塊run

3. 模塊靈活組合運(yùn)行

模塊的靈活組合,其實(shí)也非常簡單,只需要更改業(yè)務(wù)模塊配置在項(xiàng)目build.gradle的isStandalone值,然后在app殼的build.gradle中通過業(yè)務(wù)模塊的isStandalone來決定是否依賴就行,關(guān)鍵代碼如下:

dependencies {
省略...
    def modules = rootProject.modules
    def isMainStandalone = modules.main.isStandalone
    def isIMStandalone = modules.im.isStandalone
    def isLiveStandalone = modules.live.isStandalone
    // 判斷業(yè)務(wù)組件是否獨(dú)立運(yùn)行,實(shí)現(xiàn)業(yè)務(wù)組件的靈活依賴
    if (isMainStandalone && isIMStandalone && isLiveStandalone) {
        api project(':common')
    } else {
        if (!isMainStandalone) {
            implementation project(':main')
        }
        if (!isIMStandalone) {
            implementation project(':im')
        }
        if (!isLiveStandalone) {
            implementation project(':live')
        }
    }
}

產(chǎn)品技術(shù)債

OK,現(xiàn)在已經(jīng)把組件化/模塊化所面臨的問題消滅了,那就回過頭來整理現(xiàn)有產(chǎn)品的技術(shù)債:

  1. 代碼耦合、臃腫、混亂
  2. 模塊層級不合理
    1. 業(yè)務(wù)模塊相互依賴耦合
    2. 業(yè)務(wù)模塊拆分粒度不夠,某些模塊像個(gè)大雜燴
    3. 業(yè)務(wù)模塊無法單獨(dú)編譯運(yùn)行,業(yè)務(wù)模塊之間無法靈活組合成apk
  3. 基礎(chǔ)組件無法快速提取,以供給其他工程使用

上述問題直接導(dǎo)致新來同事無法快速理清工程結(jié)構(gòu),以及無法快速進(jìn)入開發(fā)。
若團(tuán)隊(duì)后續(xù)擴(kuò)張的話,勢必會按照業(yè)務(wù)功能模塊劃分獨(dú)立的業(yè)務(wù)小組,那么會導(dǎo)致人員組織架構(gòu)和工程組織架構(gòu)上打架

對癥下藥

(一)控制代碼質(zhì)量

團(tuán)隊(duì)內(nèi)部人員需要有代碼質(zhì)量意識,否則,容易出現(xiàn)有一波人在重構(gòu)優(yōu)化,而另一波人卻在寫bug、寫爛代碼,這樣就完全失去了重構(gòu)的意義。所以,在進(jìn)入重構(gòu)之前務(wù)必要明確傳達(dá)控制代碼質(zhì)量

  1. 控制公共分支(master、develop和版本分支)權(quán)限,將公共分支的權(quán)限集中在少數(shù)人手里,可避免代碼沖突、分支不可控
    • 非項(xiàng)目負(fù)責(zé)人只有develop權(quán)限——無法merge遠(yuǎn)端倉庫的master、develop和版本分支
  2. 制定git flow和code review流程,提高團(tuán)隊(duì)協(xié)作效率
    1. 項(xiàng)目負(fù)責(zé)人從master(或者develop分支,視自身的項(xiàng)目管理而定)遷出版本分支
    2. 開發(fā)人員從版本分支遷出個(gè)人的開發(fā)分支
    3. 開發(fā)人員在個(gè)人的開發(fā)分支上進(jìn)行開發(fā)工作
    4. 開發(fā)人員在個(gè)人分支上開發(fā)完成后需要push到遠(yuǎn)端,
    5. 開發(fā)人員在遠(yuǎn)端(我們用的是gitlab)創(chuàng)建merge request(Source branch:個(gè)人分支,Target branch:版本分支),同時(shí)指派給項(xiàng)目負(fù)責(zé)人并@相關(guān)開發(fā)人人員
    6. 執(zhí)行code review
    7. review完成,由負(fù)責(zé)人進(jìn)行遠(yuǎn)端的分支合并

(二) 合理的模塊層級

首先來看下模塊層級架構(gòu)圖:

組件化/模塊化架構(gòu).png

在原有app的層級上,重新劃分模塊層級,這是很吃力的一件事情。因?yàn)橐粋€(gè)項(xiàng)目經(jīng)常是有多人并行的開發(fā)迭代的,當(dāng)你已經(jīng)切割或者規(guī)劃出模塊層級了,但是其它成員卻在反其道而行之,必然會導(dǎo)致實(shí)施過程中進(jìn)行代碼合并時(shí)有茫茫多的沖突需要解決和返工,所以我們在這里還需要灌輸模塊層級思想和規(guī)劃。

  1. 劃分層級,從上到依次為:app殼層、業(yè)務(wù)層、common層、basic層,它們的職責(zé)如下
    • app殼層——直接依賴業(yè)務(wù)模塊
    • 業(yè)務(wù)層——項(xiàng)目中各個(gè)獨(dú)立的業(yè)務(wù)功能的聚合,由多個(gè)業(yè)務(wù)模塊構(gòu)成業(yè)務(wù)層
    • common層——承上啟下:承接上層業(yè)務(wù),提供業(yè)務(wù)模塊路由服務(wù);依賴底層basic,統(tǒng)一提供給上層使用
    • basic層——basicRes和basicLib
      • basicRes——包含通用資源和各UI組件
      • basicLib——包含網(wǎng)路組件、圖片加載組件、各種工具等功能組件
  2. 業(yè)務(wù)模塊提取通用代碼、組件、公共資源進(jìn)行下沉
    • 通用代碼下沉到common,可能涉及到BaseAplication、BaseActivity、廣播通知事件(也可能是EventBus相關(guān)事件,具體視自身而定)
    • ui組件和基礎(chǔ)資源下沉到basicRes
    • 網(wǎng)路組件、圖片加載組件、各種工具等功能組件下沉到basicLib
  3. 大雜燴模塊拆分獨(dú)立。以主業(yè)務(wù)模塊為例,包含了推送、分享、更新、地圖、用戶中心、二手房、新房、租房……,如此臃腫的模塊不可能一次性拆分完成,所以必須制定一個(gè)計(jì)劃,有節(jié)奏的進(jìn)行推進(jìn)。我們的規(guī)劃是按照業(yè)務(wù)關(guān)聯(lián)性由低到高的原則依次拆分:
    1. 分享、更新下沉到basicLib
    2. 推送、地圖下沉到basicLib
    3. 用戶中心獨(dú)立成業(yè)務(wù)模塊
    4. 二手房、新房、租房獨(dú)立成業(yè)務(wù)模塊
  4. 業(yè)務(wù)模塊獨(dú)立運(yùn)行;業(yè)務(wù)模塊之間靈活組合成apk

(三) 基礎(chǔ)組件內(nèi)網(wǎng)maven依賴

基礎(chǔ)組件拆分完成后,如果直接進(jìn)行module依賴的話,會導(dǎo)致重復(fù)編譯和無法靈活供給其它app使用的問題。所以我們需要將基礎(chǔ)組件上傳內(nèi)網(wǎng)maven,然后通過maven進(jìn)行依賴。

  1. basicRes和basicLib作為基礎(chǔ)資源組件和基礎(chǔ)功能組件上傳到內(nèi)網(wǎng)maven
  2. 對basicRes和basicLib根據(jù)組件細(xì)粒度拆分上傳內(nèi)網(wǎng)maven,方便其他工程能夠根據(jù)實(shí)際需求靈活依賴

設(shè)定節(jié)奏和目標(biāo)

制定重構(gòu)節(jié)奏和目標(biāo),將規(guī)劃合理分配到各個(gè)版本中去,在保證產(chǎn)品迭代的同時(shí),能夠穩(wěn)步推進(jìn)基于組件化/模塊化的重構(gòu)探索實(shí)踐。

節(jié)奏 目標(biāo) 執(zhí)行范圍
第一期 控制代碼質(zhì)量 1. 控制公共分支(master、develop和版本分支)權(quán)限;2. 制定git flow和code review流程
第二期 合理的模塊層級(現(xiàn)有層級分割下沉) 1. 劃分層級;2. 業(yè)務(wù)模塊提取通用代碼、組件、公共資源進(jìn)行下沉
第三期 合理的模塊層級(大雜燴模塊拆分獨(dú)立1) 分享、更新下沉到basicLib
第四期 合理的模塊層級(大雜燴模塊拆分獨(dú)立2) 推送、地圖下沉到basicLib
第五期 合理的模塊層級(大雜燴模塊拆分獨(dú)立3) 用戶中心獨(dú)立成業(yè)務(wù)模塊
第六期 合理的模塊層級(大雜燴模塊拆分獨(dú)立4) 二手房、新房、租房獨(dú)立成業(yè)務(wù)模塊
第七期 合理的模塊層級(業(yè)務(wù)模塊獨(dú)立運(yùn)行和靈活組合) 業(yè)務(wù)模塊獨(dú)立運(yùn)行,業(yè)務(wù)模塊之間靈活組合成apk
第八期 基礎(chǔ)組件內(nèi)網(wǎng)maven依賴 1. basicRes和basicLib上傳到內(nèi)網(wǎng)maven;2. 對basicRes和basicLib根據(jù)組件細(xì)粒度拆分上傳內(nèi)網(wǎng)maven
最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,922評論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,536評論 19 139
  • 獻(xiàn)給所有買了書讀不完而心懷愧疚的人 前不久整理家中書籍,把看過的書和沒看過的書分類安置,沒看過的書竟達(dá)百本,...
    圓夏木閱讀 770評論 0 1
  • 這篇文章通過分析游戲?qū)θ说男睦頋M足來尋找用戶需求。因?yàn)榉治鲇螒虿⒁糜螒蛘撌稣紦?jù)了本文最初版本的70%以上。一方面...
    熱愛游戲的產(chǎn)品人閱讀 1,813評論 0 3
  • 目錄 上一章 忙活一天的餐館,可算是迎來了休息的時(shí)間。老李隨手抄起一旁的板凳,走到路燈下坐著,看看那遠(yuǎn)處的深邃...
    正曉孩閱讀 394評論 1 7

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