“面向对象就是一个错误!”
IlyaSuzdalnitski Python大本营 昨天
作者 | Ilya Suzdalnitski
编译 | 弯月 责编 | 张文
出品 | CSDN(ID:CSDNnews)
C++和 Java 可能是计算机科学领域最大的错误。就连面向对象的创建者 Alan Kay 都曾对这两门语言提出了严厉的批评。然而,C++和 Java 都是比较主流的面向对象语言。
面向对象编程的流行是计算机科学领域的不幸,它对现代经济造成了极大的破坏,造成了数万亿美元的间接损失。在过去的三十年中,几乎所有行业都因潜在的面向对象编程危机而受到影响。
为什么面向对象编程如此危险?下面我们一起来寻找答案。
2007 年 9 月,美国 Jean Bookout 驾驶的 2005 款凯美瑞突然失控,Bookout 尝试刹车但是失败,最终发生了碰撞事故,导致车内另一人身亡,Bookout 受伤。然而,此案只是丰田在美上百起车辆意外加速投诉的其中之一。
在 Bookout 事件调查的过程中,原告方聘请了两位软件专家,他们花了将近 18 个月的时间来研究丰田代码。最终,他们都形容丰田代码库为“面条式代码”(Spaghetticode),程序的流向就像一盘面条一样扭曲纠结在一起。
软件专家演示了大量丰田软件可能导致意外加速的情况。最终,丰田被迫召回 900 多万辆汽车,赔付款项高达 30 多亿美元。
面条式代码有什么问题?
然而,丰田并不是唯一一家有面条式代码问题的公司。曾经有两架波音 737 Max 飞机坠毁,造成 346 人死亡,损失超过 600 亿美元。这两起事件的原因也出在了软件 bug 上,而且都是由面条式代码引起的。
面条式代码困扰着全世界上许许多多的代码库,包括飞机、医疗设备以及核电站上运行的代码。
程序代码不是为机器编写的,而是为人类编写的。Martin Fowler 曾说过:“任何傻瓜都可以编写计算机能够理解的代码。但只有优秀的程序员可以编写人类能够理解的代码。”
如果代码不能正常运行,那说明出了问题。但是,如果人们不理解代码,那么它肯定会出问题。迟早的事儿。
此处,我们来谈论一下人类的大脑。人脑是世界上最强大的机器。但是,它有其自身的局限性。我们的工作记忆是有限的,人脑一次最多只能思考 5 件事。这意味着,程序代码的编写方式不应该超出人脑的局限。
然而,面条式代码导致人类无法理解代码库。这就会埋下深远的祸根,因为我们不清楚某些代码变动是否会引发问题。我们无法运行详尽的测试,找出所有缺陷,甚至没有人知道这样的系统是否能正常工作。即便系统能够正常工作,我们也不明白为什么。
面条式代码的起因
为什么经过一段时间的发展之后,代码库会出现面条式代码?因为熵。宇宙中的一切都变得混乱无序。就像电缆终将乱如一团麻,我们的代码最终也将变得混乱不堪。除非我们施加足够的约束。
为什么高速公路有时速限制?这是为了防止我们撞车。为什么道路上有交通信号?为了防止人们走错路,为了防止事故发生。
编程也一样。这样的约束不应让程序员来决定,应该通过工具自动实现,或者理想情况下通过编程范例本身来实现。
为什么面向对象是万恶之源?
我们怎样才能施加足够的约束,防止面条式代码的出现?两个办法:手动或自动。手动很容易出错,人类难免会犯错。因此,我们理应自动执行此类约束。
然而,面向对象编程并不是我们一直在寻找的解决方案。它没有提供任何约束来帮忙解决代码扭曲纠缠的问题。一个人可以精通各种面向对象编程的最佳实践,例如依赖注入、测试驱动的开发、领域驱动的设计等(这些实践确实有帮助)。但是,这些都不是由编程范例本身来强制执行的(而且也没有相应的工具来强制执行最佳实践)。
面向对象编程内部没有任何功能可以帮助我们预防面条式代码,封装只是隐藏和打乱了程序的状态,只会让情况变得更糟。继承带来了更多的混乱。面向对象编程的多态性更是火上浇油,我们根本不知道程序运行时会采用哪种确切的执行路径。特别是在涉及多个继承级别时。
面向对象进一步加剧了面条式代码的问题
然而,面向对象的缺点可不止缺乏适当的约束。
在大多数面向对象编程语言中,默认情况下一切都是通过引用共享的。这实际上将一个程序变成了一个庞大的全局状态。这与面向对象原本的思想背道而驰。面向对象的创建者 Alan Kay 拥有生物学的背景。他想到了一种语言(Simula),可以让我们按照生物细胞的组织方式编写计算机程序。他希望有独立的程序(细胞)通过相互发送消息进行通信。独立程序的状态永远不会与外界共享(封装)。
AlanKay 从来也没想过让“细胞”直接进入其他细胞的内部做任何修改。但现代面向对象编程就这么干了,因为在现代面向对象编程中,默认情况下,一切都是通过引用共享的。这也意味着破坏正常功能的错误无法避免。修改程序的某一部分就会破坏其他功能(这在函数式编程等其他编程范例中很少见。)
我们可以清楚地看到,现代面向对象编程本质上就存在很大的缺陷。它不仅会让你在日常工作中痛苦不堪,而且还会让你夜不成寐。
可预测性
面条式代码是一个重大的问题。面向对象的代码特别容易形成面条式。
面条式代码导致软件无法维护,但这只是问题的一部分。此外,我们还希望软件具有可靠性,以及可预测性。
任何系统的用户都应该享受相同的、可预测的体验。踩下油门,汽车就会加速;相反,踩刹车,汽车就会减速。用计算机科学术语来说,我们希望汽车的行为是确定的。
我们非常不希望汽车表现出随机行为,例如加速器无法加速,或制动器不能减速(丰田的问题)。即使此类问题发生的概率非常低。
然而,大多数软件工程师的心态都是“我们的软件要足够好,才能让客户继续使用。”我们能做的只有这么多吗?当然不是,我们应该做得更好!然而,首先最起码应该解决程序的不确定性。
不确定性
在计算机科学中,确定性算法指的是针对相同的输入,算法始终能够表现出相同的行为。而不确定性算法恰恰相反,即便输入相同,每次运行算法也会表现出不同的行为。
举个例子:
console.log( 'result', computea(2) );
console.log( 'result', computea(2) );
console.log( 'result', computea(2) );
// output:
// result 4
// result 4
// result 4
无需在意上述函数的具体功能,你只需要知道对于相同的输入,它总是会返回相同的输出。下面,我们看一看另一个函数 computeb:
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
console.log( 'result', computeb(2) );
// output:
// result 4
// result 4
// result 4
// result 2 <= not good
这一次,这个函数在面对相同的输入时,却给了不同的输出。这两个函数之间有什么区别?前者针对相同的输入,总是能给出相同的输出,就像数学函数一样。换句话说,这个函数是确定的。而后者则不一定会输出预期的值,换句话说,这个函数是不确定的。
如何判断某个函数是确定的,还是不确定的?
不依赖外部状态的函数百分百都是确定的。 只调用其他确定的函数的函数也是确定的。
function computea(x) {
return x * x;
}
function computeb(x) {
return Math.random()< 0.9
? x * x
: x;
}
从确定的到不确定的
function add(a, b) {return a + b;};
const box = value => ({ value });
const two = box(2);
const twoPrime = box(2);
function add(a, b) {
return a.value +b.value;
}
console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));
// output:
// 2 + 2' == 4
// 2 + 2' == 4
// 2 + 2' == 4
function add(a, b) {
a.value += b.value;
return a.value;
}
console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));
console.log("2 + 2' == " + add(two, twoPrime));
// output:
// 2 + 2' == 4
// 2 + 2' == 6
// 2 + 2' == 8
总结一下
副作用
function add(a, b) { a.value += b.value;return a.value;}
纯 粹
面向对象编程是否是纯粹的?
银弹
无知并不值得羞愧,无知又不学才让人羞愧。 —— 本杰明·富兰克林