软件开发架构师

Android价值十亿美元的错误-InfoQ

前端 5 2019-11-09 01:34

Android价值十亿美元的错误-InfoQ-1

这是一篇关于 Android 价值十几亿美元级错误的文章,包含那些被假设的错误和没有说出来的错误,本文还讨论了不要用糟糕的文档误导新开发人员的重要性。

十亿美元的错误的故事

你有没有听过价值数十亿美元的错误的故事? 下面就是一个很好的例子:

我把它称为我的价值十几亿美元的错误。这是关于 1965 年 null 引用的发明。那时,我正在用面向对象语言 (ALGOL W) 设计第一个全面的引用类型系统,我的目标是确保所有引用的使用都是绝对安全的,由编译器自动执行检查。但是我无法抗拒放入 null 引用的诱惑,因为它太容易实现了。这导致了无数的错误、漏洞和系统崩溃,在过去的 40 年里可能造成了十亿美元的麻烦和损失。

2009 年,Tony Hoare 在伦敦 QCon
https://en.wikipedia.org/wiki/Tony_Hoare

Tony Hoare 是一位编程英雄

如果你和我一样,当你第一次听到这句话的时候,你的反应是:“哇哦,我也犯了很多错误,但通常不会导致那么多钱的损失!”

最近我对此有了更深入的思考,现在我认为 Tony Hoare 是一个伟大的编程英雄!这不仅是因为他在这个 10 亿美元的错误之外所做的一切令人印象深刻的工作。

Android价值十亿美元的错误-InfoQ-2

我认为,因为他公开承认了它的“错误”,这也使他变得更伟大了!

你认为他是唯一 一个犯了 10 亿美元级错误的程序员吗? 仔细思考一下,IT 行业规模庞大,Facebook、谷歌、亚马逊、苹果、微软的市值在 5000 亿到 1 万亿美元之间。任何使其估值缩水 0.2% 的编程错误都可以被定义为数十亿美元的错误。

Tony Hoare 被称为“犯下数十亿美元错误的人”的真正原因是,他明确而公开地将自己的决定描述为一个错误,并通过这样做发出了一个明确的信号,即事情必须改变。

这样做我的朋友们会为软件行业带来巨大的利益,这就是为什么 Kotlin 和其他编程语言在它们的类型系统中构建了 null-safety 。它们仍然有 null,这本身不是问题,但它集成在类型系统中,以确保所有引用都是绝对安全的,由编译器自动执行检查。

Tony Hoare 是一个真正的好人,一个不以自我为中心的程序员,他敢于为一个错误承担责任,让我们充分认知错误的严重性,我们都应该感谢他。

回到 Android 世界,事情有点不同。在深入研究问题之前,我们将从一个简单的示例开始。

Android 匈牙利标记法

在 Android 刚开始的 9 年里,世界上大多数 Android 的代码都经受着无意义的变量匈牙利标记法的困扰。

它的缺点是对 Android Studio 中简单的代码高亮显示规则没有任何好处,并且明显的缺点是使所有东西的可读性降低。

当你在 2019 年之前提出这个问题时,你通常会得到以下两个答案:

  • 这就是现状,所以是好的。
  • Android 团队的要求,如果您在 Android 开源项目中贡献代码,就必须遵循这个约定。

但实际上

  • 第一个答案是错误的。我们之所以这样讲是因为自从匈牙利记数法被废除后,再没有什么人想要使用它了。
  • 第二个答案更糟糕,它属于“不偏不倚”的类别。这种说法基本上是说其他人都错了。我们明显可以反问:为什么? 因为每个人都在学习 Android 文档和示例,关于这个约定到处都是。这正是在开始时没有完善的一致性约定工作,而这恰巧导致了一个有害的规则。

是什么扼杀了 2019 年 5 月的匈牙利标记? 不是对错误的改正,而是因为要引入 Kotlin。为什么我们要等这么久?

Android 的十亿美元的错误

其实这涉及到很多方面:最大的错误是关于延至今日的 Android 编程的教授方式,它对编程实践有很大的破坏力,早期决策的短视是这一切混乱的根源。 我们应该认识到这个错误,并向每个人发出警告,让他们停止走这条路。但首先,我需要处理一些反馈,我得到的反馈是,将 Android 所做的事情贴上“错误”的标签太苛刻了。Android 不是我们这个时代最大的成功之一吗?

定义“错误”这个词

Android 显然是一个巨大的商业成功,我并不是说要反对这个。Android 和 iPhone 成功地在智能手机领域形成了双重垄断,因此即便有什么肯定也不是战术上的“错误”。我们必须使用 Android 团队提供的任何工具。

我也认为从用户的角度来看 Android 是一个很好的操作系统。你可以更多地喜欢 iOS,我对此也没什么意见,但这并不会让 Android 变差。

在本文的上下文中,错误意味着在一条会给开发人员带来痛苦的道路上误导他们。

我也不是说这是 Android SDK 中唯一的大错误,也不一定是 Android SDK 中最重要的错误。

如果你想了解 Android 的缺点,#androiddev Reddit 社区整理了一个非常有用的列表,列出了 Android 的缺点。但这里我要关注一个有趣的基础性错误。

Android 墨西哥卷设计模式

关于 Android 一件令人悲伤的事是,官方 Android 样例都采用以色列费雷尔卡马乔称为 Android 墨西哥卷的设计模式:将一切包装成一个 GodActivity 或 GodFragment,然后一切基于此完成。

官方的 camera-samples 就是一个很好的例子。不幸的是,我不能在这里展示它,因为它比我的文章长的多,但看看他的结构就可以了:

复制代码
public inline fun needsRefactoring(): Nothing = throw NotImplementedError(""" This does too much and needs to be refactored. Don't put any kind of logic in the Activities and Fragments. """.trimIndent()) class Camera2BasicFragment : Fragment(), View.OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback { private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) = needsRefactoring() override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) = needsRefactoring() override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) = needsRefactoring() override fun onSurfaceTextureUpdated(texture: SurfaceTexture) = needsRefactoring() } private lateinit var cameraId: String private lateinit var textureView: AutoFitTextureView private var captureSession: CameraCaptureSession? = null private var cameraDevice: CameraDevice? = null private lateinit var previewSize: Size private val stateCallback = object : CameraDevice.StateCallback() { override fun onOpened(cameraDevice: CameraDevice) = needsRefactoring() override fun onDisconnected(cameraDevice: CameraDevice) = needsRefactoring() override fun onError(cameraDevice: CameraDevice, error: Int) = needsRefactoring() } private var backgroundThread: HandlerThread? = null private var backgroundHandler: Handler? = null private var imageReader: ImageReader? = null private lateinit var file: File private val onImageAvailableListener: ImageReader.OnImageAvailableListener = needsRefactoring() private lateinit var previewRequestBuilder: CaptureRequest.Builder private lateinit var previewRequest: CaptureRequest private var state = STATE_PREVIEW private val cameraOpenCloseLock = Semaphore(1) private var flashSupported = false private var sensorOrientation = 0 private val captureCallback = object : CameraCaptureSession.CaptureCallback() { private fun process(result: CaptureResult): Unit = needsRefactoring() private fun capturePicture(result: CaptureResult): Unit = needsRefactoring() override fun onCaptureProgressed(session: CameraCaptureSession, request: CaptureRequest, partialResult: CaptureResult) = needsRefactoring() override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) = needsRefactoring() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = needsRefactoring() override fun onViewCreated(view: View, savedInstanceState: Bundle?) = needsRefactoring() override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) needsRefactoring() } override fun onResume() { super.onResume() needsRefactoring() } override fun onPause() { super.onPause() needsRefactoring() } private fun requestCameraPermission(): Unit = needsRefactoring() override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Unit = needsRefactoring() private fun setUpCameraOutputs(width: Int, height: Int): Unit = needsRefactoring() private fun areDimensionsSwapped(displayRotation: Int): Boolean = needsRefactoring() private fun openCamera(width: Int, height: Int): Unit = needsRefactoring() private fun closeCamera(): Unit = needsRefactoring() private fun startBackgroundThread(): Unit = needsRefactoring() private fun stopBackgroundThread(): Unit = needsRefactoring() private fun createCameraPreviewSession(): Unit = needsRefactoring() private fun configureTransform(viewWidth: Int, viewHeight: Int): Unit = needsRefactoring() private fun lockFocus(): Unit = needsRefactoring() private fun runPrecaptureSequence(): Unit = needsRefactoring() private fun captureStillPicture(): Unit = needsRefactoring() private fun unlockFocus(): Unit = needsRefactoring() override fun onClick(view: View): Unit = needsRefactoring() private fun setAutoFlash(requestBuilder: CaptureRequest.Builder): Unit = needsRefactoring() companion object { init { needsRefactoring() } private val ORIENTATIONS = SparseIntArray() private val FRAGMENT_DIALOG = "dialog" private val TAG = "Camera2BasicFragment" private val STATE_PREVIEW = 0 private val STATE_WAITING_LOCK = 1 private val STATE_WAITING_PRECAPTURE = 2 private val STATE_WAITING_NON_PRECAPTURE = 3 private val STATE_PICTURE_TAKEN = 4 private val MAX_PREVIEW_WIDTH = 1920 private val MAX_PREVIEW_HEIGHT = 1080 @JvmStatic private fun chooseOptimalSize( choices: Array<Size>, textureViewWidth: Int, textureViewHeight: Int, maxWidth: Int, maxHeight: Int, aspectRatio: Size): Size = needsRefactoring() @JvmStatic fun newInstance(): Camera2BasicFragment = needsRefactoring() } }

到这里看全部信息 android / camera-samples / Camera2BasicFragment.kt

Activity 中有一个新活动事件后前一个事件都会被挤到后台。直到现在,官方 Android 文档和示例都是这样做的。

如果你遵循安卓的墨西哥卷设计模式,会出现什么问题呢?

崩溃

Activity 是一种特殊的环境,充满了随时可能爆炸的地雷。最明显的问题是,由于这个复杂的生命周期,您的 Activity 可能在任何时候被系统终止。使用具有简单生命周期 (如 Application) 的上下文要安全得多。

内存泄漏

Activity 是绑定到整个用户界面的高消费对象。依附于 Activity 对象很容易产生很多麻烦。随之而来的是内存泄漏。实际上,这是一个非常常见的陷阱,甚至在 Android SDK 本身的类中也会看到这个错误,不管是在一些糟糕的三星 fork 中,还是在 Android 开源项目本身中。这是一个如此常见的问题,以至于 square 的工程师投入了很多时间和精力以实现自动检测这些问题。

Android价值十亿美元的错误-InfoQ-3
点击查看 GitHub

大量遗留代码

遗留代码经常被用作一个模糊的术语,意思是“代码非常难以理解,以至于您害怕更改它”。Michael Feathers 的经典著作《有效地使用遗留代码》有一个更精确且具有操作性的定义:任何没有被单元测试自动覆盖的代码都可以被定义为遗留代码。

任何遵循 Android Burrito 设计模式的代码都可能立即成为遗留代码。

我一直想知道为什么官方的 Android 文档如此强调仪表化测试。

以我的经验,这些很难写,从根本上来说很慢——它们必须在 Android 设备上运行——最糟糕的是,当它们失败时,我们能从中获得的错误信息很少。

我采用了完全相反的方案,写了很多简单、快速、有侧重点的 JVM 测试用例,结果要好得多。事实上,谷歌的测试团队有一篇精彩的文章,解释了为什么端到端测试是一个好的方案却在实践中失败了:

不要再用端到端测试了
好的想法常常在实践中失败,在测试的世界中,一个公认的好测试方案也常常会在实践中失败,这就是建立在端到端测试基础上的测试策略。
[请阅读整篇文章,非常好]
谷歌测试博客:不要再用端到端测试了

所以仪表化的 Android 测试不是一个好方案。

但老实说,如果你把你的逻辑放到 Android 组件里,你能做的就只有这些了。

检验墨西哥卷的唯一方法就是品尝。

回想起来,Android 墨西哥卷的设计模式明显是错误的,这让我很好奇:它从何而来,又是如何存活到今天的?

安卓墨西哥卷的设计模式是如何形成的?

一些 Context 组件

给你一些 Context 组件,这是 Android SDK 1.0 的两个最基本的构件:

  • android.content.Context 提供对有关应用程序环境的所有全局信息的访问。它允许访问特定于应用程序的资源和类,以及应用程序级操作的向上调用,如启动活动、广播和接收意图等。
  • android.app.Activity 为一个应用程序提供了一个 main() 函数,但是添加了很多移动操作系统需要的功能,最重要的是一个复杂的 Activity 生命周期

Activity 就是一个 Context

在 Android 1.0 有一个致命的错误

复制代码
package android.app; import android.content.Context; class Activity extends Context { }

首先讲一点理论知识。
继承和组合

在你的面向对象编程课程中,你可能记得对象之间有两种非常不同的关系:

  • 继承:房子是建筑的一种
  • 组合:一所房子有一个房间

比起继承,组合更受大家的喜欢,它也是一个众所周知的设计原则,在一些有影响力的书中也提到过。

Android 只是另一种 SDK(软件开发工具包),但可能有一个原因,原则不适用这里? 我知道事实并非如此,因为……

Fragment 不是上下文

如果你看看来自 Android SDK 的另一个构建块 androidx.app.Fragment ,它与 Activity 非常相似,但在后面才被引入,你应该注意到它并没有扩展 Context。但 Fragment 具有上下文。

那么,为什么 Android 团队改变了主意,尽管并没有大肆宣扬?

在 Android 中一切都需要上下文

你可以也应该避免墨西哥卷的设计模式。但你不能逃避的是,在 Android 中,你需要一个上下文来做基本上所有的事情:

复制代码
class SomeThirdPartyClass { fun doStuff(contex: Context) = TODO() }

但即使是这个比较传统的 SomeThirdPartyClass 类也是一个随时可能爆炸的地雷。
Activity 是一个 Context,所以很容易将 this@Activity 作为参数传递给 doStuff()。但是这样做是错误的,你不能确定某个 SomeThirdPartyClass 正在做正确的事情,或者你正在做正确的事情。崩溃、内存泄漏和不可测试性将接踵而至。

现在的文档和示例仍然很差

我想指出的是,我说的不仅仅是一个历史性的短视决策。

2014 年,我还是一名年轻且经验不足的 Android 开发人员,周围也是一群年轻且经验不足的 Android 开发人员。我们试图了解 Android 是如何工作的,并使用 Android 文档和示例作为指导。回想起来,这是一个可怕的错误。我们最终得到了一个难以理解、难以测试、甚至难以修改的痛苦的烂摊子。不是因为我们没有遵循“Android 最佳实践”,而是因为我们恰恰遵循了!

快进到今天,虽然在许多领域都取得了进展,但 Android 官方文档和示例的很大一部分仍然编写得很糟糕。它继续误导新一代缺乏经验的开发人员。正如 Bob 叔叔会告诉你的那样,大多数开发人员都是新手,因为 IT 行业的规模每五年就会扩大一倍。

我知道,对于某一学派来说,所有这些都是公平的游戏。“这些错误是愚蠢的,我是一个真正的程序员,不会上当。但你总不能阻止蠢人变蠢吧?”

但是我来自为人类设计的思想学派,所以在我看来,当一个程序员犯了一个错误,那是程序员的错,但是当超过十年,成千上万的程序员犯了同样的错误,那就是设计师没有做好工作。理想情况下,做正确的事情应该是容易的,且不容易搬起石头砸自己的脚。

所以,现在是时候明确地指出,墨西哥卷的 Activity 和 Fragment 是不可接受的。修复文档和示例也已经是早就该做的事情了。

错误已经犯下

我确实理解,尽管这些错误在今天令人痛苦,但它们是在特定的历史背景下犯下的。Android 项目不得不做出一些改变或者变得无关紧要,这是一个不同的领域,那时智能手机的功能还没有今天这么强大。

这和 JavaScript 是一样的。它的设计在短短十天内就完成了,然后在 Netscape Navigator 1.0 中发布,其中很多设计都成为历史了。

这并不是说没有解决办法可以解决这类历史错误。聪明人一旦痛苦地意识到问题所在,通常会很快找到解决方案。这正是托尼•霍尔 (Tony Hoare) 无私诚实的伟大之处:它立刻让人们意识到,这里有一个问题需要解决。这正是当今 Android 世界所缺乏的。直到现在,官方的 Android 文档仍然继续使用 Android 墨西哥卷设计模式。

请允许我引用 Tony Hoare 的话作为结尾:

这导致了无数的错误、漏洞和系统崩溃,在过去的十年中已经造成了价值数十亿美元的麻烦和损失。

原文链接
Android’s billion-dollar mistake(s)

文章评论