[关闭]
@pockry 2017-01-09T11:10:11.000000Z 字数 5162 阅读 1322

沪江学习安卓端重构实践

移动


对于大的架构重构,其实我们一直很谨慎的。我们的原则是将重构融合在每次迭代中,逐步优化代码的结构。这次针对整个应用的架构的调整的背景是公司移动开发部门的人数和项目越来越多,当初设计的移动端的架构让项目的依赖关系越来越复杂,维护成本也越来越高。刚好赶上公司产品的特别需求,我们决定梳理并优化一下整个项目解构。在实施过程中,我们依然坚持将整个重构的过程融合在每个迭代中,逐步完成一次大的架构升级。

目标

如图所示,这次重构围绕一个老生常谈的概念「解耦」展开,设定几个目标:

现状

在重构之前,我们的应用架构可以大致分为两层,应用层和 Library 层。一些通用的 Library 主要由专门的部门的同事维护,各业务线也会有一些自己维护的依赖库,也属于 Library 层。各业务线的主应用通过直接依赖的方式使用所需要 Library 提供的功能。

各个 Library 之间的依赖关系也是通过直接依赖的方式,由于没有一个明确的层级划分,随着 Library 数量的不断增加,他们之间的依赖关系变得越来越复杂,大致是这样一个状态。

这样的应用架构在一个相对小的团队中,可以很好的满足需求,将单独的功能模块和业务模块直接抽离成依赖库的方式去维护,可以降低模块之间的耦合性,又能保证不同应用能够使用统一的公共服务(Library)。但当开发团队发展到一定规模,由于模块数据的增加,模块之间的依赖关系错综复杂,各业务线的业务需求千差万别,这样简单的架构就会显得捉襟见肘了。下面的情况常常让人很头疼。

上面的这些问题,如果在规模相对小的团队中,也许表现的不是特别突出,但是当团队规模到达一定程度,存在多条主开发团队在开发不同的业务,同时又想共用许多公共模块的时候,就会经常困扰开发团队。我们可用通过一些规范或约定来规范大家的行为减少这些问题发生,也可以通过构建一些辅助工具或平台帮助我们将一些问题提前暴露出来而不至于影响到线上应用,但这终究是治标不治本。

重构方案

整个架构的核心思想是面向接口编程和依赖注入使各个模块之间实现解耦,然后通过横向角色划分与纵向层级划分的方式约定各个模块之间的关系,再通过接口分层的方式,明确具体模块在不同层级上需要实现的功能,最后 AOP 横向切入的方式,去实现测试、调试工具、插桩等行为。

面向接口与依赖注入

面向接口和依赖注入是我们这次重构的核心思想,通过接口的方式,约定各个模块对外暴露的功能,再用依赖注入的方式实现模块间的完全解耦。

面向接口编程

面向接口编程的概念可以在网上找到很多描述,上面详细的说明了面向接口编程的好处。这里我简单说一下我们想要使用面向接口编程的理由:

依赖注入

依赖注入在我们看来是在面向接口编程的基础上再往前走了一步,让模块之间彻底解耦。在单纯的面向接口编程中,如果你需要使用某个功能模块的功能,你本身还是需要依赖对应的模块,并且需要初始化对应的实现。但通过依赖注入的方式,使用方本身不用关注具体实现的初始化。而是由统一的注入模块将实现注入,调用方只需要和接口进行交互。这样做的好处是让模块间彻底解耦,也不会担心由于引入某个依赖库而导致引入一些本不想引入的库。

层级划分与角色划分

在我们的架构中,总体分为三个层次:底层、组件层和应用层。

除此之外还有一个很重要的 DI 控制模块,这一层主要负责将具体接口的实现注入,这一层本身可以脱离主项目存在,但介于现在 Android 还没有一个较为满意的依赖注入框架,暂时我们先放在应用层,后面计划封装成一个单独的框架,就可以实现类似 Sprint 用配置文件的方式来控制依赖注入,然后通过使用 AOP 的方式进行初始化,这样整个依赖注入控制模块就和主项目没有任何直接的关联了。

除了在每层的角色划分外,还有一个 Common Utils 模块,这是一个并列与所有层级存在的一个模块,可以被任何一个模块引用。

接口纵向分层和横向分类

纵向分层

在多线开发过程中,针对某个功能模块抽象出来的接口,要么不能满足所有产线的需求,要么会定义一些其他线不需要的接口。所以针对这样的过程我们考虑了接口纵向分层的概念,如果在一个相对小的合作团队中,可以不考虑这个问题。比如在网络请求这个功能模块的封装,我们将接口层分为 Common 接口层和业务接口层。

横向分类

关于接口层里面不同功能模块的接口以怎样的形式组合和依赖,有两种方式,一种是在同一接口层的所有接口都放在一个库里面,所有需要用到任何接口的部分,都依赖这个接口库;还有一种是将接口和具体实现放在一个项目一起维护,这个模块提供两个依赖库:接口库和实现库。这两种实现方案各有利弊。

第一种方案方便使用方的理解和使用,对于使用方只需要知道接口这个库,其他不需要知道,当有接口不满足的话,直接提需求就可以了。弊端是在任何一个模块的接口发生变化,都需要更新整个接口层,不过好在 Gradle 通过依赖合并的方式解决了这个问题,你只需要在你需要升级接口的地方升级,其他地方如果跟这次接口升级没有关系的话,依然可以使用老的版本。

第二种方案比较方便于模块维护人员的维护,当你需要升级接口时能够在一个项目里面同时把接口层和实现层同时升级掉。第二种方案的缺陷也是很明显的,这样在接口层之前形成的依赖关系,又会和之前模块之间形成的依赖关系一样复杂,对于使用方还是要去理解复杂的依赖关系。

结合这两种方案的优缺点,我们选择一个相对折中的方案,在同一层级的接口上进行横向分类,这样将整个接口层分成几个大的接口库去维护,这样能够有效降低一些模块变化对整个接口库版本的升级的影响,同时也可以减少一个接口库同时维护人员的数量,如果分配合理的话,甚至可以每个接口库和对应的实现层都是一个人维护,同样降低维护人员的成本。当然如果你们的接口本身就很少,就不用为这个问题烦恼了。

消息通讯

除了通过接口实现模块间的通讯方式,我们还设计了一套内部通讯协议,用于在应用内部消息通讯。对于一些易变的、灵活的、简单的通讯,可以直接通过发送消息的方式进行通讯。在任何一个想要接受到消息的地方,只要监听对应的消息就可以了,不管你是在主线程或者子线程。

AOP

我们是从解决Android 6.0 的权限处理问题引入 AOP 技术的,为了在不影响之前的代码的情况下,我们通过横向切入的方式解决了这个问题。

除了解决类似这种问题以外,我们使用 AOP 技术来实现只在 Debug 包才需要的功能,如一些测试的辅助工具、快速调试工具(调试板)等。这些功能只会出现在 Debug 包中,又不会影响到主项目的代码。

实施过程

我们将整个重构融合到每个迭代中,逐步实现一次架构的大调整,为了保证业务正常的进行,同时进行平稳的重构,我们把整个实施过程进行了细致的划分,这里大概总结下我们的实施过程。

从底层开始着手

抽离功能模块

抽离公共视图模块

在抽取公共试图的时候需要区分哪些是属于哪些是公共试图,哪些是具体业务定制的试图。将属于公共的部分逐步抽离到公共试图库。需要说明的是,公共试图模块不一定是一个单独的库,可以根据需要拆分成不同的库,只要保证整个都在试图模块这一范畴就可以了。

抽离公共业务模块

在抽离业务模块的时候,当业务模块提供了试图直接给外部使用,个人建议是将试图部分和具体的业务实现拆分到不同的 Model。试图属于试图模块,通过调用接口的方式实现内部逻辑。主项目或者其他模块需要使用该试图的时候,可以直接依赖(业务模块可以直接依赖任何试图组件)。

抽离产线业务模块

和抽离公共业务模块的步骤类似。

DI 控制框架封装

实现 DI 的配置化框架。

需要注意的问题

在重构过程中,会碰到平时写代码由于不注意导致的隐形的坑,比如我们碰到过一个在库里面存在类型强转导致的问题。这种问题不可避免,但是如果你在平时写代码时比较遵守代码规范,就会发现这种问题会相对较少。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注