基于组件化/模块化的重构探索实践

背景

具体啥公司啥产品就先隐了,反正不是BAT或者一线大厂

当前参与的项目历史也很久远,第一行代码据说是写于2014年的某一天,那时Android用的ide还是Eclipse、那时Android还没有很好的架构指导(mvp、mvvm)、那时Android最新的版本是5.0、那时Android的Material Design还没流行……

随着业务和产品发展,目前参与的项目apk有2~10个Android开发人员(注:开发人员数回浮动,不是因为离职,而是是因为当前项目团队在承接多个项目的并行开发)在进行迭代和维护。当前技术部移动团队有30+开发人员,有多个不同的项目在并行开发,但是却没有架构组(底层码农管不了组织的事,只能埋头敲代码),没有架构组的最直接的问题是没有一个组织来统一各个项目的技术选型和技术方案。

组件化/模块化

  • 组件:基于可重用的目的,对功能进行封装,一个功能就是一个组件,例如网络、IO、图片加载等等这些都是组件
  • 模块:基于业务独立的目的,对一系列有内聚性的业务进行整理,将其与其他业务进行切割、拆分,从主工程或原所在位置抽离为一个相互独立的部分

由于模块是独立解耦可重用的特性,在实施组件化/模块化的过程中,我们需要解决三个主要问题:
1. 模块通信——因为业务模块是相互隔离的,它们完全不知道也无法感知其他业务模块是否存在,所以需要一种尽最大可能的隔离、耦合度相对最低、代价相对最小的可行方案来实现通信
2. 模块独立运行——在后续迭代维护的过程中,各个业务线的人员能够职责更加清晰
3. 模块灵活组合运行——能够适应产品需求,灵活拆分组合打包上线

NOTE组件化/模块化这一节将会以XModulable为例进行解释它是如何进行组件化/模块化:阐述和理解一个程序问题,最直接的方式是写一个小的demo演示和show关键代码。本文可能有些地方讲的不够详细,强烈建议拉下XModulable运行看看。

XModulable架构图.png
XModulable工程结构.png

解决抛出的三个问题之前,先过下[XModulable]的工程结构图和架构图,上图中的module对应层级:

  • app壳层——依赖业务层,可灵活组合业务层模块
  • 业务层——im、live和main,面向common层实现业务层服务接口,向common注册和查询业务模块
  • common层——依赖基础组件层;承接业务层,暴露业务层服务接口,同时为业务层提供模块路由服务
  • basic层——basicRes和basicLib
    • basicRes——包含通用资源和各UI组件
    • basicLib——包含网路组件、图片加载组件、各种工具等功能组件

1. 模块通信

模块化的通信(UI跳转和数据传递),需要抓住几个基本点:隔离解耦代价小(易维护)、传递复杂数据FragmentViewFile……)。实现独立互不依赖模块的通信,很容易能够想到以下几种方式:

  • Android传统通信(比如aidl、广播、自定义url……)
    • 无法避免高度耦合、以及随着项目扩张导致难以维护的问题
    • 还有另外一关键个问题就是只能进行一些非常简单的数据传递,像FragmentViewFile……这些数据(或者叫对象也行),完全无法通信传递,但是这些数据在实际的app中恰恰是组成一个app的关键节点。比如说app的主站中有一个MainActivity,它是一个ViewPager+TabLayout的结构,其中的每一个页面都是来自于不同模块的Fragment,这个时候我们的通信就完全无法满足了。
  • 第三方通信(比如EventBusRxBus……)
    • 容易陷入茫茫的event通知和接收中,增加调试和维护的成本
    • 能够传递一些复杂的数据,通过event事件来携带其它数据对象,但是代码耦合性相应的会增加
  • 第三方路由库(比如ARouter、OkDeepLink、[DeepLinkDispatch](htt ps://github.com/airbnb/DeepLinkDispatch)……)基本都能够实现隔离解耦代价小(易维护)。至于数据传递的话默认只支持一些简单数据,但是我们可以结合面向接口编程,公共层暴露接口,业务层面向公共层的接口去实现对应的接口方法(UI跳转、数据读写……),最后当业务层使用的时候只需要通过路由到接口,就可以完成复杂数据的通信。以ARouter为例,可以在common层暴露业务模块的服务接口(IProviderARouter提供的服务接口,只要实现了该接口的自定义服务,ARouter都能进行路由操作),然后交由对应的业务模块去实现common层对应的服务接口,最后在业务模块中使用ARouter进行路由其他业务模块暴露的服务接口来实现。

从上面的分析来看,路由+面向接口编程是实现组件化/模块化的不二之选,但是这里又有一个问题——假设哪天抽风想要更换路由库或者可能某种特殊需求不同的业务模块使用了不容的路由库,那怎么办呢?没关系,我们这时候需要对路由库做一层封装,使业务模块内的路由都相互隔离,也就是一个业务模块内部的路由操作对其他业务模块来说是一个黑箱操作。我的封装思路是这样的:加一个XModule(可以把它想象成一个容器)的概念,在common层暴露服务接口的同时暴露XModule(它的具体实现也是有对应的业务模块决定的),每一业务模块都对应一个XModule,用于承载common层暴露的服务接口,业务模块之间的通信第一步必须先获取XModule,然后再通过这个容器去拿到服务。

综上所述,最终的组件化/模块化采用的是封装+路由+面向接口编程。以live业务模块为例,从源码的角度看下它们是实现这套思路的。在common层把live业务模块想要暴露给其他业务模块的服务LiveService进行了暴露,同时在common层暴露了一个LiveModule(live业务模块的服务容器,承载了LiveService),l,live业务模块面向common层对应的接口进行实现(LiveModuleImplLiveServiceImpl)。这样的话,上层业务就可以通过XModulable SDK获取到LiveModule,然后通过LiveModule承载的服务进行调用。

// common层live暴露的XModule(LiveModule)和服务接口(LiveService)

public abstract class LiveModule extends BaseModule {

    public abstract LiveService getLiveService();
}

public interface LiveService extends BaseService {
    Fragment createLiveEntranceFragment();

    void startLive();
}

// 业务模块层——live针对common层暴露的实现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. 模块独立运行

业务模块在Android Studio中其实就是一个module,从gradle的角度来说,module不是以application plugin方式运行,就是以library plugin方式运行,所以为了业务模块也能够独立运行,就需要控制gradle能够在application plugin和library plugin两种形式下切换,同时还要提供单独运行时的源码。

首先在项目的build.gradle中创建业务模块配置,isStandAlone表示业务模块是否独立运行:

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

    // 通过更改isStandalone的值实现业务模块是否独立运行,以及app壳工程对组件的灵活依赖
    modules = [
            main: [
                    isStandalone : false,
                    applicationId: "${applicationId}.main",
            ],
            im  : [
                    isStandalone : false,
                    applicationId: "${applicationId}.im",
            ],
            live: [
                    isStandalone : true,
                    applicationId: "${applicationId}.live"
            ],
    ]
}

然后设置对应业务模块的build.gradle:

def currentModule = rootProject.modules.live
// isStandalone的值决定了当前业务模块是否独立运行
if (currentModule.isStandalone) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
 省略...
    defaultConfig {
        if (currentModule.isStandalone) {
            // 当前组件独立运行,需要设置applicationId
            applicationId currentModule.applicationId
        }
        省略...

        def moduleName = project.getName()
        // 业务组件资源前缀,避免冲突
        resourcePrefix "${moduleName}_"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [
                        // ARouter处理器所需参数
                        moduleName   : moduleName,
                        // XModulable处理器所需参数
                        XModule: moduleName
                ]
            }
        }

    }
省略...
    sourceSets {
        main {
            // 单独运行所需要配置的源码文件
            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']
            }
        }
    }
}
省略...

最后,在业务模块中编写build.gradle中sourceSets声明单独运行所需要的额外源码文件,比如ApplicationSplashActivityManifest

完成上面的过程后,就可以选择对应的业务模块live运行

选择业务模块run

3. 模块灵活组合运行

模块的灵活组合,其实也非常简单,只需要更改业务模块配置在项目build.gradle的isStandalone值,然后在app壳的build.gradle中通过业务模块的isStandalone来决定是否依赖就行,关键代码如下:

dependencies {
省略...
    def modules = rootProject.modules
    def isMainStandalone = modules.main.isStandalone
    def isIMStandalone = modules.im.isStandalone
    def isLiveStandalone = modules.live.isStandalone
    // 判断业务组件是否独立运行,实现业务组件的灵活依赖
    if (isMainStandalone && isIMStandalone && isLiveStandalone) {
        api project(':common')
    } else {
        if (!isMainStandalone) {
            implementation project(':main')
        }
        if (!isIMStandalone) {
            implementation project(':im')
        }
        if (!isLiveStandalone) {
            implementation project(':live')
        }
    }
}

产品技术债

OK,现在已经把组件化/模块化所面临的问题消灭了,那就回过头来整理现有产品的技术债:

  1. 代码耦合、臃肿、混乱
  2. 模块层级不合理
    1. 业务模块相互依赖耦合
    2. 业务模块拆分粒度不够,某些模块像个大杂烩
    3. 业务模块无法单独编译运行,业务模块之间无法灵活组合成apk
  3. 基础组件无法快速提取,以供给其他工程使用

上述问题直接导致新来同事无法快速理清工程结构,以及无法快速进入开发。
若团队后续扩张的话,势必会按照业务功能模块划分独立的业务小组,那么会导致人员组织架构和工程组织架构上打架

对症下药

(一)控制代码质量

团队内部人员需要有代码质量意识,否则,容易出现有一波人在重构优化,而另一波人却在写bug、写烂代码,这样就完全失去了重构的意义。所以,在进入重构之前务必要明确传达控制代码质量

  1. 控制公共分支(master、develop和版本分支)权限,将公共分支的权限集中在少数人手里,可避免代码冲突、分支不可控
    • 非项目负责人只有develop权限——无法merge远端仓库的master、develop和版本分支
  2. 制定git flow和code review流程,提高团队协作效率
    1. 项目负责人从master(或者develop分支,视自身的项目管理而定)迁出版本分支
    2. 开发人员从版本分支迁出个人的开发分支
    3. 开发人员在个人的开发分支上进行开发工作
    4. 开发人员在个人分支上开发完成后需要push到远端,
    5. 开发人员在远端(我们用的是gitlab)创建merge request(Source branch:个人分支,Target branch:版本分支),同时指派给项目负责人并@相关开发人人员
    6. 执行code review
    7. review完成,由负责人进行远端的分支合并

(二) 合理的模块层级

首先来看下模块层级架构图:

组件化/模块化架构.png

在原有app的层级上,重新划分模块层级,这是很吃力的一件事情。因为一个项目经常是有多人并行的开发迭代的,当你已经切割或者规划出模块层级了,但是其它成员却在反其道而行之,必然会导致实施过程中进行代码合并时有茫茫多的冲突需要解决和返工,所以我们在这里还需要灌输模块层级思想和规划。

  1. 划分层级,从上到依次为:app壳层、业务层、common层、basic层,它们的职责如下
    • app壳层——直接依赖业务模块
    • 业务层——项目中各个独立的业务功能的聚合,由多个业务模块构成业务层
    • common层——承上启下:承接上层业务,提供业务模块路由服务;依赖底层basic,统一提供给上层使用
    • basic层——basicRes和basicLib
      • basicRes——包含通用资源和各UI组件
      • basicLib——包含网路组件、图片加载组件、各种工具等功能组件
  2. 业务模块提取通用代码、组件、公共资源进行下沉
    • 通用代码下沉到common,可能涉及到BaseAplication、BaseActivity、广播通知事件(也可能是EventBus相关事件,具体视自身而定)
    • ui组件和基础资源下沉到basicRes
    • 网路组件、图片加载组件、各种工具等功能组件下沉到basicLib
  3. 大杂烩模块拆分独立。以主业务模块为例,包含了推送、分享、更新、地图、用户中心、二手房、新房、租房……,如此臃肿的模块不可能一次性拆分完成,所以必须制定一个计划,有节奏的进行推进。我们的规划是按照业务关联性由低到高的原则依次拆分:
    1. 分享、更新下沉到basicLib
    2. 推送、地图下沉到basicLib
    3. 用户中心独立成业务模块
    4. 二手房、新房、租房独立成业务模块
  4. 业务模块独立运行;业务模块之间灵活组合成apk

(三) 基础组件内网maven依赖

基础组件拆分完成后,如果直接进行module依赖的话,会导致重复编译和无法灵活供给其它app使用的问题。所以我们需要将基础组件上传内网maven,然后通过maven进行依赖。

  1. basicRes和basicLib作为基础资源组件和基础功能组件上传到内网maven
  2. 对basicRes和basicLib根据组件细粒度拆分上传内网maven,方便其他工程能够根据实际需求灵活依赖

设定节奏和目标

制定重构节奏和目标,将规划合理分配到各个版本中去,在保证产品迭代的同时,能够稳步推进基于组件化/模块化的重构探索实践。

节奏 目标 执行范围
第一期 控制代码质量 1. 控制公共分支(master、develop和版本分支)权限;2. 制定git flow和code review流程
第二期 合理的模块层级(现有层级分割下沉) 1. 划分层级;2. 业务模块提取通用代码、组件、公共资源进行下沉
第三期 合理的模块层级(大杂烩模块拆分独立1) 分享、更新下沉到basicLib
第四期 合理的模块层级(大杂烩模块拆分独立2) 推送、地图下沉到basicLib
第五期 合理的模块层级(大杂烩模块拆分独立3) 用户中心独立成业务模块
第六期 合理的模块层级(大杂烩模块拆分独立4) 二手房、新房、租房独立成业务模块
第七期 合理的模块层级(业务模块独立运行和灵活组合) 业务模块独立运行,业务模块之间灵活组合成apk
第八期 基础组件内网maven依赖 1. basicRes和basicLib上传到内网maven;2. 对basicRes和basicLib根据组件细粒度拆分上传内网maven
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,391评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 献给所有买了书读不完而心怀愧疚的人 前不久整理家中书籍,把看过的书和没看过的书分类安置,没看过的书竟达百本,...
    圆夏木阅读 639评论 0 1
  • 这篇文章通过分析游戏对人的心理满足来寻找用户需求。因为分析游戏并引用游戏论述占据了本文最初版本的70%以上。一方面...
    热爱游戏的产品人阅读 1,597评论 0 3
  • 目录 上一章 忙活一天的餐馆,可算是迎来了休息的时间。老李随手抄起一旁的板凳,走到路灯下坐着,看看那远处的深邃...
    正晓孩阅读 321评论 1 7