面向对象被高估了吗?
大多数专业编程语言都是面向对象的,包括Java和C语言。他们的关键特征(如封装和继承)在最近的记忆中多次受到攻击。面向对象的反对者声称这些特征过于复杂。他们说这些特征使代码编写和维护更加困难。我们将探讨这些论点,以及它们的反驳,以了解OOP在总体上有多大用处。
面向对象范式的特点
为什么面向对象编程范式("OOP "或 "OO范式")如此受欢迎?部分原因是它包含了其他范式所不具备的功能。如果这些特性使编程更容易,那么具有这些特性的OO语言应该被更多地使用。如果这些特性没有帮助,围绕它们建立的语言就不应该被广泛采用。
封装
封装是OOP的决定性特征。数据值成为属性,用于该数据的函数成为方法,两者都被保存在对象中("封装")。
一个对象的内部数据通常从外部隐藏,除了通过方法。如果你需要一个可重复使用的方法来打印一个文本文件中的字,你可以做一个TextFile
类。这成为TextFile
对象的模板,这些对象是该类的 "实例"。
class TextFile { // the full class
attribute filedata; // The content of the file is a data attribute.
attribute filelength; // So are the file's length and name.
attribute filename;
method print_file(self){ // reusable method to print this file's contents
System.print(self.filedata);
}
}
myTextFile = new TextFile("my_file.txt"); // Create a new TextFile object, an instance of the TextFile class.
myTextFile.print_file(); // Quickly print the file object's contents.
这使得编程更容易,因为大多数IDE可以自动完成一个对象的方法和属性。对函数和数据进行分组,使人们很容易看到所有可用的选项。另外,整个系统更加模块化。所有与文本文件相关的功能都保存在代码的一个部分,所以冲突和重复的可能性较小。
继承性
继承让一个类重用并建立在一个 "父类 "的现有功能上。当有许多类型的对象时,每种类型都需要一些独特的功能。通过为这些对象建立一个父类,每个对象只需要实现其独特的功能就可以工作。共享功能只需要写一次。
class TextFile { // TextFile is the parent class.
attribute filedata;
attribute filelength;
attribute filename;
method print_file(self){
System.print(self.filedata);
}
}
// MarkdownFile is a child of TextFile.
class MarkdownFile extends TextFile { // MarkdownFile uses TextFile's functionality, and "extends" it with custom methods.
method change_the_markdown_tables_to_be_better(self){ // Custom method, only usable by MarkdownFile.
for line in self.filedata {
line.fix_markdown_table(); // fix tables (only in markdown)
}
}
}
myMarkdown = new MarkdownFile("markdown.md"); // Create a MarkdownFile object.
myMarkdown.change_the_markdown_tables_to_be_better(); // Use the new feature...
myMarkdown.print_file(); //...but the normal TextFile functionality still works!
多态性
多态性意味着我们可以对一个对象做一些事情,即使我们还不知道该对象的确切类型。这建立在上面的继承思想之上。一个子类可以通过定义它自己的父类方法的版本而表现得与父类不同。然后我们可以在一个对象上调用该方法,不管它是父类还是子类。
class TextFile { // TextFile is the parent class.
attribute filedata;
attribute filelength;
attribute filename;
method print_file(self){
System.print(self.filedata);
}
}
// MarkdownFile is a child of TextFile.
class MarkdownFile extends TextFile {
method print_file(self){ // The child does something different with print_file()
System.print_differently(self.filedata); // printing child is different from printing parent
}
}
myTextFile = new TextFile("hello.txt"); // Create a TextFile object.
myTextFile.print_file(); // prints the file normally
myMarkdown = new MarkdownFile("markdown.md"); // Create a MarkdownFile object.
myMarkdown.print_file(); // Same method, but it prints the file differently.
反对OO特性的论点
如果这些功能如此方便,它们怎么会是坏的呢?
更多的对象,更多的问题
OOP的主要批评之一是针对封装本身。如果你有很多对象,每个对象都有内部数据(或 "状态")。在这种情况下,编程是关于跟踪和改变所有这些数据。但是,我们不是把数据传给一个函数(从输入到输出),而是改变一个对象的内部状态。在大多数大型程序中,有很多对象,它们都可以做自己的工作只要它们被允许对另一个对象的状态做微小的改变。
当然,如果许多对象可以改变彼此的状态,那就有问题了。做任何事情都会产生很多的副作用。没有人能够同时跟踪所有这些,对于超过几个对象。因此,程序的可预测性越来越低,错误越来越多。
"究竟什么是可改变的状态?任何可以改变的状态。想想OOP中的变量或字段....我们的大脑真的不擅长处理状态,因为我们的工作记忆中一次只能容纳大约5个项目。如果你只考虑代码做了什么,而不是它在代码库中改变了什么变量,那么推理一段代码就容易多了"。
继承是不现实的
面向对象的范式将问题建模为对象的层次结构,就像我们的 "父"-"子 "例子。然而,大多数现实世界中的继承并不像我们的例子那样工作。
例如,你可以有一个 "车辆 "模板来构建 "汽车 "和 "卡车"。然而,汽车可以是汽油动力的,也可以是电动的,而 "卡车 "可以是小型皮卡,也可以是18轮卡车。因此,车辆模板需要非常通用。现在,卡车必须指定发动机类型(尽管很少有电动的),汽车必须指定车轮数量(尽管大多数有4个)。
如果你想增加更多的车辆,比如 "坦克 "或 "潜水艇",事情就会变得更加棘手。你可以为新的车辆类型重新开始,但却失去了可重复使用的代码。你可以把更多的东西放在 "车辆 "类中,这就分散了功能,破坏了封装点--OOP的关键思想相互冲突。
多态性做得更好(在OOP之外)
面向对象编程的最大替代方案是函数式编程。与其把数据和函数放在一起,不如在一个地方定义数据,在另一个地方定义函数。
当有事情需要发生时,数据会被传递给一个有固定输出的函数,并且(最好)没有副作用。关于函数式编程,没有什么可以阻止多态性。比方说,你希望print_file()
函数对不同的数据有不同的作用。你可以用同样的名字定义不同的函数。
function print_file(Markdown){
// do something
}
function print_file()
或者传入数据类型
function print_file(myfile){
if myfile's type is MarkdownFile{
// do one thing
}
if myfile's type is RichTextFile{
// do another thing
}
}
或者任何其他的解决方案。多态性并不是OOP独有的,当子类的怪异性被排除在外时,它可能会更容易。
OO的防御措施
对于反对OOP的论点,存在着为其辩护的反驳意见。
首先,OO范式的问题可能是不良编码实践的错误。从理论上讲,好的面向对象程序员应该使用设计模式来解决问题。
用"开放-封闭原则 "来构建类,而不是编织纠结的状态网。不要把所有的共享功能放在父类中,而是为其他类建立一个原型模板。
OO的另一个关键论点是关于情景使用的。当一个现实生活中的问题很复杂时,它可能需要一个基于对象的系统的复杂性。
"我们如何向这些客户发送这封但不是那封的电子邮件,但不是那些客户?"OO是围绕对象之间的信息共享而建立的。这种范式可以更好地处理独特的对象。
相比之下,函数式范式是建立在以特定方式处理所有数据的函数上。这种系统在处理边缘案例时比较困难。
总结
面向对象的范式一直是长期和深入辩论的主题。毕竟,大规模公司的程序(以及他们的开发人员的职业生涯)都处于危险之中。如果OOP是有害的,大多数代码库可能需要进行根本性的改变以保持可维护性。如果OOP的顾虑被夸大了,批评者就会引诱开发者转向更糟糕的范式。
面向对象语言、函数式语言和其他语言正在争夺开发者的眼睛和公司的钱包。希望这篇文章能帮助你找到自己的位置。