面向对象编程/OOP 简介
有关面向对象编程 OOP 的概述和历史,请参考维基百科文章.
我们预计读者对编程有基本了解,因为我们会用多种语言给出示例。我们会解释讨论中任何不明显的语法,尽管这并非重点。重点是提供一些关于语言风格的指示,以及对面向对象思想在现实世界中的应用的洞察。
我们将 OOP 分为两个阶段——经典阶段和现代阶段。虽然这种区分在一定程度上是任意的,但我们认为将 OOP 视为它在 1980 年代和 1990 年代初期的实践方式,可以说明当前实践的动机。
什么是经典 OOP?
面向对象编程可以追溯到一种名为Simula的语言,特别是 Simula 67,它在 1960 年代很流行。正是 Simula 首次引入了“类”和“对象”,从而产生了“面向对象”编程这一术语。到了 1990 年代初,人们在大型 OOP 项目方面积累了足够的经验,发现了一些局限性。诸如Self之类的语言、诸如接口编程(也称为组件或面向组件编程)之类的想法,以及诸如泛型编程之类的 методологии都是为了解决这些问题而开发的。尽管经常遭到 OOP 纯粹主义者的嘲笑,但在 1998 年——包括泛型编程功能——C++的标准化,真正迎来了 OOP 的现代时代,我们也称之为多范式编程。这主要是由于 C++ 的流行以及标准模板库(STL)的巧妙设计,向如此庞大的受众展示了这种新方法的实用性。
到 1980 年,施乐将Smalltalk提供给了外部人士,并将其恰当地命名为 Smalltalk-80。与其他早期的编程语言不同,Smalltalk 是一种完整的环境,而不仅仅是一种语言,这与当时的 Lisp 有着共同的特点。虽然 Lisp 机器预示着 IDE 的出现,但 Smalltalk 却开创了 GUI,最终影响了 Macintosh 电脑的开发。与此同时,施乐在 70 年代开发 Smalltalk 的时候,C 语言由于 UNIX 主要用 C 编写而变得流行。因此,正是 C——本来不可能成为候选者——被 Bjarne Stroustrup 与来自 Simula 的思想融合在一起,创造了“带类的 C”,它在 1983 年更名为 C++。1985 年,Bertrand Meyer 对 Smalltalk、C++ 以及各种附加在 Lisp 方言上的对象系统感到不满意,于是创建了 Eiffel。虽然 Eiffel 更近一些,但 Java 本质上是克隆了老式的 C++,因此我们将 Smalltalk、老式的 C++、Eiffel 和 Java 视为经典 OOP。附加在 Lisp 上的对象系统(最终在 1994 年标准化为 CLOS)产生了一种截然不同的方法。虽然我们不认为 CLOS 是经典 OOP,但它确实影响了现代 OOP。
经典 OOP 发展出过度依赖一种称为“继承”的技术的趋势,最终程序员意识到,他们在很多概念上截然不同的情况下使用继承。现代 OOP 基本上包含了这些概念,有时作为语言级别的特性,有时通过程序员实践。目标主要是松散耦合、更易于维护和重用。从历史上看,David Ungar 和 Randall Smith 在 1987 年完成了他们的第一个可工作的 Self 编译器,到 1990 年,Sun Microsystems 接手了这个项目。虽然 Self 没有真正发展成为现代 OOP 语言,但它是一种第二代 OOP 语言。然后,在 1990 年代初,Alexander Stepanov 和 Meng Lee 开创了泛型编程,并编写了 C++ STL 的早期草案。这开启了一种(仍在继续的)趋势,即在更传统的 OOP 环境中融入函数式编程思想,这是一种逆 CLOS。此外,1990 年代初还出现了CORBA 和微软的 COM,它们是导致最初 Windows API 出现的想法的自然延伸。这种接口或组件编程是封装——OOP 的基本原则,正如我们即将看到的那样——的自然延伸。所有这些发展都旨在进一步管理或降低复杂性。由于这是 OOP 的最初目标,并且这些技术与经典 OOP 结合使用,因此我们认为将它们纳入关于“OOP”的论述中是合适的。
因此,当代面向对象编程往往与经典面向对象编程截然不同。尤其是,现在有更多抽象可供选择,因此要习惯哪种抽象最适合哪种问题类型更具挑战性!在如今经典的书中,Gamma 等人介绍了设计模式,它帮助将各种 OOP 技术综合起来,并将其应用于非常常见的问题。
未来将包含更多函数式编程技术在 OOP 环境中的标准化,特别是 lambda 表达式和闭包,以及更强大的元编程结构。通过泛型或元编程技术自动应用设计模式是一个有趣的领域。
松散地说,术语对象用于唤起与现实世界中的物体(如椅子或吉他)的联系。但对于软件来说,只使用了一些简化的抽象,专门用于手头的任务。虽然真正的椅子是由原子和分子构成的,并根据物理定律及其原子构成对环境做出反应,但“椅子对象”会根据你是编写游戏还是为家具店编写 POS 系统而有很大差异。从椅子的角度来解决问题,不如从问题的角度来解决问题更有成效。
在本手册中使用的大多数语言中,你会发现从技术角度来说(类似于以 9600 波特率吹口哨,我理解),对象是“类的实例”。太好了,那是什么意思呢?好吧,我们可以将这种想法一直追溯到柏拉图及其柏拉图式理想。如果你花更多的时间看《比尔和泰德的奇妙冒险》,而不是读柏拉图,那么这个想法是,椅子的概念是一个独立于任何特定椅子的实体。换句话说,你可以抽象地想象一把椅子,而不必去想任何特定的椅子。
在大多数 OOP 语言中,这种抽象的椅子概念被称为类(来自分类),是实际制造椅子的原型或蓝图。用蓝图制造东西的行为通常被称为实例化,制造出来的东西既是对象,又是充当蓝图的类的实例。作为人类,我们通常倾向于反过来——我们对遇到的物体进行分类。我们可以很容易地识别出我们遇到的类似椅子的东西是椅子;正是这种分类让我们首先获得了术语类。
很容易陷入关于对象性的深奥哲学辩论;在一些领域,如知识表示和计算本体论,它们非常重要。然而,对于计算机程序员来说,只需要弄清楚你的应用程序需要了解和处理关于椅子的什么信息。这本身可能是一个非常困难的问题,通常没有必要使其更加困难!
如果你对整个概念仍然不太清楚,可以考虑一个更技术性的解释。结构体(结构)、记录、表和其他组织相关信息的 方式早于面向对象编程。你可能熟悉以下类似的 Pascal 代码
TYPE chair = RECORD
model : integer;
weight : integer;
height : integer;
color : COLOR;
END;
这实际上并没有创建一个 chair 变量,而是定义了当你创建 chair 变量时它会是什么样子。你可以继续创建椅子数组等等,正如我们希望你自己发现的那样,这种东西对于保持程序的可理解性是必不可少的。面向对象编程想要利用这种优势,并尽可能地从可理解性、正确性和简单性中获益。
一个基本的解决问题的方法是分而治之——你只需要弄清楚如何将你的问题分成子问题。面向对象编程的创新者意识到,我们已经找到了划分问题的方法,并且这反映在我们组织数据的方式中,就像上面一样。如果你查看包含那个 chair RECORD 的应用程序,你肯定会发现很多关于椅子操作的代码。为什么还要费心定义它呢?所以,如果你要将所有这些代码从整个应用程序中提取出来,并将其与 chair 定义放在一起,那么推理是,你应该更容易确保
- 所有关于椅子的代码都是正确的
- 所有关于椅子的代码彼此一致
- 没有重复的关于椅子的代码
- 总的来说,你的代码更整洁,因为关于椅子的代码不再与沙发代码等混在一起
所以,你将那个 chair 定义和从整个应用程序中提取的代码放在一起,并称之为一个类。将 chair 变量称为一个对象,你就开始进行面向对象编程了。
由于这本应该是实用的书籍,让我们看看我们的第一个代码示例,这次是 Python 代码
class Chair:
model = None
height = None
weight = None
color = None
def has_arms(self):
return self.model % 2 # odd-numbered models have armrests
这看起来与 Pascal 示例并没有太大区别。唯一的区别是 class Chair 现在包含一个 has_arms 方法。方法是在类中定义的函数,通常用于处理某些特定于类的 数据。希望目的很清楚,所以我要指出重要部分:has_arms 是一个计算或推断属性——这些信息不是直接存储的。
在 Python 中,你会像这样使用这个类
c = Chair()
c.model = 15
c.height = 40
c.weight = 10
c.color = 7
if c.has_arms():
do_something
else:
do_other_thing
这里,"c" 变量是 'chair' 类的实例,也称为 chair 对象。我们像在 Pascal 中一样初始化属性(这将在以后改变!),如果椅子有扶手就做一件事,否则就做另一件事。但我们从未初始化 "has_arms" 属性或类似的东西。
由于目标之一是让你对语法无关,我们再次用 C++ 语言展示了同一个例子
class Chair
{
public:
int model;
int weight;
int height;
int color;
bool has_arms()
{
return model % 2 ? true : false;
}
};
Chair c;
c.model = 15;
c.height = 40;
c.weight = 10;
c.color = 7;
if (c.has_arms())
do_something();
else
do_other_thing();
现在,我们只想提一下,这仅仅是冰山一角,这个例子并不代表好的风格(在任何一种语言中)。创建功能如此少的对象似乎没什么意义,你很容易生成不需要新类的等效代码
struct Chair
{
int model;
int weight;
int height;
int color;
} c;
Chair c = {15, 40, 10, 7 };
if (c.model % 2)
do_something();
else
do_other_thing();
本节的目的是帮助你理解这些术语;我们将在关于封装、多态性、继承等等部分深入探讨其优势。然而,让我们说,虽然“底层”方式可能看起来更短更简单,但面向对象的优势随着程序大小和复杂性的增加而增加。这并不奇怪,因为这就是面向对象编程的设计目的,但这确实让简单的例子难以找到。所以请耐心等待。