1、目标
本文主要记录组件化项目的实践过程及其中的思考。
具体实施一项技术项目之前我们会首先确定对应的目标,之后的行动计划都会朝着目标一步步靠拢。
- 大的层面上说就是做到各个模块能够在开发阶段独立运行,充分解耦;
- 小的层面上说使各个组件(技术、业务组件)更加容易复用;
简单的总结就是把一个大的Project工程,变成若干个小的Module工程,就是这样。
组件化前后变化看到这个图大家是不是想你要说的原来这么简单啊,先把刀都放下好好说话,实际上组件化看起来简单但是实践道路确是异常艰难,不信我们继续看!
2、基础库抽取
最初我们的Project整体只有一个Module,也就是说业务代码和技术代码都在一起,通过package的方式区分开来的。这种情况下做组件化难度是极大的,注意不夸张的说就是极大,因为大家在一起紧紧的抱成一团,相互依赖,然而要想直接拆出来困难重重:首先要把所有的基础库抽取出来到单独的基础Library工程,然后App这个Module依赖于这个基础Library工程,发现以下难题:
- 基础库可能不按规范不在特定的package下;
- 基础库可能和特定业务结合的相对紧密,无法直接移动;
- 移动基础库到单独的Library使用AS工具可能移动不完整;
- 一个类可能引用了若干个类,几层引用下来,工作量远超想象;
因此当我刚刚迈出第一步的时候我的内心就已经是这样的:
安慰自己万事开头难嘛,我做了以下事情:
- 先收集、移动各个不在特定package的基础类到特定package;
- 将基础库和特定业务隔离;
- 整理、精简基础类;
然后我就开始小心翼翼移动基础类到单独的基础Library,这个过程也十分虐心,因为利用AS的Move功能并不一定保险,强烈建议:
- 在单独的分支做组件抽离,做好版本控制,频繁备份,随时还原,以防某一个类找不到导致的Build失败致使需要从头开始(一定要注意);
- Move一次,及时Build一次,可以及时发现那个类没有Move完整;
这样你就以为解决了所有问题?Naive!!
- 部分基础库,例如网络请求,涉及地方、关联的类实在太多,多次的Move功能使用也没有完全将其移动完毕,Move完毕之后各种Build失败;
当时我的表情是这样的:
实在难以一次Move完毕,于是我换了一种思路:Move基础库的原因是为了让新建的别的业务Module使用,也就是Library中必须存在这些基础类,那我直接在Library中创建出来不就行了吗?
- 将难以抽离的基础类使用Rename功能重新命名,然后Copy了一份到Library中;
- 之后将模块移出来的时候必定找不到之前的基础类,我们将报错的地方改到现在的引用;
对于难以移出的基础类我们项目确实是这么做的,效率明显更高!
3、路由的设计
3.1 为什么需要路由
明确一个问题:各个业务模块之间不会是相互隔离而是必然存在一些交互的;
- 在Module A需要跳转到Module B某界面,而我们一般都是使用强引用的Class显式的调用;
- 在Module A需要调用Module B提供的方法,例如别的Module调用用户模块退出登录的方法;
这两种调用形式大家很容易明白,正常开发中大家也是毫不犹豫的调用。但是我们在组件化开发的时候却有很大的问题:
- 模块B的Activity Class在自己的Module B,那Module A必然引用不到,显式跳转行不通;
- 同理,直接去调用某个Module的方法也行不通;
由此:必然一种支持组件化的交互方式,这种交互方式需要支持UI跳转以及方法调用的能力,同时处理好多参数及不同参数类型。
3.2 方式之使用事件通知
备注:此处事件通知指代EventBus或者广播。
这种思路很好想到,在需要交互的地方发通知,然后接收方根据不同的通知类型做出不同的处理。相信各位老司机不需要代码也能直接明白。
优点:
- 简单方便,容易上手;
- 参数类型很好支持;
缺点:
- 随着交互的增多需要定义的事件实体类爆炸;
- 不方便找调用方与接收方;
备注:EventBus只有当业务模块在真实的线上运行阶段是在自己单独的进程才不可用,而这种场景对绝大数App完全够用。
推荐星级:二星级
3.3 方式之一个固定的方法
实现方案:在Activity或者需要暴露的普通类中声明一个统一的方法,这个方法自己去实现,对Activity来说是去实现UI跳转;对普通类来说则是去实现功能调用。但是这种方式需要解决两个问题:
- 跨Module类引用不到;
- 方法签名不固定;
- 对于跨Module类引用不到:首先需要确认的是跨Module的类肯定是引用不到的,那么我们就给这些类打上标记,间接的就能知道相应的类;标记的形式可以是一个Url,对于Url肯定是不区分Module的。例如:我给ActivityA打上一个标记"activitya",然后把这个url作为key,这个类Class作为value使用HashMap存储起来,那么我在别的Module就能直接通过相应的url来获取想要调用的类,然后调用这个特定的方法即可。
- 方法签名不固定的问题:这个很好理解,我要做不同的事情那需要的参数不管是个数还是类型肯定是不一样的,但是这样的话显然无法做到调用一个固定的方法。这时候我们仍然可以选择曲线救国:我们只传递一个参数进去,而这个参数则是一个HashMap,好处则是,可以传递任意个数、类型的参数。这样调用方法的时候你可以将随意多个数、类型的参数传递进去,然后在方法内从HashMap中取出真正需要的参数。
缺点:
- 侵入性太强,任何需要被调用的地方都需要按照统一的格式进行改造;
- 实现极其不友好,如果我需要和别的Module通信,那我需要详细的知道传递的参数个数及类型,但是这种实现方式无法明确的像平时方法调用那样被IDE给提示出来;
推荐星级:强烈不推荐,经得起时间检验的方案才是可行的好方案,而这种方式是经不起规模化推广考验的。
3.4 方式之真正的路由
以上两种方式虽然都可以解决问题,但是坦白讲,如果实际用到了项目里的话推进会是极为困难的一件事,因为体验实在是太差了!一个容易被推进、使用体验好的路由应该具备使用方便、上手成本低,改造成本小等基本素质,那么分摊下来应该具体体现在这几点上:
-
针对UI跳转:
- 改造成本低,不为跳转再重写方法;
- 所有参数类型均支持传递;
-
针对Module间交互:
- 可以清晰的知道方法签名,直接被IDE提示,和普通的方法调用没有区别,调用者一目了然;
来看下实际的解决方案:
- 对于Activity,我们也是给它打上一个标记,一个Activity对应一个Url,然后处理好参数的传递问题即可。
- 对于Module间调用,我们在Library工程中创建出每个Module需要向外提供能力的接口,然后每个Module自己去实现对应的实现类;并且也使用HashMap将这个接口与实现类进行保存,这样在别的Module就可以根据在Library中存在的接口获取到真正的实现类,而方法调用的时候就是简单的调用一个对象的方法,IDE提示很友好,而且不限制方法签名哦。
推荐星级:强烈推荐!!备注:具体的路由实现之后会有专门的文章。
4、业务组件的剥离
在路由的侵入达到一定程度之后就要做业务组件的剥离,需要注意几点:
4.1 先决条件
- Library库抽离或者准备完毕;
- 路由框架侵入要靠前;
这两项属于基础设施,不能边开展边做业务组件的剥离,不然一定会万分痛苦:各种报错,各种Build不过影响工作。
4.2 业务剥离的准则
首先需要明确对于不同的项目、要求以及不同的资源分配,业务剥离的程度也是不一样的。
- 最好按照产品功能进行划分,因为本身就是相互之间就有关联,而且代码也可能在同一个package下,方便一起Move;
- 剥离的颗粒度由粗到细,组件化初期可以先粗粒度的剥离,快速验证组件化方案以及踩坑,稳定之后再细粒度拆分;
4.3 共享数据的组件
业务组件实现单独运行是可以的,但是实际上很多情况下自己独立运行是跑不起来的,举个例子:大多数业务都会和用户体系挂钩,那么缺乏用户体系的业务组件寸步难行。
那么比较好的做法就是在技术组件剥离之后,优先把共享数据的组件(例如用户组件)先剥离出来,然后别的组件需要共享数据的时候就可以直接依赖于这个组件即可。
再写下去,本文篇幅就过长不利于吸收了,别的主题我们下篇文章接着聊!
欢迎关注
网友评论