面向对象编程(OOP)具有多方面的吸引力。对管理人员,它实现了更快和更廉价的开发与维护过程。
对分析与设计人员,建模处理变得更加简单,能生成清晰、易于维护的设计方案。
对程序员,对象模型显得如此高雅和浅显。
此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务。
每个人都可从中获益,至少表面如此。
如果说它有缺点,那就是掌握它需付出的代价。
思考对象的时候,需要采用形象思维,而不是程序化的思维。
与程序化设计相比,对象的设计过程更具挑战性——特别是在尝试创建可重复使用(可再生)的对象时。
过去,那些初涉面向对象编程领域的人都必须进行一项令人痛苦的选择:
(1) 选择一种诸如Smalltalk 的语言,“出师”前必须掌握一个巨型的库。事实上,很难很好地设计出对象——从而很难设计好任何东西。
因此,只有数量相当少的“专家”能设计出最好的对象,然后让其他人享用。
对于成功的 OOP语言,它们不仅集成了这种语言的语法以及一个编译程序(编译器),而且还有一个成功的开发环境,其中包含设计优良、易于使用的库。
所以,大多数程序员的首要任务就是用现有的对象解决自己的应用问题。
本章的目标就是向大家揭示出面向对象编程的概念,并证明它有多么简单。
本章将向大家解释Java 的多项设计思想,并从概念上解释面向对象的程序设计。
但要注意在阅读完本章后,并不能立即编写出全功能的Java 程序。所有详细的说明和示例会在本书的其他章节慢慢道来。
1.1 抽象的进步所有编程语言的最终目的都是提供一种“抽象”方法。
一种较有争议的说法是:解决问题的复杂程度直接取决于抽象的种类及质量。
这儿的“种类”是指准备对什么进行“抽象”?汇编语言是对基础机器的少量抽象。
后来的许多“命令式”语言(如FORTRAN,BASIC 和 C)是对汇编语言的一种抽象。
与汇编语言相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非考虑问题本身的结
构。
在机器模型(位于“方案空间”)与实际解决的问题模型(位于“问题空间”)之间,程序员必须建立起一种联系。
这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。
由此造成的副作用便是一门完善的“编程方法”学科。
为机器建模的另一个方法是为要解决的问题制作模型。
对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。
PROLOG则将所有问题都归纳为决策链。
对于这些语言,我们认为它们一部分是面向基于“强制”的编程,另一部分则是专为处理图形符号设计的。
每种方法都有自己特殊的用途,适合解决某一类的问题。
但只要超出了它们力所能及的范围,就会显得非常笨拙。
面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具表达问题空间内的元素。
由于这种表达非常普遍,所以不必受限于特定类型的问题。
我们将问题空间中的元素以及它们在方案空间的表示物称作“对象”(Object)。
当然,还有一些在问题空间没有对应体的其他对象。
通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。
所以在阅读方案的描述代码时,会读到对问题进行表达的话语。
与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。
总之,OOP允许我们根据问题来描述问题,而不是根据方案。
然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;
它们有自己的状态,而且可要求它们进行特定的操作。
与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。
Alan Kay 总结了 Smalltalk 的五大基本特征。这是第一种成功的面向对象程序设计语言,也是Java 的基础语言。
通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
(1) 所有东西都是对象。可将对象想象成一种新型变量;它保存着数据,但可要求它对自身进行操作。
理论上讲,可从要解决的问题身上提出所有概念性的组件,然后在程序中将其表达为一个对象。
(2) 程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。
为了向对象发出请求,需向那28个对象“发送一条消息”。
更具体地讲,可将消息想象为一个调用请求,它调用的是从属于目标对象的一个子例程或函数。
(3) 每个对象都有自己的存储空间,可容纳其他对象。
或者说,通过封装现有对象,可制作出新型对象。
所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
(4) 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。
一个类最重要的特征就是“能将什么消息发给它?”。
(5) 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。
由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消
息。
这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。
这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式的程序问题,提倡将不同的方法组合成“多形程序设计语言”(注释②)。亚里士多德或许是认真研究“类型”概念的第一人,他曾谈及“鱼类和鸟类”的问题。
在世界首例面向对象语言Simula-67 中,第一次用到了这样的一个概念:
所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具有通用的特征和行为。
在Simula-67 中,首次用到了class 这个关键字,它为程序引入了一个全新的类型(clas 和 type 通常可互换使用;注释③)。
③:有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”是那个接口的一种特殊实现方式。Simula是一个很好的例子。
正如这个名字所暗示的,它的作用是“模拟”(Simulate)象“银行出纳员”这样的经典问题。
在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:
每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。
与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。
所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了“class”关键字。
当您看到“type”这个字的时候,请同时想到“class”;反之亦然。
建好一个类后,可根据情况生成许多对象。
随后,可将那些对象作为要解决问题中存在的元素进行处理。
事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”对应或映射关系。
如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。
每个对象仅能接受特定的请求。
我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。
“类型”与“接口”的等价或对应关系是面向对象程序设计的基础。
下面让我们以电灯泡为例:
Light lt = new Light();
lt.on();
在这个例子中,类型/类的名称是 Light,可向 Light 对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。
通过简单地声明一个名字(lt),我们为 Light 对象创建了一个“句柄”。然后用new关键字新建类型为 Light 的一个对象。
再用等号将其赋给句柄。为了向对象发送一条消息,我们列出句柄名(lt),再用一个句点符号(.)把它同消息名称(on)连接起来。
从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
1.3 实现方案的隐藏
为方便后面的讨论,让我们先对这一领域的从业人员作一下分类。
从根本上说,大致有两方面的人员涉足面向对象的编程:“类创建者”(创建新数据类型的人)以及“客户程序员”(在自己的应用程序中采用现成数据类型的人;注释④)。
对客户程序员来讲,最主要的目标就是收集一个充斥着各种类的编程“工具箱”,以便快速开发符合自己要求的应用。
而对类创建者来说,他们的目标则是从头构建一个类,只向客户程序员开放有必要开放的东西(接口),其他所有细节都隐藏起来。
为什么要这样做?隐藏之后,客户程序员就不能接触和改变那些细节,所以原创者不用担心自己的作品会受到非法修改,可确保它们不会对其他人造成影响。
④:感谢我的朋友Scott Meyers,是他帮我起了这个名字。
“接口”(Interface)规定了可对一个特定的对象发出哪些请求。
然而,必须在某个地方存在着一些代码,以便满足这些请求。
这些代码与那些隐藏起来的数据便叫作“隐藏的实现”。
站在程式化程序编写(Procedural Programming)的角度,整个问题并不显得复杂。
一种类型含有与每种可能的请求关联起来的函数。
一旦向对象发出一个特定的请求,就会调用那个函数。
我们通常将这个过程总结为向对象“发送一条消息”(提出一个请求)。
对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。
对于任何关系,重要一点是让牵连到的所有成员都遵守相同的规则。
创建一个库时,相当于同客户程序员建立了一种关系。
对方也是程序员,但他们的目标是组合出一个特定的应用(程序),或者用您的库构建一个更大的库。
若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事情,没有办法强制他们遵守任何约束。
即便非常不愿客户程序员直接操作类内包含的一些成员,但倘若未进行访问控制,就没有办法阻止这一情况的发生——所有东西都会暴露无遗。
有两方面的原因促使我们控制对成员的访问。
第一个原因是防止程序员接触他们不该接触的东西——通常是内部数据类型的设计思想。
若只是为了解决特定的问题,用户只需操作接口即可,毋需明白这些信息。
我们向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。
进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。
例如,我们最开始可能设计了一个形式简单的类,以便简化开发。
以后又决定进行改写,使其更快地运行。
若接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。
Java 采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边界:public,private,protected 以及暗示性的friendly。
若未明确指定其他关键字,则默认为后者。
这些关键字的使用和含义都是相当直观的,它们决定了谁能使用后续的定义内容。
“public”(公共)意味着后续的定义任何人均可使用。
而在另一方面,“private”(私有)意味着除您自己、类型的创建者以及那个类型的内部函数成员,其他任何人都不能访问后续的定义信息。
private在您与客户程序员之间竖起了一堵墙。
若有人试图访问私有成员,就会得到一个编译期错误。
“friendly”(友好的)涉及“包装”或“封装”(Package)的概念——即Java 用来构建库的方法。
若某样东西是“友好的”,意味着它只能在这个包装的范围内使用(所以这一访问级别有时也叫作“包装访问”)。
“protected”(受保护的)与“private”相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。
继承的问题不久就要谈到。