1.3.3 面向对象编程的3个原则
面向对象编程的3个原则分别是封装、继承和多态性。
1. 封装
封装(Encapsulation)是将程序代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。理解封装的一个方法就是把它想象成一个黑盒子,它可以阻止在外部定义的代码随意访问内部代码和数据。对黑盒子内代码和数据的访问通过一个适当定义的接口严格控制。例如,汽车上的自动传动装置中包含了有关引擎的信息,例如汽车正在以什么样的加速度前进,行驶路面的坡度如何,以及目前的档位,等等。驾驶员影响这个复杂封装的方法仅有一个:移动档位传动杆。所以档位传动杆是把驾驶员和传动连接起来的唯一接口。而且,传动对象内的任何操作都不会影响到外部对象,例如,档位传动装置不会打开车门。因为自动传动被封装起来了,所以任何一家汽车制造商都可以选择一种适合自己的方式来实现它。然而,从驾驶员的角点来看,它们的用途都是一样的。编程中的“封装”概念与此类似。封装代码的好处是每个人都知道怎么访问它,但却不必考虑它的内部实现细节,也不必担心使用不当会带来负面影响。
C#封装的基本单元是类。一个类(Class)定义了将被一个对象集共享的结构和行为(数据和代码)。一个给定类的每个对象都包含这个类定义的行为和结构,好像它们是从同一个类的模子中铸造出来似的。基于这个原因,对象有时被看做是类的实例(Instance)。所以,类是一种逻辑结构,而对象是真正存在的物理实体。
当创建一个类时,要指定组成这个类的代码和数据。从总体上讲,这些元素都被称为该类的成员(Member)。具体地说,类定义的数据称为成员变量(Member Variable)或实例变量(Instance Variable)。操作数据的代码称为成员方法(Member Method)或简称方法(Method)。如果对C/C++比较熟悉,则可以这样理解:这里所称的方法,就是C/C++程序员所称的函数(Function)(注:在本书中的方法和函数不做区分,它们是同一个意思)。在完全用C#编写的程序中,方法定义如何使用成员变量。这意味着一个类的行为和接口是通过方法来定义的,类的这些方法对它的实例数据进行操作。
既然类的目的是封装复杂性,在类的内部就应该有隐藏实现复杂性的机制。C#采用不同的关键字来设置类边界,这些关键字决定了谁能使用后续的定义内容。类的公共接口代表类的外部用户需要知道或可以知道的每件事情;私有方法和数据仅能被同一个类的成员代码所访问,其他任何不是该类的成员的代码都不能访问私有的方法或变量。既然类的私有成员仅能被程序中的其他部分通过该类的公共方法访问,那么程序员就能保证外部代码无法随意访问内部数据。当然,公共接口应该仔细设计,不要过多暴露类的内部内容。
2. 继承
就其本身来说,对象的概念可为我们带来极大的便利。它允许我们将各式各样的数据和功能封装到一起。这样便可恰当地表达“问题空间”的概念,而不用刻意遵照机器的表达方式。在程序设计语言中,这些概念则反映为具体的数据类型(使用class关键字)。
我们费尽心思做出一种数据类型后,假如不得不重新新建一种类型,令其实现大致相同的功能,那会是一件非常浪费时间的事情。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就会理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫做基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫做继承类或者子类)也会反映出这种变化。
继承(Inheritance)是一个对象获得另一个对象的属性的过程。继承很重要,因为它支持了按层分类的思想和概念。前面提到的大多数知识都可以按层级(也就是从上到下)分类管理。例如,猎犬是狗类的一部分,狗又是哺乳动物的一部分,哺乳动物类又是动物类的一部分。如果不是用层级的概念,就不得不分别定义每个动物的所有属性。使用了继承,一个对象则只需定义使它在所属类中独一无二的属性即可,因为它可以从其父类那里继承所有的通用属性。所以,可以这样说,正是继承机制使一个对象成为一个通用类的特定实例成为可能。下面将举例说明这个过程。
如果人们想以一个抽象的方式描述动物,可以通过身体大小、智力高低及骨骼系统的类型等属性进行描述。动物也具有确定的行为,它们也需要进食、呼吸、睡觉。这种对属性和行为的描述就是对动物类的定义。再考虑描述一个更具体的动物类,比如哺乳动物,它们会有更具体的属性,比如牙齿类型等。哺乳类动物是动物的子类(Subclass),而动物是哺乳动物的超类(Superclass)。
由于哺乳动物类是需要更加精确定义的动物,所以它可以从动物类继承所有的属性。一个深度继承的子类继承了类层级(Class Hierarchy)中它的每个祖先的所有属性。继承性与封装性相互作用。如果一个给定的类封装了一些属性,那么它的任何子类将具有同样的属性,而且还添加了子类自己特有的属性。这是面向对象的程序在复杂性上呈线性而非几何性增长的一个关键原因。新的子类继承其所有祖先的所有属性。它不与系统中其余的代码产生无法预料的相互作用。
3. 多态性
多态性(Polymorphism)是允许一个接口被多个同类动作使用的特性,具体使用哪个动作与应用场合有关。下面以一个后进先出型堆栈为例进行说明。
假设有一个程序,需要3种不同类型的堆栈。一个用于整数值,一个用于浮点数值,一个用于字符。尽管堆栈中存储的数据类型不同,但实现每个栈的算法是一样的。如果用一种非面向对象的语言,就要创建3个不同的堆栈程序,每个程序一个名字。但是,如果使用C#,由于它具有多态性,就可以创建一个通用的堆栈程序集,它们共享相同的名称。
多态性的概念经常被说成是“一个接口,多种方法”。这意味着可以为一组相关的动作设计一个通用的接口。
多态性允许同一个接口被位于同一类的多个动作使用,这样就降低了程序的复杂性。如何选择应用于每一种情形的特定动作(也就是方法)是编译器的任务,程序员无须手工进行选择,而只要记住和使用通用接口即可。
4. 多态性、封装性与继承性相互作用
如果使用得当,在由多态性、封装和继承共同组成的编程环境中可以写出比面向过程编程环境更健壮、扩展性更好的程序。精心设计的类层级结构是重用程序代码的基础,封装可以在不破坏依赖于类公共接口的代码的基础上对程序进行升级迁移,多态性则有助于编写清晰、易懂、易读、易修改的程序。
我们仍然采用汽车的例子帮助读者完整地理解这些概念。总的来说,汽车与程序很相似,所有的驾驶员依靠继承性很快便能掌握驾驶不同类型(子类)车辆的技术。不论是校车、私家轿车、大卡车或是家庭汽车,司机都能找到方向盘、油门和刹车装置,并知道如何操作。经过一段时间的驾驶,大多数人甚至能知道手动档与自动档之间的差别,因为他们从根本上理解了这两个档的超类——传动。
人们在汽车上看见的总是封装好的特性。刹车和踏脚板隐藏着底层的复杂性,但接口却是如此简单,驾驶员只要用脚踩就可以了。
最后来看多态性,它在汽车制造商基于相同的交通工具所提供的多种选择方面得到了充分反映。例如,刹车系统有正锁和反锁之分,方向盘有带助力或不带助力之分,引擎有4缸、6缸或8缸之分。无论设置如何,驾驶员都得脚踩刹车板来停车,转动方向盘来转向,按离合器来制动。同样的接口能被用来控制许多不同的实现过程。通过封装、继承及多态性原理,各个独立部分组成了汽车这个对象。这在计算机程序设计中也是一样的。通过面向对象原则的使用,可以把程序的各个复杂部分组合成一个一致的、健壮的、可维护的程序整体。