面向对象编程(OOP)的简单解释






2.58/5 (15投票s)
学习正确进行面向对象编程的方法
引言
面向对象编程(或 OOP)是计算机编程中最容易被误解的东西之一。对于那些似乎无法在实践中应用 OOP 概念的初学者来说,它尤其具有挑战性。他们可能理解变量赋值、if
-then
-else
、循环、函数,甚至模块,但类(classes)、对象(objects)和方法(methods)究竟是做什么的呢?为什么我们需要继承(inheritance)和多态(polymorphism)?在编写程序时如何使用这些东西?
背景
我最近在 Quora 上回答了这个问题:面向对象编程有哪些独特的优势?
我的回答
面向对象编程是一种将大型、复杂编程问题分解成更小、更易于管理部分的方法。如果做得好,它可以使解决方案更容易理解、更容易维护,并且可扩展!
没错,可扩展!我说出来了。历史上,使用 C++ 和 Java 的 OOP 解决方案在可扩展性方面存在问题。它们也表现出其他与将 OOP 视为抽象数据类型相关的问题,例如“钻石问题”和“脆弱基类问题”。
然而,如果你以 Smalltalk 的方式进行 OOP,正如 Alan Kay 在施乐 PARC 的设想那样,你将获得一种非常简洁、灵活且可扩展的方法。Alan Kay 的 OOP 概念并非将其视为抽象数据类型,而是视为由通过消息协议相互通信的虚拟计算机组成的虚拟网络。
他受到了细胞的生物系统的启发,众所周知,生物系统具有极强的可扩展性。看看像昆虫和老鼠这样最小的生物。再看看像大象和鲸鱼这样最大的生物。
这种方法对于互联网也同样适用,互联网具有极其、极其惊人的可扩展性。Web 服务器就像对象,它们都通过 HTTP 协议进行通信,这类似于消息。
Erlang 和 Elixir 通过其 BEAM 技术也采用了这种方法。事实上,Smalltalk 直接启发了 Erlang,根据 Alan Kay 的设想,Erlang 可以被恰当地认为是一种面向对象的语言。
Alan Kay 曾说过,当他构思 OOP 时,他并没有想到 C++。
总而言之,OOP 的关键优势在于以可扩展的方式管理程序复杂性。它已经在实践中得到验证,尤其是在 Smalltalk 领域。请注意,函数式编程(functional programming),其主要的竞争范式,在这方面尚未得到证明。这使得面向对象编程非常独特。
证明完毕。
这启发我写这篇 Code Project 文章,用最简单的术语解释 OOP。
非结构化代码
让我们看看最简单、最低级别的程序代码是如何编写的,然后逐步向上发展,直到变得更容易管理大小和复杂性。假设我们用最简单的方式编写代码:将一大堆指令放在一个文件中。它可能看起来像这样
main program:
======================
======================
======================
if ===================
===================
===================
===================
======================
======================
======================
while ================
===================
===================
===================
===================
======================
======================
======================
======================
======================
for ==================
===================
===================
===================
======================
======================
======================
======================
if ===================
===================
===================
===================
else =================
===================
===================
===================
======================
======================
======================
...
这可能持续数千上万行!它可能包含大量浪费的代码重复。它将难以阅读和理解,因为你缺少提醒你在哪里发生了什么的“路标”。
过程式编程
这就是我们第一个编程范式(programming paradigm)的起源。它被称为过程式编程(procedural programming),它使用过程(procedures)或函数(functions)将我们无尽的代码堆分解成更易于管理的部分。过程或函数是一系列指令,可以从任何地方被“调用”(called);它也可以被重复调用,从而避免代码重复。(函数是返回一个值的过程。)它的样子可能如下
main program:
======================
======================
======================
call fred
======================
======================
if ===================
===================
===================
call eric
======================
======================
======================
x := bob()
======================
======================
while ================
===================
===================
======================
======================
call eric
======================
======================
=====================
procedure fred:
======================
======================
=====================
procedure eric:
if ===================
===================
===================
======================
======================
=====================
function bob:
======================
======================
return ===============
这比原始代码库有了很大的改进。然而,随着程序的规模不断增长,即使是这样也可能变得失控。我们可以通过引入模块(modules)的概念来改善这种情况。模块就是一个函数的集合和一个供这些函数操作的数据结构。通常,一个模块代表某种内聚的抽象——数据结构本身有意义,而函数集合与之相伴。现在,我们的代码看起来是这样的
main program:
======================
======================
======================
call fred
======================
======================
if ===================
===================
===================
call eric
======================
======================
======================
x := bob()
======================
======================
while ================
===================
===================
======================
======================
call eric
======================
======================
=====================
module Tom
data:
======================
=====================
procedure fred:
======================
======================
=====================
procedure eric:
if ===================
===================
===================
======================
======================
=====================
end module To
function bob:
======================
======================
return ===============
在思维上,你可以更好地组织你对这个程序的想法。模块 Tom 是一大块代码,你可以将其视为一个单一单元。除非必要,否则你不必深入其细节。这是一种“抽象”(abstract)代码的非常有用的方式。
现在,你可能会认为这就是管理程序复杂性的全部。当你有了模块,你还需要什么呢?
类、对象和消息
然而,有一种更高层次的抽象代码的方式,可以使其更容易理解和更易于扩展:将你的编程解决方案建模为一个协作对象的集合或网络。这是杰出的远见者 Alan Kay 在 20 世纪 70 年代于施乐 PARC 创建 Smalltalk 编程系统时所设想的。
虽然面向对象编程(object-oriented programming)的种子可能在 Simula 67 中就已经播下,但正是 Smalltalk 和 Alan Kay 的构想将 OOP 推向了聚光灯下。此后,Smalltalk 直接启发了我们今天使用的几乎所有其他 OOP 语言。
那么 OOP 的理念是什么?对象(object)实际上是模块概念的演进。对象是一个封装了数据和行为的实体,其中行为是一组操作数据的函数或“方法”。听起来和模块很像,不是吗?
但是有一些关键的区别。首先,对象封装的数据对外部世界是隐藏的。模块则不是这样。
其次,对象通常比模块更精细、更具体的抽象。它经常代表现实世界中的事物。它可以非常小而简单。它比模块具有更高的内聚性。
第三,根据 Alan Kay 的设想,对象非常像一台拥有自己私有内部状态和通信协议的计算机。你通过向对象发送消息(messages)来与其通信。你通过发送消息来请求对象为你做某事,它会做出响应,就像网络中的实际计算机服务器一样。就像在实际的计算机服务器中一样,你无权访问其内部状态。
对象不是抽象数据类型
……其中抽象数据类型(Abstract Data Type)是一个听起来很专业的术语,指的是一种数据结构。
像 Java 和 C++ 这样的 OOP 语言与 Alan Kay 的 OOP 构想大相径庭。它们使 OOP 比实际需要更难,并且是许多人困惑的永恒根源。正如 Robert C. Martin 在“OOP vs FP”中所说的那样,对象是函数的集合,而不是数据的集合。对象不是数据结构。
模块也不是。
第四,与模块不同,对象可以从另一个对象(“父”对象)继承数据和行为,并添加额外的数据和行为。这对于扩展程序功能非常有益。它可以节省代码重复。
继承与另一个概念紧密相连:多态(polymorphism)。当你向对象发送消息时,对象可能会通过调用其某个方法来响应,或者它可能会调用其父对象中的方法。对象决定,而不是你。
类(class)是对象创建的模板。它包含了从它创建的任何对象的完整定义。当我们从类创建对象时,我们说我们实例化(instantiate)了这个类。这有点类似于声明一个来自模块的特定类型变量。一旦实例化了一个对象或声明了一个模块数据结构,你就可以继续调用操作数据的相关方法或函数。
使用 RKE 伪代码,我们的例子看起来是这样的
main program:
======================
======================
aDick := new Dick # instantiate using class Dick
aDick.fred() # send message fred to aDick
======================
======================
if ===================
===================
===================
aDick.eric()
======================
======================
======================
x := bob()
======================
======================
while ================
===================
===================
======================
======================
aDick.eric()
======================
======================
=====================
define class Dick
data:
======================
=====================
method fred:
======================
======================
=====================
method eric:
if ===================
===================
===================
======================
======================
=====================
end class Dic
function bob:
======================
======================
return ===============
摘要
归根结底,调用一个过程、调用一个模块中的过程或调用一个对象的方法之间并没有太大的区别。这仅仅是你选择如何抽象数据和行为(函数)的问题。
在 OOP 中,你必须识别机会来以合理的方式封装数据和函数。你可以从现实世界中获得灵感。你可以运用创造力和想象力。任何对你有用的方法。
你会发现,OOP 仅仅是过程式编程和模块的扩展。它并没有你想象的那么难,尤其是如果你使用 Smalltalk。
有关 OOP 的一些示例,请阅读什么是面向对象编程?