微信 Android 模块化架构重构实践(下)

作者:carlguo

接上篇:《微信 Android 模块化架构重构实践(上)》

取舍和选择

对于架构重构,我们也曾放眼行业内已经发布过的各种方案,希望从中找到一些解决思路。

我们参考了很多业界开放和发表的架构设计。总的来说,目前Android端App整体架设计上,除了聚焦在“大前端”之外,基本上都在“插件化/应用沙盒”上面下功夫。可以参考如atlas、small、DroidPlugin、DynamicApk等等方案,不难发现让模块最终具备动态性是它们最核心的能力。

“大前端”是个热门的方向。我们结合自身情况,考虑到迁移已有代码不是一件容易事,让团队彻底切换编程工具短期不现实。这种“远水解不了近渴”的感觉,也只能暂且作罢。

我们把目光投向了“插件化/应用沙盒”。沙盒的好处是它具备很强的隔离性,有些方案可以彻底隔离代码和资源,边界限制性极强,效果很好。此外,多数方案也都具备动态更新能力、补丁能力,动态性基本成为标配。所以这时我们在考虑需不需要走上这样的路。

一切从自身情况出发。在微信的角度看,我们最关心的问题是如何能约束住代码边界,如何防止架构劣化,如何提高开发效率。这样的情况下,重新审视了具备动态性的插件化和沙盒方案。先从动态性上考虑,回看业内的使用经验,目前的动态性通常有两个主要的用途:1)作为热补丁使用;2)业务并行发布需求强烈,例如运营需求,作为快速发布的手段。对于第二点,就目前的微信团队来说是较少遇到的弱需求。相较之下,工具类和电商类应用常有符合的使用场景。而对于第一点,我们则期望在实现模块化上,目标相对纯粹一些,专心解决代码自身问题,之后再通过tinker在开发阶段之外实现热补丁上的动态性需要。

再来看隔离性。需要说明的是,这里的隔离型主要来自于沙盒架构效果。对于非独立插件化工程(需要编译依赖其他插件)除了动态性,它的作用和普通module在代码隔离上没有区别,不做讨论。

我们心里很期盼代码与代码之间那种干净清爽的分割效果,但事与愿违。

图20 - 模块依赖示意图

从上面这张模块依赖示意图看到,微信业务模块之间数据关系相当复杂,模块间相互访问数据、共享某些功能的行为如此普遍。而实际情况比示意图更麻烦。

面对微信业务数据相互间频繁的调用,沙盒隔离容易导致代码复用困难和相互调用麻烦,想在微信上实现,目前困难极大。重构难度不谈,单是隔离的收益就很有可能无法弥补开发效率上的损失。

从开发模式上看也是一样。微信Android团队目前每个迭代大概三四十人参与,内部沟通成本不算高到不可接受。通常开发同学可能要同时开发和修改几个模块并保证他们相互模块独立,同时又可能有频繁的模块间通信。这种时候,模块调用方不方便显得很重要。

此外还需要考虑的问题,从几个成熟方案中都能看到hook Android框架、修改aapt、替换或包装android gradle plugin、代理组件等等设计。这些方案的复杂度和兼容性代价,不能忽视。使用和维护它们需要仔细权衡。

所以思前想后我们仍选择了重走最开始的模块化之路。一切问题的根本还是在那个说的很多年的代码边界、解耦和内聚上。只要有了清晰独立的代码模块,才有将来其他变化的可能性。

代码之外,架构之内

模块负责人制度

有这样一句话,“不被监管的权利一定会发生腐败” 。如果放到软件开发的行当来说,就是“不被监管的代码也一定会发生劣化”。所以代码应该要接受“监管”。

为了能长期有效的保持代码质量,我们开始执行新的代码审查机制——模块负责人制度。

代码审查的好处毋庸置疑。在这之前,微信由于业务发展快速,同学们经常会变换需求的开发方向。面对着业务模块数量比人多的情况,开发同学经常一个人需要开发多个模块。也因此许多模块被无数人维护过,基础的支撑工程更是如此。这种类似游击战的方式,开发效率很高,支撑了微信快速的研发节奏,但也导致了“无主代码”特别多。大家缺少对代码的“归属感”,也降低了改进优化模块的欲望。 另外在这之前,代码审查是由leader对申请回流主干的Merge Request进行review,这导致效率较低且容易遗漏问题。合理的代码审查更应该是全员性质的。

模块负责人制度尝试改变这些现状。通过大家认领模块,对模块的代码和设计负责,对模块对外提供的接口服务负责,对其他人修改自己模块的行为进行监督。这些情况明显提高开发同学的代码所有感,改变大家修改优化和修改代码的动机。

推动模块负责人制度,渐进式的推动了大范围的代码审查,这样的方式很适合像微信这样没有从一开始执行全员性质Code Review的项目。目前模块负责人机制运转顺利,代码审查率和模块认领率都在提高。

重构与开发者心态的关系

在一个长期没有改进的框架下,开发者的习惯可能会逐步变成跟随式、保守式的开发。这大概可以被描述成“只要别人这样做,我也这样做,哪怕这么样的设计不好,但也不会错”。随着心态逐渐普遍,另一种情形出现:经常能听到有同学吐槽一些代码,却更少看到代码在被改进。这说明一些沉积的问题不是没有被大家发现,只是没有人愿意去修改。这种情况下代码和框架会随着时间变得越来越差,有些问题逐渐变成“陈年旧病”。 面对这个问题首先要说,这不是开发者合格与否的问题,实际上有想法的开发人员有很多,但想将每个想法转换成代码并让大家接受,并不是一件很容易的事。尤其在一个大框架下,尝试改变的代价很大。如果他的主要任务不在改进某些模块上,那么很多想法最后都无法变成现实。这也是为什么保守和跟随的习惯会逐渐变的普遍。

保守的气氛需要被打破。当开启一次重构之后,你会发现团队中会有很多积极的声音响应,他们会把积压的想法和意见抛出来。一次问题的解决,可能会为另一个问题的解决带来机会,其他开发同学的一些想法也许就能更容易落地。所以不定期的推动一些模块的重构,将一些对代码的不满释放出来,是一种不错的激活。

此外在重构之后,还要考虑引导开发的代码组织方式切换,多用模板、正确的代码实例等,让他们可以放心参考。

模块划分经验谈

维持代码边界

代码的边界就像一堵墙,架构的劣化都是从这堵墙的瓦解开始的。从以往的经验来看,编译上的隔离是最好的约束手段,单纯的约定或准则并不能永远的保持下去。 所以在任何情况下都尽可能不要放开编译上的约束。接着,将接口和实现分离,其他工程只依赖接口而不依赖实现,这样的边界效果更好。当然破坏无处不在,例如遇到某个紧急需求要某模块新增若干接口,就可能出现跳过接口直接依赖实现工程进行开发的情况。这时可以考虑通过代码的审查进行监督,也可以通过开发简单的编译脚本,检查是否有不当依赖产生。

划定模块边界的细节问题

当对代码进行解耦时,即便大体上的模块职责划分已经清晰,但因为模块间的各种业务关系,细节上仍会遇到纠缠不清的情况。事实上,因为需求及功能的不同,并没有哪一种模块划分的规则是完全适用于每个应用的。随着业务的发展和变化,模块边界出现不合适的情况完全符合预期。

那么如何让模块划分更让大家觉得合理,或者说当遇到一个两难选择时,按照什么样的方式大家会更好理解?我们建议的方法其实也很简单:试着对代码“讲一个符合逻辑的故事”,哪个故事讲得通,你就可以将之作为拆分的选择。因为代码解耦从来不是问题,纠结的只是解耦行为能不能让人理解。例如一些模块间通信用的数据结构究竟属于那个模块的问题就可以用这种方式仲裁。在纠结的时候,能自圆其说的方案往往就足够了。我们要尽力避免的,应该是随意拼凑和单纯为了类型解耦而解耦的情况。

模块的一般组织方式

设计一个模块,我们有一个一般性的组织方式,可以将模块分成三个工程:implementation工程、api工程、library工程。

implementation工程提供逻辑的实现。api工程提供对外的接口和数据结构。library工程,则提供该模块的一些工具类。

从另一个角度看,implementation工程实际上是和应用的状态、生命周期相关的,它的执行依赖于各种应用状态。而library工程则不关心这些状态。因此也可以看做library提供某种功能,implementation则是如何运用这种功能。例如,我们实现一个表情模块,library工程提供表情的资源、表情的渲染和播放能力,api工程提供了使用表情的服务接口,implementation工程则提供了api的实现,及何时开始加载表情资源、缓存管理、以及其他表情功能例如商店等等。

当然,这是一个指导性的建议,很多时候library工程和api工程之间没有明显边界也很正常。但强烈建议至少要有implementation工程和api工程。

分析依赖关系的工具

解耦代码时,快速分析代码的依赖关系能很好的提升工作效率。Android Studio提供了一个不错的工具。

图21 - Analyze dependency工具

文件、资源以及工程,都可以进行依赖分析。有了分析结果,接下来一步一步把代码分离就简单多了。

最后

重构整体架构不是一件容易事,通常也不太可能让整个团队停下来只做重构。所以一直以来微信的重构都是随着版本迭代进行“拆分”-> “灰度” -> “回流”的循环节奏。

“设计系统的组织,其产生的设计和架构等价于组织间的沟通结构”。对于微信几年间走过的路程,时至今日团队内的沟通形式还可以做到较多的直接沟通。这些情况决定了微信如今的技术选择。所以在方案选择上我们就更愿意寻求相对简单合适的方式解决问题——用纯粹的模块化保持后续架构的灵活性和健壮性,重新强调依赖、强调应用状态和生命周期、强化代码的边界。

除了代码上的设计,代码之外我们也做了些努力。我们认同代码审查的意义,也开始推行模块负责人的审查机制。此外我们还打算强化文档的使用,当然这个还在规划中。

最后希望我们分享的一点经验能对大家有些价值,欢迎留言交流。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏恰同学骚年

探索抽象工厂模式(Abstract Factory)——TerryLee

原文地址:http://terrylee.cnblogs.com/archive/2005/12/13/295965.html

713
来自专栏数据派THU

数据蒋堂 | 存储过程的利之弊

来源:数据蒋堂 作者:蒋步星 本文长度为2240字,建议阅读5分钟 本文通过剖析存储过程的优点,探查存储过程的潜在风险及应用场景。 存储过程是数据库领域中应用非...

1848
来自专栏BestSDK

Java 9正式发布:这次Jigsaw终于来了

在历经多次跳票之后,Java 9 终于在千呼万唤中正式发布。从这个版本开始,Java 将每半年发布一个版本。作为霸占编程语言排行榜鳌头多年的老牌语言,Java ...

3565
来自专栏程序你好

软件架构30条原则

原则 1: KISS (Keep it simple, stupid) “指设计时要坚持简约原则,避免不必要的复杂化。” 其思想是使用最简单的解决方案来完成这项...

802
来自专栏携程技术中心

携程2015 Open House获奖项目:响应式的蜕变

响应式的蜕变 Ctrip Tech 本文不再从最基本的语法开始行文,而在列举一些最基本的信息之后,开始探讨传统响应式设计的问题,与在实践当中思考出来的改进方法,...

1859
来自专栏阿杜的世界

【转】交易系统在分布式环境下的问题探讨

众所周知在互联网公司,如果你没有对你的系统进行分库分表,那你怎么好意思跟人打招呼?但是分库分表带来的难题也是众所周知的,除了多机查询(分批查询、合并结果等等)等...

473
来自专栏JackieZheng

开发人员看测试之TDD和BDD

前言:   已经数月没有来社区了,写博客贵在坚持,一旦松懈了,断掉了,就很难再拾起来。但是每每看到自己博客里的博文的浏览量每天都在增加,都在无形当中给了我继续写...

3626
来自专栏SAP最佳业务实践

SAP最佳业务实践:SD–售前活动(920)-1业务概览

用途 This scenario describes the pre-sales business processes usingthe functions...

3436
来自专栏一个会写诗的程序员的博客

测试思考拾叶集测试万能公式自动化测试自动化测试分层自动化测试框架工具平台数据准备服务Bug

须理清“SUT的功能”,“SUT的所有输入”,“每一个输入的取值范围”,“SUT的所有输出”,“根据功能推出每一个输出的预期值”。

744
来自专栏程序人生

思考,问题和方法

转眼已是七月。距我上次更新公众号,已经一月有余。离我加入 Arcblock,也有两月。如果把人看做一个运行的软件,那么这两个月我已经迭代好几轮,就像龙珠里在飞往...

1150

扫码关注云+社区