对象和面向对象
计算机源于数学理论和电子技术的结合,而编程则是我们与计算机之间对话的一种媒介或手段。正如语言可以描绘其他的创作形式,如诗歌、绘画、音乐、舞蹈等,编程语言就是一种可以创建思想和表达结构的工具。
抽象
抽象是对复杂事务的一种简单化概括。如果要向从没有吃过苹果的爱斯基摩人描述苹果是什么,只能说一种水果,形状像一个拳头大小的雪球,能吃,有点甜也有点酸
——这就是一种对苹果的抽象和类比描述。同样,汇编语言是对底层机器码的抽象,而高级编程语言则是对汇编语言的抽象。如果没有汇编语言和高级语言,恐怕今天所有从事计算机编程的人都不得不学习如何用二进制来向计算机输入指令,并读懂输出的二进制结果了,那今时今日的鼠标、键盘以及我们所看见的一切图形化的东西都不存在了,这种计算机也失去了存在的意义。
虽然与汇编相比,类似C、BASIC这类高级语言已经有了巨大的质的提升,但它们所谓的抽象,依然要求人们从计算机的结构,也就是寄存器指令、内存地址等方面来解决具体问题,而非从问题本身的角度出发来理解问题的内涵。
所以,那时候的工程师们,不得不在 机器模型(实际能解决问题的上下文空间)和 问题模型(需要面对的麻烦的上下文空间)之间建立起一种特定的关联。
这种努力既相当耗费精力,又不能通用,基本上都只能处理非常具体的问题。问题模型一旦发生变化,这个过程又得从头再来。这种方式开发出来的程序代码,不仅极为脆弱,而且维护代价高得离谱,因为没人愿意接手这样的烂摊子
。
在这种背景下,计算机科学家们,尤其是软件专家们想尽一切办法,试图来结束这种痛苦
——他们尝试各种做法,试图从一切不同的角度来观察世界
。在这种努力尝试的过程中,诞生了很多种不同的编程方法,有基于归纳的编程模式(如LISP和APL),有基于约束的编程模式(如PROLOG),还有基于图形符号设计的编程模式(如FORTRAN、COBOL和PL\I),但它们都无法做到通用——只要问题模型
稍稍超出其机器模型
范畴,它们就无能为力了。
但这种尝试也并非一无所获,因为在此期间诞生了一种称为Smalltalk的编程语言。
Smalltalk和以往所有的编程语言都不同,用Alan Curtis Kay(艾伦·凯)的话来说,Smalltalk具有一些划时代的特性。
万物皆对象。这是从问题本身的特性抽象出的概念,而后通过程序代码来表现这种特性。
对象之间能彼此传递消息。要调用另一个
对象
的方法
,就需要告诉它具体的请求上下文。对象是嵌套的。一个对象还可以容纳其他的对象,例如
人
总是有手
、脚
、身体
和五官
的。每个对象都有与之相对应的类型。每个
对象
都是某个抽象类型的具体实例
。例如,李星云
是一个人
,同时,也是一个热血青年
。同一类对象能接收相同的消息。对于
志愿者
来说,他们有自己的组织,能够定期或不定期地收到
只针对志愿者的开展援助
消息。这也意味着Smalltalk可以让程序统一指挥
某一类对象,让它们做出符合预期的行为。这称为对象的可替换性
。

Smalltalk是世界上第二门面向对象的编程语言,也是世界上第一个完全面向对象
的编程语言,它在程序设计的基础上跨出了一大步。由于对象
这种概念具有极其普遍的代表性,而且不受限于特定类型的问题(甚至问题
本身也是对象)。相对于机器模型
来说,问题模型
空间中的元素在机器模型
中的映射被Smalltalk称为对象
。
通过对象
、消息
、对象嵌套
和对象类型
,Smalltalk可以非常灵活地对程序代码进行调整,以便解决任何特定
的问题。当阅读这些代码时,也差不多就是在理解问题模型
中的问题本身,也就是业务逻辑。
Smalltalk也因此被称之为面向对象的语言(Object-Oriented Language,OOL),而使用它来实现开发的过程,也叫做面向对象编程(Object-Oriented Programming,OOP)。
面向对象编程允许人们从现实世界的角度来描述问题:它们有各自的状态,某一类对象能进行特定的操作,这完全符合人们对现实世界的理解。
自Smalltalk之后,又出现了更多、更强大的面向对象编程语言,而Java就是它们当中的集大成者——一门纯面向对象
且跨平台的编程语言。
封装
一般来说,科学家侧重于研究,而工程师侧重于应用开发,二者之间相辅相成。
当科学家发布研究成果的时候,工程师会将这些成果转化为产品
、交付件
或商品
。在很多大中型公司中也有类似的划分,一个是基础架构部门,专门设计、研究并搭建各种基础组件、设施和装备;另一个是应用开发部门,专门将这些基础组件、设施和装备应用于业务功能的开发之中,然后反馈问题,让基础架构部门改进。
有时候,有些工具的功能还未完全成型,不能完全公开其中的内容,这样既可以有效地避免该工具被错误地使用和篡改,减少出错的概率;又可以避免让应用开发工程师陷入到具体的细节中。
因为,对于 封装 来说,其目的和意义有三点。
让应用程序员避免陷入细节之中——应用程序员并不关心问题是怎样解决的,他们只关心当他们向这些组件、设施和装备输入信息时,是否能得到它合理且正确的响应。例如,对于一个登录功能,用户并不需要知道当他收入手机号以后,前台页面、后台服务以及它们之间会发生什么事件,以及这些事件是如何确保用户能够登录成功的,用户所关心的只是登录后能使用系统提供的哪些功能而已。而封装正是对用户屏蔽了这些细节,排除了干扰,让他得以专心地处理自己的事务——这是应用层次的封装;
当一个对象向另一个对象发出消息,或者说
调用请求
时(例如对象B调用对象A中的方法),工程师也不想知道对象A的功能是怎么实现的,他只关心调用对象A的功能后,能否得到正确的结果——这是开发层次的封装(其实是应用层次封装的一种内涵延申而已)。使组件、设施和装备的创建者(如科学家)在不影响工程师们使用的情况下能够继续完善和更新这些组件、设施和装备。这涉及到封装的另一层意思:访问控制。一般来说,OOP有三个显式的关键字来实现对类的访问控制:
public
、private
和protected
。这些访问修饰符分别表示公开、私有和受保护的访问方式,它决定了谁
以及如何
使用方法、变量或类——这是访问控制层次封装。
封装并非OOP所独有的特性,一些面向过程的编程语言,例如BASIC也有封装的概念,例如将多个函数功能封装
在一起,共同对外提供服务,这也叫封装。
继承
继承是OOP的关键特性之一。
当一位亿万富翁去世以后,其子嗣往往能得到大量的财富,继承
了上一辈的遗产,避免了从头打拼的艰辛;在为人处世、行事风格,乃至包括样貌形态上,这位富翁的后代也继承
了他的诸多特点,这些特点也许可以让他们赚到同样多的财富。
继承 是对象
的一种克隆手段,通过这种手段,对象
能够将自己的属性(类似于拥有的资源
或标签
)和方法(类似于性格
或行为
)完整地复制到另一个对象中去(这和物种的繁衍非常像)。从开发层面来说,这么做的好处在于:工程师不必每次都从头开始创建另一个几乎一模一样的对象,而是只需要创建出一个模板
或公共父亲
(OOP称之为父类
),再通过它来克隆所需的子嗣
即可。而且每个克隆后的子嗣
都可以通过修改它们的属性和行为,来实现个性化
的微调。

例如,动物
这个大类是哺乳类
、爬行类
和鸟类
的共同父类。
所有的动物都有
皮肤
,都有血液
。所有的动物都需要
吃东西
,都要喝水
,它们都会繁衍
、排泄
、自由行动
,而且遇到天敌时会逃跑
。
但是,作为子嗣
(也就是OOP中的子类
),它们也都有各自的特色
。
哺乳类体表有体毛,用于保持恒温,而且哺乳类大多会奔跑。
爬行类体表被鳞片覆盖,而且体温会随着环境的变化而变化,它们基本上是以爬行为主。
鸟类的体表被羽毛替代,只有这样才能利用空气动力进行飞行。
因为父类和子类有一些共同的行为,例如吃东西
,所以它们都可以接收某些共同的消息
,如饥饿感。但对于爬行类来说,它就无法接收到上升气流发出的可以飞行
信号,因为只有鸟类才能接收到它。
在继承当中存在两种关系。
是一个(is-a) 的关系:
麻雀
是一个鸟
,蜥蜴
是一个爬虫
等。这表示说,麻雀和鸟之间,蜥蜴和爬虫之间是直接的父子
关系,不是领养、不是过继,就是最直接的血缘亲属。像是一个(is-like-a) 的关系:
鲲鹏
像是一个鸟
,美杜莎
像是一个爬虫
等。这是借助于想象力,在原父类
的基础上,给子类
添加了一些父类
不存在的元素(属性和行为)。虽然此时子类
仍然具备某些父类
的元素,但这种替代并非符合自然生物的衍变规律,是一种不合理或不完美的替代,这种关系就被称为 像是一个(is-like-a)。
这两种关系,在实际开发中都有大量的应用场景。
多态
在程序处理这种类的层次结构
时,通常会将对象
当作某一个类来看待,而不会把它当成具体的实例。这是什么意思呢?例如,对于动物来说,既然它们都要吃东西,都要喝水,都要行动,那么是不是可以编写出不局限于特定类型的代码?如果单独给麻雀写一套吃东西
、喝水
和行动
的代码,然后再给鸟类写一套,然后给猴子再来一套,这样下去什么时候才算完?所以,对于程序来说,它操作的是针对类的方法
或行为
,而不管它们是麻雀,鸟类还是猴子。
这样一来,即使新创建或清除掉某个子类
,也不会对程序有任何影响——这正是OOP的强大扩展能力之一。
但这里面有个问题,即便是吃东西
这个行为,对于哺乳类和鸟类来说是有区别的:哺乳类大多用咀嚼
,而鸟类几乎都是生吞
。如果用程序来实现这个行为,那它怎么知道是哪一种动物在吃东西?又是哪一种动物在行动
呢(爬行类和鸟类的行动方式也是不同的)?
这就是面向对象编程的另一个关键特性:程序运行时的动态绑定!
以前的高级语言,例如BASIC、PASCAL这种结构化的设计语言,他们虽然比汇编语言更抽象,但也就是仅仅做了一层指令的封装,将人可以理解的东西翻译成机器可以理解的东西。这些语言的编译器只能理解事先赋予它们的东西,也就是在程序运行之前,一些变量、方法的地址空间就已经被分配好了,是无法在运行时改变的。
但面向对象的编程语言不同,它存在着独一无二的类的层次结构
,所以即使是在程序运行期间,只要不立即访问某个对象的属性,或调用某个对象的方法,那么可以说该对象是 不存在的——直到运行时才能真正确定它所在的内存地址。这就给了相同父类
的不同子类
实现不同方法的绝佳机会:当创建一个哺乳类对象时(在OOP中也称为实例化
),只要它不吃东西
,就可以认为它像它的父类动物
那样去吃东西
。而一旦它真正要进食的时候,程序才会给吃东西
这个行为分配内存地址,同时发现它是哺乳类
,进而改变其吃东西
的方式为咀嚼
!
这种 程序运行时动态绑定(地址)的特性,被OOP称之为 多态。
重载与重写
提到多态,一般情况下也要提一下重写
,然后再顺带提一下它和重载
的区别。
所谓重写
,就是当子类
继承了父类
的行为以后,并不想或不能够原封不动地去实现这些行为,而是需要根据自己的情况来做出相应改变。就比如之前说过的吃东西
,对于哺乳类和鸟类来说就是完全不同的行为。
而重载
虽有一字之差,但指的是另一个完全不同的意思。当某个类或对象有两个相同的行为却完全不同的参数时,这些具有相同名称的行为或方法就称为重载方法
。例如,在家吃东西
和在飞机上吃东西
就是不同的行为(当然这个比喻未必恰当,但现实世界中很难举出两个名称一样但参数
不一样的行为)。
复用
无论什么时候,都不必也不应该重复造轮子。实现登录的代码,即使换一个系统,其原理和过程也都是差不多的,把约束条件稍稍修改一下,就应该能够立即运行起来。
复用
也并不是OOP所独有的优点,即使BASIC、C或者PASCAL也能做到复用。
有两种形式的复用关系
。
组合(Composition)
:这是一种整体
和部分
的关系,而且这种关系具有统一性,也就是说,如果整体不存在了,那么部分也会消失;而且大多数时候反过来也成立。例如,人
和人身上的心脏
的关系。可以说:人
包含一个(contains-a)心脏
。聚合(Aggregation)
:这是一种关系较弱的组合。它也表示整体和部分的关系,但它当中的部分
是可以脱离于整体
而独立存在的。例如,某个互联网大厂 有一个(has-a) 名叫李星云的员工。
在OOP中,经常会重点强调继承
特性,这就导致很多新手程序员认为继承应当随处可见
。但其实用这种思路开发出来的程序通常都比较复杂。
相反,在创建新类型时建议首先考虑组合
或聚合
关系,因为它们更简单灵活,更易扩展与维护,且设计更加清晰。
接口
古希腊哲学家亚里士多德(Aristotle)曾提出过 类 这样的概念,他认为所有的对象都是唯一的
(例如,你今天吃的那条鱼是世界上唯一的一条,以后也不会再有和它完全相同的一条鱼存在了),但同时这些唯一的对象也都是具有相同的特性和行为的类的一部分。
亚里士多德的这种对象
思想被世界上第一个面向对象编程语言Simula-67所采用,它在程序中使用关键字class
来引入新的类型,又通过关键字type
来定义接口
,而class
只是实现接口的一种特殊方式。
创建好类或类型以后,可以生成许多实例化的对象,但如果需要这些对象完成所需的功能,就需要通过这些对象的行为才能完成了。
所谓接口
,指的是一种行为的抽象。例如,吃东西
是一种行为的抽象,不管是哺乳动物、爬行类还是鸟类,都有这个行为。如果工程师将进食
功能定义为吃东西
,那么对于OOP来说,就可以通过多态来实现所有动物吃东西
的这个功能。反之,如果将进食
功能仅仅定义为咀嚼
,那结果就只有哺乳动物能够活下来,而大部分爬行类和所有鸟类将全部饿死。
因此,类或对象是对现实世界实际事物(OOP将这种实际事物
统称为 实体)的抽象,而接口则是对实体行为的抽象。
如今所说的面向接口编程,其意义之一就是上面所说的对行为的抽象。
/**
* 行为接口,有一个行为的抽象方法:吃东西
*
*/
public interface Behavior {
public void eat();
}
/**
* 鸟类实现了行为接口,并重写了“吃东西”这个方法
*
*/
public class Bird implements Behavior {
@Override
public void eat() {
System.out.println("生吞");
// TODO SOMETHING
}
}
// 将对象实例化为行为的抽象
Behavior bird = new Bird();
bird.eat();
上面的代码展示了实际开发中的所谓面向接口编程
是怎么回事。
感谢支持
更多内容,请移步《超级个体》。