用开放/封闭的原则理解面向对象

一般地说,高度依赖性不好。从本质上讲,软件是个复杂的东西,为了控制复杂性,有效的方法是将整体分割成几个相互独立的部分进行开发。但是,有了高度依赖性,就不能将组成程序的“零件”(类以及子程序)进行分解,一个一个的“零件”会很大,结果复杂性就很难控制。

很久以前,技术人员将计算机的机械部分称为硬件,这是计算机所有实体部件和设备的统称。与此相对应,没有实体的程序被称为软件。如今硬件和软件作为计算机关联用语已经固定下来了,但当初却是技术人员之间的俗语。

说起软件,会让人想起“柔软灵活”,但事实上,缺乏灵活性的东西有很多。程序规模小的时候,还能够简单地更改,让人觉得有灵活性。但对于那些大规模商用软件,各部分依存关系很紧密,改动一个地方就会对别的地方有影响,总是不能随心所欲地更改。

在软件开发过程中,会遇到各种各样的问题,原因归结起来主要有两个方面,一个是复杂性,一个是变化性。

软件的规模越大,各个部分之间的牵连越复杂,更改也就越难。如果软件单纯而且规模小,更改还相对容易。随着计算功能的提高,交给计算机的任务规模也越来越大。几乎所有的软件,都随着用户需求的提高而得以扩展,变得越来越复杂。

对模块扩展必须开放(Open),对修改必须封闭(Closed)。

所谓“对模块扩展必须开放”,是指模块可以扩展。比如,如果数据结构能够追加新的字段,或是能够追加新的功能,就可以称模块是开放的。某一模块会被用到什么地方,不可能完全预测。为了应对将来的需要,对于扩展必须是开放的。

所谓“对修改必须封闭”,是指某一模块被别的模块引用时的要求。必须做成这个样子:即使被引用一方的实现细节发生变化,也不会带来问题。

也就是说,即使某一模块的内部结构改变了,对外接口也应当是不变的。如果对外接口不能保持不变,模块就不能稳定使用。使用不稳定的模块,别的模块也必须时常跟着改变,软件的复杂性和维护成本都要增加。

面向对象的情况

既要开放,又要封闭,这看起来互相矛盾。但是面向对象编程语言能够很彻底地消除这个矛盾。 可以用多态完美实现。

使用面向对象语言,功能使用方可以不必知道功能提供方各种类的详细内容,而只需要着眼于它们具有什么样的接口就可以。功能扩展以后,由于多态,扩展后的功能能够自动使用,使用方只要知道接口就行了。功能提供方提供了新功能,或是内部有了更改,功能使用方都不用做任何更改。也就是说,从功能的使用方来 看,模块对于修改是封闭的。

另一方面,功能的提供方只要生成与既存对象具有相同接口的对象,任何时候都能追加新的功能。也就是说,对于功能扩展而言是开放的。这种情况下的接口,对于静态语言来说,就是相同的类型;而对于动态语言来说,就是有没有相同的方法(换个说法就是 Duck Typing)。 很多面向对象语言,通过使用继承,只要添加一个子类,就可以往模块(类)里随时追加功能。因为有继承而允许功能的追加(对功能扩展而言是开放的),因为有多态而维持模块接口的稳定性(对修改而言是封闭的),开放和封闭能同时实现。

从实用主义的观点看,面向对象的精髓就在于对 OCP 的实践。至于把对象看做物体理解起来比较容易,能够建立现实世界的模型等,这些都只不过是些锦上添花的东西。

“数据与函数没有一体化,所以不是面向对象”,“封装得不充分,所以不是面向对象”等,世上有不少人作出类似这样的判断。道理上也许的确是这样,但 面向对象无非是编程的一种工具,是不是理论上正确的面向对象不重要,是否符合OCP,生产性高不高,维护性好不好,能否适应将来的更改,等等,这些才是重点。

世上很多设计模式,为了能应对将来可能的修改,都是按照 OCP 的要求来设计的。

人使用语言进行思考。没有语言的系统也就无法思考,不使用语言,也不能与其他人进行交流。人类的生存与人际关系都是跟语言密切相关的。