从零学Java设计模式
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 OOP与APIE

在上一节中,我们讨论了用高级语言编写的程序代码最终会转换成CPU能够处理的机器指令。我们为什么要用高级语言来编写程序呢?因为高级语言提供了一套框架,你只需要遵照这种语言的标准来编写代码就能把自己的意思表达出来。这样的语言,通常都提供许多巧妙的结构或语句,让你尽情发挥想象力,将自己想要实现的功能写出来。如果某种语言是面向对象编程(Object-Oriented Programming,OOP)语言,那么你就需要用对象(object)这一核心概念来表达你的意思。本书专注于Java语言。Java是一门完全面向对象的语言,它不仅符合OOP语言的基本要求,而且还提供其他一些特性。面向对象语言究竟意味着什么呢?在计算机领域,这意味着用这种语言编写程序的时候,开发者需要专注于类这个概念,其中某个类的实例称为该类的对象。接下来,我们会再次强调OOP范式的重要性,并讲解OOP的一些基本概念。

这些基本概念可以浓缩成APIE这个词,这四个字母分别是Abstraction(抽象)、Polymorphism(多态)、Inheritance(继承)与Encapsulation(封装)的首字母。抽象、多态、继承与封装是OOP语言的四个基本支柱。下面我们将分别用一小节来讲解这四个概念,但我们反过来讲,也就是按照EIPA的顺序,先讲封装,然后讲继承,接下来讲多态,最后讲抽象。这样安排是为了让大家更清晰地理解OOP。

1.3.1 封装——只公布那些必须公布的信息

按照反向次序,我们首先要讲的是封装。OOP语言(也包括Java语言),要用类这个概念把程序搭建起来。某一个类就好比某一种车。类中的字段在我们创建了该类的一个实例(或者说,在计算机内存中分配了该类的一个实例)之后就可以使用了。定义在类中的方法也是如此。有了该类的某个实例,我们就能在这个实例上调用该类所定义的方法,某个类定义了某个方法正如某种车辆具备某项功能。这些方法能够操纵对象的字段,以改变该字段的值。如果用车辆来打比方,那就是车辆的功能可以改变这辆车的内部状态(参见范例1.1)。

范例1.1 用Vehicle类隐藏车辆的内部状态(moving)

我们以车辆为例来理解封装这一概念。在这辆车里面,所有的内部元件与内部功能都不为驾驶者所知。这辆车只把它必须提供的部件与功能(例如,方向盘)公布给驾驶者,令其能够通过这些部件与功能来控制这辆车。这就是封装的一般原则。我们只把必须让用户知道的方法或字段公布出来,令其能够通过这些方法与字段修改或更新该实例的状态,除此之外的所有内容都不让外界知道。例如,对象内部的数组就不应该公布给外界,你应该做的是提供一些方法,让用户通过这些方法修改此数组。这个问题我们将在后面再讲,这里只是先提一下。

1.3.2 继承——在应该创造新类的时候创造

在上一小节中,我们假想出了一种车,这种车把不需要让驾驶者知道的东西全都封装了起来。这意味着驾驶者只需要开车就行了,不用管发动机是怎么运转的。

本小节要讲的是继承,这个概念将在下一个范例中演示。这里我们先假设这辆车的发动机坏了,那么怎样换发动机呢?我们的目标是用一个能够运转的发动机来替换目前这个已经坏掉的发动机。新的发动机跟现在这个发动机的运转方式未必完全相同,因为目前这种车型的某些部件市面上可能已经买不到了,所以,你很难找到一个跟现在这个发动机的运作方式一模一样的发动机。

为了描述新的发动机,我们可以套用现在这个发动机的各项属性与功能,而不必重新定义什么是发动机。放在类的语境中,这意味着,我们应该在类体系中新建一个子类,让这个子类继承现有的发动机类,以表示新的发动机。

新的发动机跟原来那个未必一模一样,而且这两个对象的标识符也不相同,但原来那个发动机所具备的属性,新的发动机同样具备(或者说,这个新发动机类继承了原发动机类的各项属性)。

这就是OOP的第二个基本概念——继承。它让我们能够在现有的某个类下衍生一个子类,以突出这种子类的特性,或者反过来说,让我们能够在现有的某些类上提取一个超类,以概括这些类的共性。另外,我们在设计软件的时候必须注意,不要让子类去依赖超类的实现细节,否则会破坏刚才讲的OOP第四支柱——封装。

1.3.3 多态——根据需要表现出不同的行为

按照EIPA的顺序,我们要讲的第三个概念是多态[1]。多态可以理解为多种形态。那什么是多种形态呢?

以上一小节的车辆为例,这就好比某些功能可以用多种方式来执行。具体到Vehicle类,我们可以说它的move方法能够根据用户的输入或者该实例的状态表现出不同的行为。

Java有两种多态,这两种多态的意思是不一样的,下面就来详细解释。

1.3.3.1 方法重载

这种多态叫作静态多态(static polymorphism)。这意味着,程序在编译的时候,从多个同名方法中把正确的那个方法选出来,由于方法判定发生在编译期(compile time,也称为编译时),所以称作静态多态。Java中的静态多态(也就是方法重载,method overloading)有以下两种实现方式:

❍在参数个数相同的前提下按照参数的类型重载,如图1.4所示。

图1.4 通过改变参数的类型来重载Vehicle类的move方法

❍按照参数的个数重载,如图1.5所示。

图1.5 通过变更参数的个数来重载Vehicle类的move方法

下面来看另一种多态。

1.3.3.2 方法覆写

这种多态叫作动态多态(dynamic polymorphism)。这意味着程序究竟执行一组同名方法中的哪一个方法,要到运行时(runtime)再决定。受到覆写的方法需要在指向某个对象(或者说,某个子类实例)的引用上调用,而这个引用,通常声明为超类类型。下面举个简单的例子来演示这一点。假设我们把Vehicle类当作超类使用(参见图1.6与范例1.2),这个超类里已经有了名叫move的方法。

图1.6 子类覆写超类中的同名方法

现在,我们给Vehicle创建一个子类,叫作Car,并且让Car中也有一个叫作move的方法。子类的move方法的行为与超类中的同名方法稍有区别,因为子类的对象都是Car实例,而不是一般的Vehicle实例,这种实例的移动速度比Vehicle实例快一些。

范例1.2 Vehicle型的vehicle变量引用的是个Car类的实例,程序执行时调用的是该实例所属类的move方法(参见图1.6)

我们将在第3章中再详细讲解这个话题。

1.3.4 抽象——从细节中提取一套标准功能

现在我们来讲OOP的最后一个基础概念,也就是抽象(Abstraction),这个概念在OOP的四大支柱之中排在首位,它是APIE中的A。抽象就是把一批对象的具体细节去掉,让这些对象的共性(或者说,让这些对象都应该支持的那一套通用功能)浮现出来。

为了让大家理解这个概念,我们还是以车辆为例。我们并不想刚一开始就直接描述某种具体的车,而是想先把所有车辆在我们关注的范围内都应该支持的一套功能[例如,移动(move)、停止(stop)等]给定义出来。明确了这套功能,我们就可以创建出适当的抽象(例如,一个抽象类),稍后再让某种具体的车辆继承这个类(参见范例1.3)。

这样设计使我们能够暂时抛开各种车辆的差异,把重点放在所有车辆都应支持的通用功能上。而且,这么做还能减少代码,并让这些代码可复用。

Java中的抽象可以通过以下两种方式实现:

❍用带有抽象方法的抽象类来实现(参见范例1.3和图1.7)。

图1.7 用AbstractVehicle这个抽象类表示车辆的共性,并让CommonCar与SportCar实现该类

范例1.3 在抽象类中描述共有的功能,但不对这些功能做特定的实现

❍用接口来实现,接口中的方法相当于抽象方法(参见范例1.4和图1.8)。

图1.8 用接口做抽象

范例1.4 用Java接口进行相似的功能提取

这两种抽象概念实现方式也可以结合着使用,如图1.9所示。

图1.9 把两种抽象概念实现方式结合起来

抽象类与接口在设计代码结构的时候都有着各自的意义。具体怎么用,要看你的需求。总之,这两种办法都能让代码变得更容易维护,并且让你能够更为流畅地运行设计模式。

1.3.5 把抽象、多态、继承、封装这四个概念贯穿起来

前面几小节提到的每一种概念都是为了让代码的结构变得更好。这些概念有各自的作用,彼此之间又互为补充。我们现在以一种事物为例来介绍如何将这些概念贯穿起来,这种事物指的是表示车辆的Vehicle类及其实例。我们会把实例中的逻辑与数据封装起来,并通过方法公布给外界。我们把所有车辆都应具备的共性提取到抽象类或接口之中,这样,在设计一款新车时,就可以继承或实现已有的抽象类或接口,而不用从头开始写。公布给外界的方法,其行为可以通过多态技术予以定制,使得每一种具体的车辆,都能表现出与该车辆相对应的特殊行为。另外,方法还可以提供参数,让用户通过传入不同的参数值来调整实例的行为或修改实例的内部状态。我们构想一种新车的时候,总是可以先考虑一下这种车与目前的这些车之间还有哪些共性,并把这些共同的行为提取到抽象类或接口之中。

下面我们用开发Vehicle类体系的过程将刚才说的意思演示一遍,如图1.10所示。每次定义新车模型的时候,我们总是应该先想一想它跟现有的车有什么共同的特征,并把这些特征提取到抽象类或接口之中。

图1.10 将APIE视为一个持续改进的过程

虽然这四个概念看起来似乎很简单,但要想严格遵照这些概念来设计软件却相当困难。

目前,我们已经知道了OOP的四个基本支柱,以及怎样运用这些理念来设计代码。接下来,我们将讲解几个与可持续代码设计有关的概念。