原文:http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
在过去的几个月,在我和同事@pedro_g_s和 @flipper83(顺便说下,这两个同事是Android开发厉害角色)在 Tuenti 友好的讨论之后,我认为现在是写一篇关于架构Android应用的好时机。
这篇文章的目的是秀一下过去几个月萦绕在头脑中一些方法,加上我从调查和实现中学到的一些事情。
入门
我们都知道,写高质量软件是困难和复杂的:软件不只是满足需求,而且应该足够健壮、可维护、可测试和灵活来适应发展和变化。这个就是“
整洁架构”的来源,而且是一个可以应用的好方法当开发任何应用软件。
思想很简单:
整洁架构代表着一组可以用来构建系统的实践:
- 独立于框架层
- 可测试
- 独立于UI
- 独立于数据库
- 独立于任何外部代理
从上面图片可以看到四个环,当然不必需只是四个环,图片仅仅概要的。但是你应该考虑到
依赖规则(Denpendency Rule):
源代码依赖仅仅指向内部,但是内环代码不能知道外环的任何事情
下面一些能够更好熟悉和理解这个方法的一些术语:
实体(Entities): 应用的业务对象
用例(Use Case): 这些用例协调流向或者流出实体的数据流,也叫做交互件
用例(Use Case): 这些用例协调流向或者流出实体的数据流,也叫做交互件
接口适配器(Interface Adapters): 这些适配器集合用来转换数据,这些数据格式是对用例和实体方便使用的
框架和驱动(Frameworks and Drivers): 这个就是细节所在: UI、工具、框架等等。
我们的情景
为了事情进行,我从一个简单的情形开始:仅创建一个小应用,来显示从云端获取的好友和用户列表,当点击其中的一个,将会打开一个新的屏幕来显示这个用户的更多的信息。
这里给你一个视频,以便你可以了解我正在说什么的一个蓝图
(注: 视频不能播放)
Android 架构
目标是,通过业务规则完全不知道外部世界,来实现
关注分离(separation of concerns)。所以,业务规则可以在没有依赖外部元素的情况下测试。
为了达到这一点,
我的建议是,把工程分解为三个不同的层级,在每个层级有自己的目的,而且和其他层级相互隔离。
值得一提的是,每个层级有自己的数据模型,所以这个独立性是可以达到的(你将会在代码中看到,需要数据映射来完成数据迁移。当你不想你的模型在整个应用中交叉使用,你需要付出这个代价)
下面是一个图解,用来说明这个是看上去是什么样的:
注意:我没有使用任何外部库(除了gson库来解析json数据,junit,mockito, robolectric和espresso来测试)。理由是这使得这个例子变得更加清楚。不管怎样, 请不要犹豫添加ORMs来储存磁盘数据,或者任何依赖注射框架,或者任何你熟悉的工具和库,这些都会使得生活更加便利。(记住,
重新发明轮子不是一个好的实践)。
呈现层
有关视图和动画的逻辑发生在这里。这里仅仅是使用了
模型-视图-呈现(Model View Presenter)(从现在开始缩写
MVP ),但是你可以用其他的模式,像MVC或者MVVM。我不想涉及到关于它的细节。但是
fragments和activities在这个只不过是视图,除了UI逻辑,没有其他的逻辑在他们里面,而且所有的渲染事务都是在这里发生的。这个层级的
呈现是和
交互件(用例)合成的,这些交互件是在Android UI线程以外的新的线程来完成工作的,然后通过一个回调返回将要在视图中渲染的数据。
如果你想要一些比较酷的关于使用MVP和MVVM的
Effective Android UI 例子,可以看看我的朋友Pedro Gómez的做的事情
领域层
业务规则放在这里:所有逻辑发生在这里。关于Android项目,你也将看到所有的交互件(用例)的实现在这里。
这个层级是一个没有任何Android依赖的纯粹的Java模块。当连接业务对象时所有的外部组成部分应当使用接口。
数据层
应用所需的所有数据,通过一个UserRepository的实现(接口在领域层),都来自这个层级。这个实现使用仓储模式,即一个通过一个工厂根据不同的条件选择不同的数据源的策略。
比如,当通过id来获取一个用户,如果这个用户已经在磁盘缓冲,将选择磁盘缓冲数据源,否者,将查询云端来获取数据
,然后保存数据到磁盘缓冲。
所有这些背后的思想是数据来源对客户端来说是透明的,即客户端是不关心数据是否来自内存,磁盘,还是云端。唯一需要的是,数据能够来到和得到。
注意: 在代码中,我利用文件系统和Android preferences实现了一个简单初级的磁盘缓冲,这只是为了学习的目的。再次记住,如果有库更好地干这些工作,你就
不应该重新发明轮子。
错误处理
这个是一个永远需要讨论的主题。如果你能够分享你的解决方案,我将感激不尽。
我的策略是使用回调。比如,如果数据仓储发生了社么,将有两个回调方法
onResponse() 和
onError(). 后一个在“ErrorBundle”包装类里封装了异常。这个方法带来一些麻烦,因为回调链条是一个接一个直至这个错误走到渲染的呈现层的。代码可读性会打一些折扣。
另外一方面,我可以实现一个事件总线系统,如果某些事情出错了,系统就抛出事件。但是这个解决方案有点像
GOTO,而且依我看来,当你订阅了一些事件时如果你没有仔细控制,有时你会变得很迷惑。
测试
关于测试,我倾向于不同的层级不同的解决方案:
- 呈现层:用Android instrumentation 和 espresso来集成和功能测试
- 领域层: 单元测试这里用Junit 加上mockito
- 数据层: 集成和功能测试用Robolectric(这个层级有Android依赖)、junit和mockito
秀上代码
我知道你一直在想代码在哪里呢。呵呵,这个是代码的
github链接,在这里你可以发现我做了什么。关于目录结构,有一些需要声明,不同层级使用模块表示
- 呈现:这个是呈现层的Android模块
- 领域:一个没有Android依赖的模块
- 数据: 获取所有数据的Android模块
- 数据测试: 数据层的测试。使用robolectric时,由于一些限制,我不得不在独立的一个Java模块中使用
总结
就像鲍勃叔叔说的,
架构是目的,而不是框架。我完全同意这个箴言。当然,做不同的事情(不同的实现)有不同的方式,我非常肯定你就像我一样每天面对着不同的挑战,但是,通过这个技巧,你可以肯定你的软件能
- 容易维护
- 容易测试
- 非常具有结合性
- 解耦合
作为总结,
我强烈推荐你着手试试看,分享你的结果和经验,发现有没有其他更好的方法,我一直坚信,
持续改进永远是正确的事情。
希望这篇文章对你有所帮助,和以前一样,非常欢迎反馈
链接和资源
// 花了四个小时翻译校对,欢迎指正,请勿拍砖。