一、为什么要组件化
随着项目需求的迭代,工程的规模越来越大,各种业务错综复杂的交织在一起,代码没有约束,边界越来越模糊,牵一发而动全身,开发和测试效率越来越低。这种情况下我们就要考虑采用组件化的架构思想对项目进行重构,拆分出基础组件和业务组件,解除业务之间的耦合。
- 单一职责:开发人员只用关心自己负责的业务
- 代码解耦:组件之前不直接依赖,编译器隔离
- 复用性强:基础组件解耦,便于其它项目复用
- 编译集成:业务组件可以独立调试,提升编译速度
- 平台化:大型团队通常会根据业务进行拆分和平台化,组件化利于划清各自的代码边界,提高各团队配合的效率
二、组件化架构设计

架构分层
- 应用层:app壳工程,Application、SplashActivity等
- 业务组件:根据业务模块进行划分,比如首页、发现等
- 通用业务:通用基础业务,比如BaseActivity、埋点、网络库封装等
- 基础组件:与业务不相关的基础库,比如日志库、加解密等
- 依赖库:依赖的三方库
组件拆分
Android Studio可以将工程拆分成多个module,每个组件对应一个module,组件代码稳定之后,可以上传maven库,使用gradle方式依赖。
组件命名规范
层级 | 命名 |
---|---|
应用层 | app |
业务组件 | bussniss_home、bussniss_find |
通用业务 | common、service |
基础组件 | lib_log、lib_ui、lib_utils |
依赖关系
- 遵循依赖倒置原则,上层依赖下层,业务层之间不互相依赖
- common依赖常用基础组件和三方库,建议采用api方式依赖,这样业务组件的build.gradle直接依赖common即可
- 业务组件对基础组件和依赖库的依赖采用implementation方式
- app对业务组件的依赖采用runtimeOnly方式
三、组件通信
业务组件之间完全隔离,不允许直接依赖,但有些场景下组件间需要通信,比如页面跳转和方法调用。经过综合对比,我们最终选择了ARouter作为通信的基础方案。整体是接口+实现的方案,各业务组件把需要对外暴露的接口抽象出来,抽象接口类放到统一的service服务组件,实现类放到各业务内部。
页面跳转
ARouter.getInstance().build(RouterPathConstant.PATH_FIND).navigation();
方法调用
以用户组件为例,说明下组件之间方法调用的实现方式。
service组件的IUserService,定义用户组件对外暴露的接口。
public interface IUserService extends IProvider {
public String getUserId();
public String getNickname();
public String getAvatar();
}
bussiness_user组件接口实现类UserServiceImpl。
@Route(path = ServicePathConstant.SERVICE_USER)
public class UserServiceImpl implements IUserService {
@Override
public void init(Context context) {
}
public String getUserId() {
return UserCenter.getInstance().getUserId();
}
public String getNickname() {
return UserCenter.getInstance().getNickname();
}
public String getAvatar() {
return UserCenter.getInstance().getAvatar();
}
}
现在接口定义和实现OK了,怎样将接口类暴露出去,让调用方拿到IUserService对象呢?还是借助ARouter实现服务工厂类ServiceFactory,对抽象接口进行统一管理,ServiceFactory位于service组件中,所有业务组件可以直接调用。
public class ServiceFactory {
public static IUserService getUserService() {
return (IUserService) getService(ServicePathConstant.SERVICE_USER);
}
private static Object getService(String path) {
return ARouter.getInstance().build(path).navigation();
}
}
这里借助ARouter的注解运行时获取IUserService的实现类对象,如果不使用ARouter,也可以在ServiceFactory中定义set方式,在Application初始化的时候实例化UserServiceImpl并赋值到ServiceFactory中。
至此,我们就可以在任意业务组件内部调用用户组件的方法了。
String userId = ServiceFactory.getUserService().getUserId();
调用app方法
上面我们说到依赖倒置原则,业务组件不能依赖app层。在组件化逐步重构过程中,可能还有部分业务代码是在app module中,如果业务组件需要调用这部分代码,可以定义IUserDelegator接口,在app初始化的时候将实现类设置到IUserService。
(IXXService是业务组件对外暴露的接口,IXXDelegator是业务组件需要调用方实现的接口,两者不要弄混了~)
service组件增加IUserDelegator接口类。
public interface IUserDelegator {
void go2Main();
void go2Spalsh();
}
service组件IUserService增加setDelegator和getDelegator方法。
public interface IUserService extends IProvider {
void setDelegator(IUserDelegator delegator);
IUserDelegator getDelegator();
}
app module实现IUserDelegator并调用IUserService的setDelegator。
public class WalletApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ServiceFactory.getUserService().setDelegator(UserDelegatorImpl());
}
至此,business_user组件中就可以调用app的方法了。
IUserDelegator delegator = ServiceFactory.getUserService().getDelegator();
delegator.go2Main();
四、资源文件管理
资源文件当然也要拆分到各个业务module中,为了避免冲突,强烈建议资源文件加上统一的前缀,比如R.drawable.user_icon_back, R.layout.user_activity_edit, R.string.user_dialog_title。module中资源文件名重复并不影响编译,但只会有1个打包进去,为啥我的布局文件改了就是不生效,相信有不少同学遇到这类问题。
build.gradle中可以增加资源文件前缀检查,如果命名不规范,Android Studio会提示警告。
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
resourcePrefix "user_"
}
五、组件集成与调试
某些项目会有打包多个app的需求,比如马甲包、多国家版本。这就需要有多个project和多个app module,app module中选择依赖需要的业务组件,打包生成不同的apk。
前面有提到app对业务组件的依赖使用runtimeOnly的方法,这样做的好处是能够解除app和业务组件之间的耦合。比如app中不应该直接访问UserFragment,应该在IUserService中提供获取方法,Fragment的实例化过程放到bussiness组件中,app不用关心拿到的是个什么样的Fragment。
public interface IUserService {
public Fragment newUserFragment();
}
组件独立编译调试
目前我们工程编译2-3分钟,还算能忍,所以暂时没做组件独立编译。如果团队规模大,工程编译慢,也是可以去做的。
六、思考
-
耦合和冗余有时候会冲突
比如多个业务都需要调用某个api,那么api的response model是否需要提取到common组件中呢?分拆到各个业务组件中是没有耦合了,但看着一堆冗余代码又总感觉怪怪的。解耦和去冗余,就按我们怎么选择了。
-
常量如何管理
看过有些工程代码,喜欢定义一个全局的Constants常量类,各个模块都在引用这个类,甚至有的把常量类下沉到common模块中。这并不是个好的做法,常量应该分散到各个业务组件中去,不同模块之间尽量不要共享常量。
-
高内聚低耦合
代码重构一定要时刻牢记这6个字,组件化不是为了拆分module,而是要写出高内聚低耦合的代码。
-
代码洁癖
最后,希望大家都保持代码洁癖,做一个有洁癖的程序猿。