Skip to main content

面向对象编程

图中的帅哥正在面向他的对象编程......

如果用一句话解释为什么要有“面向对象编程”(Object-Oriented Programming,OOP),那就是:面向对象编程可以有效的把大程序拆分成小模块,帮我们创建一个既灵活又稳定的系统:灵活体现在可以随时添加新的功能;稳定性体现在它在添加新功能时,不需要改动已有的程序模块。

现实中需要编程解决的问题越来越复杂,规模越来越大,但是最直观的,面向过程的编程方式和函数式编程方式却并不适合把项目拆解成小模块。并不是说面向过程的方式,完全无法实现程序的模块化,而是再按照这种思路模块化程序会出现很多问题,难以扩展和管理。小程序还好,大程序基本上就不实用了。程序员最终都不得不借助面向对象的编程来解决这些问题。

程序的模块划分

采用面向过程的编程思想时,会把程序看成是一组过程或功能的集合,程序通过书写的顺序,以及条件,循环等结构控制这些功能执行的顺序。使用面向过程的编程思想设计程序时,最自然的方式是从上至下的设计程序。

比如,程序要求完成一项测试任务。在设计这个程序的时候,首先会设计程序的总体框架,可以把这个测试程序分为几个过程:采集数据、分析数据、显示数据、保存数据等。所以程序的主函数中就应该包含完成这个任务的子函数。接下来,再分别设计这几个子函数,也就是把这几个任务再进行细分。比如,数据采集可以由以下几个过程组成:打开硬件设备、设置硬件设备、从硬件设备读回数据、关闭硬件设备等。

这样,一个程序就很自然地被划分为不同层次的多个模块。程序规模越大,程序模块的数量就越多。由于不同的程序以及程序不同的部分多少会有些相似性,为了节约程序开发时间,有一些程序模块会被用在程序多个不同的部分,甚至不同的程序中。

程序规模越来越大,这是必然的趋势。而大型程序的复杂度高、代码量大,需要多人合作完成,并且很可能每个程序开发人员都只能详细了解程序的一小部分。但是,在开发过程中,一个程序员如果发现自己可以利用已有的程序代码来完成所需的功能,他基本上是会直接利用已有程序代码的。再加上,需求随时会改,每次更改需求,不可能完全从最顶层重新设计整个程序的结构,必然还要迁就以后的程序模块。

因此,一个大型程序完成后,会发现它的模块之间的调用关系已经不再是最初设计得非常规范的树状结构了。模块的层次关系可能也会变得混乱,并且模块的开发者和使用者可能都不能完全了解这个模块究竟在程序的哪些地方被用到了,是如何被使用的。

一旦某一个模块需要改动,问题就出现了。程序模块在编写好之后,开发者可能又发现它存在一些小毛病,或者对它有了新的要求需要改动它的功能。于是模块的开发者或维护者直接就按照新的要求把它改动了。但是,他们并不知道,这个模块已经被其他程序员应用在了程序的其它部分,而且使用方法并不是模块开发者或维护者所预期的那样。模块一旦被改动,程序中使用到它的其它部分也许就不能正常运行了。模块的使用者可能并不知道模块被改动过,找出程序出错的原因也要颇费一番力气。

代码和模块的重用,在大型程序中是不可避免的。大型程序开发和维护的困难,很大一部分就是这种模块使用的混乱造成的。为了解决这个问题,必须找到一种更好的模块设计和实现方式。程序中模块的接口应当非常清晰,模块的使用者应该可以轻易地了解到一个模块有哪些数据,哪些方法。对模块的使用应当有所限制,比如模块中某些方法可以被其它程序使用;而模块中将来可能会改变、或者可能会引起问题的方法则不能公开,应该禁止被其它程序使用。模块还应该能够方便地升级、优化,以及添加新的功能等等。诸如此类的一些问题,都可以使用面向对象的程序设计方法来解决。

类和对象

现实世界是由各种各样的实体所组成的。比如,屋中有一张桌子、一把椅子、一台电脑,还有我。有些实体,它们之间有很多共同点,可以被划分为同一。比如说,“人”是一个类,“阮奇桢”这个人则是人类中的一个实体。人类中的实体都有一些共同的特点,比如用两条腿走路、能说话、会思考等。

计算机软件都是为了解决现实世界中的某个问题或是对现实世界某个方面的模拟。在程序设计中,同样存在类似的情况。比如,为公司编写一个管理软件,公司里有多名员工:张三、李四、王二麻子等。这些员工都有一些共同的、需要被程序处理的特点,比如说他们每个人都会有姓名、性别、年龄等,虽然这些特性的值可能不同。

对象是属于某个类的一个实体,有时会直接被称为实例。比如,上例中每一个具体的员工(如张三),是员工类的一个对象。

属性也经常被叫做数据或者变量,是用来描述对象静态状态的。比如员工类的实例,也就是每个员工,需要有姓名、性别、年龄等属性。

方法也经常被叫做函数,用来描述对象的动作。比如员工类的实例可能会有加班,领取工资等方法。

面向对象的程序设计

面向对象的程序设计方法,是指在开发程序时以对象作为程序的基本模块,而不是如同面向过程的程序设计方法那样,以程序的功能或过程来划分模块。以对象作为程序的基本模块,可以提高程序的重用性、灵活性和扩展性。

面向对象的程序设计有三大特征:封装性、继承性和多态性。

封装(数据抽象)

封装是指,把高度相关的一组数据和方法组织在一起,形成一个相对独立的模块。外部程序只能通过严格定义的接口访问这个模块公开的数据和方法;而对于不需要与外部发生联系的数据和方法,则把它们隐藏和保护起来。这样,就避免了编程过程中,模块常常被到处滥用以至于难以维护的弊病。

比如,需要设计一个用来模拟几个小动物日常生活的程序。在设计程序时,可以把所有小动物的行为和特性抽象归为一个“动物”类。这个类包括了一些公开的属性,如年龄、产地、名字等等;还可以包含一些公开方法,即动物的行为,比如进食、移动、发声等。这个动物类中还有一些内部的属性和方法,它们只能提供类内部使用,而不能被类之外的其它程序调用。比如,让动物走几步,可以通过调用动物类的移动方法,这个方法在内部其实还调用了类的一个私有属性:“腿的数目”,而这个属性是不能够被类之外的程序直接修改或读取的。

继承

继承是指,在一个已有的类的基础上,可以生成定义更加细致的子类。子类具有原有的类(称为父类)的所有公开出来的属性和方法。除此之外,它还有一些特有的更为具体的属性和方法。这使得我们可以定义相似的类型并对其相似关系建模。

比如程序中涉及到几只小狗和几只小鸡,它们都是动物类的实例。但是,小狗们还有它们共有的一些特点。为了使程序代码更好地被重用,这些共同点也应被抽象出来,形成一个子类,“狗”类。

动物类的所有属性和方法,狗类也都具有。所以在定义狗类的时候,先声明它是动物类的一个子类,这样,狗类就立刻具备了所有动物类公开了的属性和方法。再加上狗的一些特殊属性和方法就可以了,比如狗“看家”,鸡“下蛋”等。

子类常常也被叫做派生类;父类也可以被称为基类超类。父类、父类的父类或更往上的被统称为祖先类;相对应,子类、子类的子类或更往下的被统称为子孙类或后代类。

多态(动态绑定)

多态最早也是个遗传学概念,指源自同一祖先的不同生物会表现出多种不同形态。在面向对象的程序中,多态是指同一个方法,在不同子类中具有不同的表现方式。虽然子类具有很多继承自父类的相同的方法,但它们的实现可以是不同的。在调用这些方法时,可以使用父类的名称来调用它们。这样,程序虽然调用了相同的方法,但是因为它们所属的子类不同,其表现行为也会不同。使用多态,可以让我们在应用程序中,在一定程度上忽略相似模块的区别,而以统一的方式调用它们。

比如,几个子类同样都具有一个继承自父类“动物”的方法“发声”。而不同的子类,狗和鸡的“发声”的实现代码是不相同的:狗汪汪,鸡打鸣。这样,在应用程序中需要让一组动物逐个发声时,可以把所有动物都当作是动物类的一个实例,使用相同的代码调用每个实例对应的动物类的“发声”方法。而程序运行到这里,会自动判断要处理的实例是属于“狗”子类还是属于“鸡”子类,然后分别调用狗或鸡类中“发声”方法,或发出汪汪声,或发出唧唧声。