16年8月底,公司新启动了一个D项目(代号),从敲下第一行代码到如今,刚好1个年头,我们已经在VCS上经历了多次调整。
第一阶段(2016.9 - 2017.2)
在这个阶段,开发同学是2个人,需求快速迭代,此时在一个仓库上开发,工作流简单,代码规范等约束成本非常低,这也是最舒适的一个阶段。
本地feature, 不推远程,rebase到dev,保持线性,需求变化很快,甚至经常在开发阶段要经常变动,此时线性的git tree的优势非常明显,快速定位到具体的提交,再执行后续的操作。
第二阶段(2017.2 - 2017.4)
在2月份时,团队成员增长到4个开发同学(后来在5月份变成5个),因为模块间没有一个明显的界线,所以在开发时可能会有意、无意的碰到其它模块的代码,merge、rebase解决冲突的次数明显上涨。
此外项目增长到一定规模,模块之间存在较严重的耦合,同时一些模块相对于稳定,一直存在于主工程并不合适。
更重要的是,任何一处改动都需要编译整个项目,每次编译的等待时间都足以让你怀疑人生。
模块化势在必行
Android官方提供一种模块化方案,即模块以Module被主工程依赖,第一阶段中,我们已经对一些模块或者Library进行抽离,但是还存在以下问题:
1、 稳定的Lib作为Module存在并不合适,每次主工程的编译都会带动这些Lib的编译,导致编译效率降低,急需将这些Lib上传到maven,从project依赖转换为坐标依赖
2、 一些模块存在于主工程,仅以package分离,我们需要将这些迁移到Module
改造后的架构:
还不够!
1、 即使在做了上面的工作后,依然无法解决模块的追溯问题,我们只能从整个仓库的git历史去追溯模块的commit,出现问题检索时,解决速度并不高效
2、 鉴于Gradle的生命周期:同步时,会扫描整个工程.gradle文件并执行,即使我只编译某个子模块!
虽然我们基于模块开发的效率已经大幅度提高,但是因为上述Gradle的特性,我们依然浪费了一些构建时间
我们决定一步到位,将子模块全部迁移到独立仓库中!
每个模块保证有独立的追溯历史,模块测试放到独立的仓库里进行。
这样迁移后,项目架构如下:
理想很丰满,现实很骨感
在迁移后,我们的工作流转变成:
开发feature时,会在对应子模块工程里开发,然后发布到maven,主工程依赖新的子模块坐标。
如上的工作流其实没有什么问题,但是:
1、 因为我们App体量并没有足够的大,业务虽然没有16年时变动那么大,但是还是常有跨多模块的需求,这种情况下,我们多是在主工程来测试的,如果过程有问题,就需要子模块重新发布,主工程重新同步、编译,而Andorid的同步速度大家是知道,如果开发额不够顺利,上面的步骤会多次重复...
2、 团队4个开发同学,但是算上主工程有8个业务模块,平均1个人对应2个模块,这种规模之下,把所以子模块都迁移到独立仓库后,维护成本真的很高,模块的完全解耦,让开发同学感到不适
3、 检索代码麻烦,主工程因为是坐标依赖,想搜索模块的代码,或者搜索某个方法的调用链都成为非常麻烦的事,而打开对应模块工程去搜索,耗时且麻烦
4、 很多情况我们需要一起切换分支或者一起checkout到某一个commit节点,之前在一个仓库时checkout即可,但现在多个仓库很头痛
以上导致一个比较严重的问题:代码没有很好的在可控范围内,Review代码,查找BUG等都变得困难。
第三阶段(2017.4 - 至今)
为了解决上面的痛点,我们找到2个东西: Repo,Git Submodule
Repo
Repo是Google管理Android源码的工具,是基于Git之上的构建工具,可以看作是Git的Wrapper,它提供一些命令集来操作其关联的模块。它引入一个Repo的角色来管理各个仓库,这意味着主工程和子模块是同等地位的。
使用Repo的话,模块关系如下:
这样的话,开发方式和之前没有任何区别,依然是子模块发布maven,主工程坐标依赖,来保证主工程和子模块的版本关系。
Repo只是帮我们打包了多仓库的 检出、切换等Git命令,并没有很好的解决上面提到的问题,所以Repo目前并不能解决我们的痛点,现阶段并不适合我们。
Git Submodule:
Git Submodule是Git提供的功能,它允许你将一个 Git 仓库作为另一个 Git 仓库的子目录,它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
在Submodule中,当子模块的仓库有新的commit时,主仓库就会产生一个该模块的SHA值改变,该SHA值纪录了子模块的对应commit,以此来连接宿主关系。
使用Git Submodule的话,模块关系如下:
这个关系图太熟悉了,不正是Android标准模块化的关系图吗?!所以我们决定在这个看似很完美的方案上摸索。
在经过Git Submodule的改造后,架构图如下:
在经历一段适应期后,会发现Git Submodule的设计还是非常智慧的,但不可否认,不经任何装饰的Git Submodule直接用到App的工作流中,还是挺坑的。
比如当你主工程检出分支到某个commit时,子模块并不跟随切换到对应模块,需要手动执行 submodule update命令来让子模块切换到对应commit,但是这些更新后的子模块会在游离的HEAD上,在该状态上的commit是没有意义的。
除此之外还有很多其它的坑,如果让团队成员每个人都搞懂这套繁琐的机制并不现实,同时多仓库的存在让Git flow也繁琐很多,所以为了解决这些问题,我提供了一个脚本命令工具:ggsm
ggsm
ggsm可以看做是Repo + Git Flow的集合,它提供一些命令,可以方便打包操作多仓库,目标是像操作一个Git仓库一样操作主工程以及其Submodules,而目前我们确实也做到了。
ggsm是Git Submodule的Wrapper,它并不会影响我们平时的开发习惯,你依旧在某个仓库里add ,commit - 当你要开发某个feature时,start命令帮你把所有仓库都切换到新的feature分支,当子模块都开发完成后,主工程里提交对应子模块的SHA(类似上传maven,主工程依赖新的坐标),然后使用merge finish推送代码到远程仓库,最后通过mr命令提交MR。
可以看到 我们把Repo和Git Flow集成在一起了,这样团队成员对Git Submodule几乎是无感知的,同时git flow也规范化了。
可以做的更多
事实上,ggsm承载了更多的功能:
1、 容错处理机制,检测模块commit的完整性
2、 Git hooks,钩子的更新、安装都放到了ggsm内,这一过程透明,目前我们做了commit msg检查、代码规范检查
3、CodeReview,目前我们把Merge Request也集成到ggsm里,这样开发同学完成某个feature后会自动通过GitLab API创建MR,发送邮件通知等
关于Repo和Git Submodule
如果你的项目各个模块非常独立,体量也比较大的话,Repo是非常好的管理工具,总之:
没有最好的方案,只有更适合的方案
软链接
我们使用了软链接,它将子模块的Module软链接带主工程内,从而可以使用Project依赖。
在开发跨模块的需求上Project依赖会让你的开发效率大幅度提高,对于单一模块的需求,我们打开对应的子模块直接在其内开发即可。
未来
我们所做的一切,都是为了在 高可维护性和开发舒适性 上找到一个更好的平衡点。
对待子模块,我们并不拘泥于一种形式,比如 Msg
和Pay
这2个子模块是比较稳定的,那么这2个模块仓库就完全不需要使用Git Submodule关联,使用坐标依赖。
模块足够独立或稳定的时候,Git Submodule是多余的,反而会拖慢集成主工程时的效率。
此外目前的阶段有足够的灵活性,未来某个模块足够稳定或独立时,rm -rf submodule
就可以解除宿主Git Submodule关系,转变为坐标依赖即可。
目前来看当前的VCS以及模块化方案已经算是一个很好的平衡点,足以应对当前阶段的D项目。
未来我们也会继续在VCS、工作流、自动化等方面做更深入的探索。