这是一篇关于 Android 价值十几亿美元级错误的文章,包含那些被假设的错误和没有说出来的错误,本文还讨论了不要用糟糕的文档误导新开发人员的重要性。
十亿美元的错误的故事
你有没有听过价值数十亿美元的错误的故事?下面就是一个很好的例子:
我把它称为我的价值十几亿美元的错误。这是关于 1965 年 null 引用的发明。那时,我正在用面向对象语言(ALGOL W)设计第一个全面的引用类型系统,我的目标是确保所有引用的使用都是绝对安全的,由编译器自动执行检查。但是我无法抗拒放入 null 引用的诱惑,因为它太容易实现了。这导致了无数的错误、漏洞和系统崩溃,在过去的 40 年里可能造成了十亿美元的麻烦和损失。
2009 年,Tony Hoare 在伦敦 QCon
Tony Hoare 是一位编程英雄
如果你和我一样,当你第一次听到这句话的时候,你的反应是:“哇哦,我也犯了很多错误,但通常不会导致那么多钱的损失!”
最近我对此有了更深入的思考,现在我认为Tony Hoare是一个伟大的编程英雄!这不仅是因为他在这个 10 亿美元的错误之外所做的一切令人印象深刻的工作。
我认为,因为他公开承认了它的“错误”,这也使他变得更伟大了!
你认为他是唯一 一个犯了 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 就是一个很好的例子。不幸的是,我不能在这里展示它,因为它比我的文章长的多,但看看他的结构就可以了:
到这里看全部信息android / camera-samples / Camera2BasicFragment.kt
Activity 中有一个新活动事件后前一个事件都会被挤到后台。直到现在,官方 Android 文档和示例都是这样做的。
如果你遵循安卓的墨西哥卷设计模式,会出现什么问题呢?
崩溃
Activity 是一种特殊的环境,充满了随时可能爆炸的地雷。最明显的问题是,由于这个复杂的生命周期,您的 Activity 可能在任何时候被系统终止。使用具有简单生命周期(如 Application)的上下文要安全得多。
内存泄漏
Activity 是绑定到整个用户界面的高消费对象。依附于 Activity 对象很容易产生很多麻烦。随之而来的是内存泄漏。实际上,这是一个非常常见的陷阱,甚至在 Android SDK 本身的类中也会看到这个错误,不管是在一些糟糕的三星 fork 中,还是在 Android 开源项目本身中。这是一个如此常见的问题,以至于 square 的工程师投入了很多时间和精力以实现自动检测这些问题。
大量遗留代码
遗留代码经常被用作一个模糊的术语,意思是“代码非常难以理解,以至于您害怕更改它”。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 有一个致命的错误
首先讲一点理论知识。
继承和组合
在你的面向对象编程课程中,你可能记得对象之间有两种非常不同的关系:
继承:房子是建筑的一种
组合:一所房子有一个房间
比起继承,组合更受大家的喜欢,它也是一个众所周知的设计原则,在一些有影响力的书中也提到过。
《设计模式:可重用的面向对象软件的元素》(1994),作者:Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides
Android 只是另一种 SDK(软件开发工具包),但可能有一个原因,原则不适用这里?我知道事实并非如此,因为……
Fragment 不是上下文
如果你看看来自 Android SDK 的另一个构建块androidx.app.Fragment,它与 Activity 非常相似,但在后面才被引入,你应该注意到它并没有扩展 Context。但 Fragment 具有上下文。
那么,为什么 Android 团队改变了主意,尽管并没有大肆宣扬?
在 Android 中一切都需要上下文
你可以也应该避免墨西哥卷的设计模式。但你不能逃避的是,在 Android 中,你需要一个上下文来做基本上所有的事情:
但即使是这个比较传统的 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)
评论