这篇文章中我们将通过面向对象设计中的单一职责原则来观察 Activity,并试图了解如何更好地在 Activity 中实施单一职责原则。
Activity
Activity 是一个应用组件,用户可与其提供的屏幕进行交互,以执行拨打电话、拍摄照片、发送电子邮件或查看地图等操作。
依照上述内容我们自然会想到 Activity 是应用程序的 UI。模块化设计变得特别流行并且许多缩写(如:MVP)成为 “热门词语” 的今天,对 Activity 作为 UI 元素这点尤其值得关注。
当我带着这些概念进行了一段时间的 Android 开发后,我开始怀疑、质疑,最终得出一个想法:UI 实现细节不应属于 Activity?这不是一个容易接受的概念,但当我把它应用到实际的开发过程之后,我再也没有疑惑。
单一职责原则
单一职责原则是由罗伯特·C·马丁(Robert C. Martin)提出的,定义为:
一个类或者模块应该有且只有一个改变的原因。(维基百科)
为了当前讨论的内容,我们将 “改变的原因” 缩小为两点:
重新设计那些不会改变应用基本功能的 UI(整容)
修改那些不需要变动 UI 的功能
我们将指定由于原因 #1(UI 变化)可能修改的逻辑为 “UI 逻辑”,由于原因 #2(功能变化)可能修改的逻辑为 “业务逻辑”。
为了遵循 “狭义的” 单一职责原则,所有的类不能同时包含 “UI 逻辑” 和 “业务逻辑”。
为什么要分离 UI 逻辑和业务逻辑
你可能会好奇为什么我们要遵循单一职责原则。虽然 UI 逻辑和业务逻辑可以共存,但分离有明显的好处:
修改 UI 更容易。更新,甚至替换 UI 的同时保持业务逻辑(几乎)不变
更高的可读性及可维护性。避免 UI 逻辑 “污染”、“混淆” 业务逻辑
更高的可测试性。如果 UI 实现不是 “抽象的”,测试业务逻辑的同时就需要兼顾 UI 方面的逻辑,这就是 UI 对于单元测试来说比较棘手的原因(以及 UI 变化时所有的 tests 都需要修改)。
虽然上面并没有列出所有的点,但足以表明为什么要分离 UI 逻辑和业务逻辑。
Activity 的 “标准” 实践
当我们认同 UI 逻辑和业务逻辑分离是一个理想系统的特性时,我们就可以回到 Activity 上。我们平常使用 Activity 的方式已经做到了这点吗?
在大多数应用中,我们都可以在某个 Activity 找到以下两个职责:
为用户与 UI 的交互注册监听器
执行相应的操作以响应用户与 UI 的交互
Activity 既注册监听器又处理用户的交互不是很正常吗?让我们换个角度来看待这个问题。
为了给 UI 组件注册监听器,Activity 必须知道它们的 ID(R.id.*)。这是明确的 UI 实现细节,Activity “知道” UI 组件的名称(通常也会知道它们的类型:TextView,Button 等)。因此,上述的职责 #1 归属于 “UI 逻辑”。
响应用户与 UI 的交互时,Activity 会处理许多的 UI 操作(例如改变颜色和形状),通常情况下还会执行一些额外的操作(例如在笔记应用中点击按钮后会保存笔记)。这部分操作并不属于 UI 操作,而且不以任何形式依赖于 UI。它们属于应用的功能,即应用的 “业务规则”。因此,一般情况下职责 #2 归属于 “业务逻辑”。
我们现在注意到的是:即使在同一个类中注册监听器及处理这些组件的交互也会导致 UI 逻辑和业务逻辑纠缠在一起。每个开发者都曾试过调试 500 多行的 Activity 代码,并且其中大部分的代码都是 UI 操作混杂几行业务逻辑。他们也懂得试图找到症结所在及如何解决问题(不影响其他逻辑)的痛楚。
为什么 Activity 不是 UI 元素
为了遵循我们 “狭义的” 单一职责原则,Activity 应该只包含 UI 逻辑或业务逻辑。哪种更好?
实际上 Android 团队已经为我们给出了答案。Activity 的依赖足以证明将业务逻辑从 Activity 中分离是不可能的:
Activity extends Context
虽然有更多的点使得 Activity 与业务逻辑不可分离(例如:运行时权限,LoaderManager的集成等),但这点已经足以证明。可能令人意想不到的是,所有人都习以为常的一个基本事实竟如此重要,但事实往往就这么简单。为了支持上述观点,让我对 Android 中的 Context 做一个简短的描述。
在功能上,Context 对象能为第三方应用提供大多数 Android 平台的功能。(值得注意的是,从面向对象编程的角度来看,Android 中 Context 是 God Object,违反所有 SOLID原则)
上述意思是 Android 的 Activity(其他与 Context 相关的类)是第三方应用与平台集成的主要区域。换句话说:我们使用 Activity 来控制平台的功能及资源以支持应用的功能。而这些功能及资源的相关逻辑即是我们的业务逻辑。因此,无论我们多么努力尝试,都无法将业务逻辑完全从 Activity 中分离。
由于我们不能从 Activity 中分离业务逻辑,所以必须从中分离所有 UI 逻辑。这不是一项简单的任务,但从长远来看,这样做是很有价值的。
脏度测试
为了方便讨论 UI 逻辑与业务逻辑的分离程度,我们应当定义某种 “脏度指标” —— 一个可作为指示逻辑 “脏” 的度量。我们将定义两个指标:一个用于业务逻辑,一个用于 UI 逻辑。
业务逻辑脏度测试(针对 Activitiy)
在 Activity 中出现以下任一情况算作一个 “脏点”:
查找 R.layout.* (布局文件)
查找 R.id.* (View 的 ID)
对上述两点的类或接口存在依赖
注意这个测试是 “可传递的” —— 不仅 Activity 不应了解 UI 细节,Activity 中引用的类也不能了解这些细节。因此,你不能简单地将所有 “脏代码” 放置于需要在 Activity 实例化的 “helper” 中(除非你不以 0 分为目标)。
UI 逻辑脏度测试(针对封装 UI 逻辑的类)
在封装 UI 逻辑的类中出现以下任一情况算作一个 “脏点”:
对 Activity 依赖
对第一点的类或接口存在依赖
注意这个测试也是 “可传递的” —— 从封装 UI 逻辑的类到 Activity 之间必须没有 “依赖链”。我不能将 Context 定义为 “脏点” —— 你需要提供 Context 给封装 UI 的类,因为没办法不借助 Context 来创建 View(毕竟 Context 是 God Object,对不?)。
上述的测试中,我们应以 0 分作为目标。实际上并不能一直达到 0 分,但我们应明确应用中有哪些 “脏点”,并且有充分的理由证明不能清除它们。
总结
在这篇文章中,我们讨论了为什么 Activity 不应包含 UI 逻辑,了解到将 UI 逻辑和业务逻辑分离是一个值得拥有的特性。然后表明由于 Activity 与 Android 框架各个部分之间非常紧密的耦合,将业务逻辑与 Activity 分离几乎是不可能的。我们还定义了业务逻辑和 UI 逻辑的 “脏度指标”,以便测量我们应用中的 “脏度”。