Files
OnJava8/book/01-What-is-an-Object.md
2018-12-25 22:36:47 +08:00

15 KiB
Raw Blame History

第一章 对象的概念


“我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们,语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)

计算机革命的起源来自机器。编程语言就像是那台机器,它不仅是我们思维的放大工具和另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。

面向对象编程(Object-oriented programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对不太理解,可先行跳过本章。等你对编程具有一定理解后,请务必再回过头来看。因为这样你才能理解面对对象编程的重要性以及如何使用它来设计你的程序。

抽象

所有编程语言都提供一种“抽象”的方法。从某种程度上来说解决问题的复杂性与抽象的种类和质量直接相关。这里的“种类”意思是你准备对什么进行抽象汇编语言是对机器底层的一种少量抽象。后来的许多“命令式”语言如FORTRANBASIC 和 C是对汇编语言的一种抽象。与汇编语言相比这些语言已有了长足的进步但它们的抽象原理依然要求我们着重考虑计算机的结构而非问题本身的结构。

在机器模型(“解决方案空间”)与实际解决的问题模型(“问题空间”)之间,程序员必须建立起一种关联。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的“编程方法”学科。

为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有 问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为 处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。

面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种 表达非常具有普遍性所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示物称作“对象”Object。当然还有一些在问题空间没有对应的对象体。通过添加新的对象类型程序可进行灵活的调整以便与特定的问题配合。所以在阅读方案的描述代码时会读到对问题进行表达的话语。与我们以前见过的相比这无疑是一种更加灵活、更加强大的语言抽象方法。总之OOP 允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。

Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基础语言Alan Kay 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:

  1. 万物皆对象。你可以将对象想象成一种特殊的变量。它可以存储数据,可以在你对其“发出请求”时执行本身的操作。理论上讲,你可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表达为一个对象。
  2. 程序是一组对象,通过消息传递来告知彼此该做什么。要请求一个对象,你需要向该对象发送 一条消息。
  3. 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
  4. 每个对象都有一种类型。根据语法每个对象都是某个“类”的一个“实例”。其中“类”Class是“类型”Type的同义词。一个类最重要的特征就是“能将什么消息发给它”。
  5. 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法大家不久便能理解。由于类型为“圆”Circle的一个对象也属于类型为“形状”Shape的一个对象所以一个圆完全能接收形状消息。这意味着可让程序代码统一指挥“形状”令其自动控制所有符合“形状”描述的对象其中自然包括“圆”。这一特性称为对象的“可替换性”是OOP最重要的概念之一。

Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。

接口

亚里士多德(Aristotle或许是认真研究“类型”概念的第一人他曾谈及“鱼类和鸟类”的问题。是世界首例面向对象语言Simula-67 中,第一次用到了这样的一个概念: 所有对象——尽管各有特色——都属于某一系列对象的一部分这些对象具有通用的特征和行为。在Simula-67 中,首次用到了class 这个关键字,它为程序引入了一个全新的类型(class 和 type 通常可互换使用,有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”是那个接口的一种特殊实现方式)。

Simula 是一个很好的例子。正如这个名字所暗示的它的作用是“模拟”Simulate象“银行出纳员”这 样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一 些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有 自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实 体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。

因此在面向对象的程序设计中尽管我们真正要做的是新建各种各样的数据“类型”Type但几乎所 有面向对象的程序设计语言都采用了class关键字。当您看到“type”这个字的时候请同时想到class;反之亦然。

创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存 在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对 一”的映射关系。

那么如何利用对象完成真正有用的工作呢必须有一种办法能向对象发出请求令其解决一些实际的问题比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”Interface定义的对象的“类型”或“类”则规定了它的接口形式。“类 型”与“接口”的对应关系是面向对象程序设计的基础。

下面让我们以电灯泡为例:

reader

Light lt = new Light();
lt.on();

在这个例子中,类型/类的名称是 Light可向 Light 对象发出的请求包括包括打开on、关闭off、变得更明亮brighten或者变得更暗淡dim。通过简单地声明一个名字lt我们为 Light 对象创建了一个“句柄”。然后用new关键字新建类型为 Light 的一个对象。再用等号将其赋给句柄。

为了向对象发送一条消息我们列出句柄名lt再用一个句点符号.把它同消息名称on连接起来。从中可以看出使用一些预先定义好的类时我们在程序里采用的代码是非常简单和直观的。

上图遵循 UMLUnified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 UML 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。

服务提供

在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。

那么问题来了:我们该选择哪个对像来解决问题呢? 例如,你正在开发一个记事本程序。你可能会想在屏幕输入一个默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?

我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。

隐藏实现

我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否按照正确的规则来使用或是改变了该工具类。只有设定访问控制,才能从根本上阻止。

因此使用访问控制的原因有以下2点

  1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);

  2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松的完成改造。

Java 有三个显式关键字来设置类中的访问权限:public(公开),private(私有)和protected(受保护)。这些修饰符可以明确谁能访问后面的方法、变量或类。

  1. public (公开) 表示任何人都可以访问和使用该元素;

  2. private (私有) 除了类本身,外界无法直接访问该元素和内部方法。private是你和调用者之间的屏障。任何试图访问私有成员的人都会收到编译时错误;

  3. protected (受保护) 类似于private,区别是继承类(下一节就会引入继承的概念)可以访问protected的成员,但不能访问private成员;

  4. default (默认) 如果你不使用前面的三者,默认就是default访问权限。default被称之为包访问,因为该权限下的资源可以同一包(库组件)中其他类的成员访问。

复用性

创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。但并不象许多人希望的那样,这 种重复使用的能力并不容易实现;它要求较多的经验以及洞察力,这样才能设计出一个好的方案,才有可能 重复使用。

许多人认为代码或设计方案的重复使用是面向对象的程序设计提供的最伟大的一种杠杆。 为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能将那个类的一个对象置入一个新 类。我们把这叫作“创建一个成员对象”。新类可由任意数量和类型的其他对象构成。无论如何,只要新类 达到了设计要求即可。这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组织称 作“包含”关系,比如“一辆车包含了一个变速箱”。

对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”Private使用这个类的客户程序 员不能访问它们。这样一来,我们可在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行 期”更改成员,这进一步增大了灵活性。后面要讲到的“继承”并不具备这种灵活性,因为编译器必须对通 过继承创建的类加以限制。

由于继承的重要性,所以在面向对象的程序设计中,它经常被重点强调。作为新加入这一领域的程序员,或 许早已先入为主地认为“继承应当随处可见”。沿这种思路产生的设计将是非常笨拙的,会大大增加程序的 复杂程度。相反,新建类的时候,首先应考虑“组织”对象;这样做显得更加简单和灵活。利用对象的组 织,我们的设计可保持清爽。一旦需要用到继承,就会明显意识到这一点。

继承

单继承

集合

生命周期

异常处理