Azige 的面向对象哲学(二) —— 泛化与特化

概述

  咦?为什么是泛化与特化的主题,那面向对象的特性中经常说到的继承去哪了?这里先要说一下,对于抽象概念来说,泛化与特化才是描述有联系的概念之间的正确术语,在使用类来描述概念的语言中,类的继承是特化的一种手段而已。

泛化

  泛化(Generalization)是指从概念中消除某些特征,以使得概念可以更宽泛的描述事物的方式。为什么先说泛化呢?因为人总是先认识了最具体的事物,然后再开始抽象的嘛。用生物学的分类方式举个例子,我是一个人,抽象化之后可以得到人类这个概念,泛化一下,可以变成灵长目,再泛化一下,可以变成脊椎动物,变成动物,最后变成最抽象的生物。这其中,任何概念用来描述我都是正确的,我是脊椎动物,也是生物。并且,随着泛化的进行,能够描述的东西也越来越宽泛。开始的时候,我们只能用“人类”来描述大部分住在钢筋混凝土里的东西,后来我们可以用“灵长目”来描述动物园里的东西了,最后我们可以用“生物”来描述整个地球上的大部分东西。

  然而,大部分时候我们并不需要将概念泛化到如此抽象的程度,只是需要提取某些概念的 共性 以专注于我们所关心的部分,例如上一章提到的钢笔与铅笔。我们对钢笔的认识是:可以注入墨水反复使用,写出的字迹不容易被擦除等等;对铅笔的认识是:削好就可以直接使用,写出的字迹可以擦除等等。但在我们仅仅是需要 写字 的时候,我们并不需要考虑某种东西能否重复利用,写出来的字质量如何,仅仅需要它是 就行了。这时,钢笔与铅笔的概念就被泛化了成了笔,纯粹的是“能用来写字的东西”。我们所关注的仅仅是笔的功能与特性,而不在乎我们拿到的东西究竟是钢笔还是铅笔。

  从 Java 的历史中我们可以找一个例子来实际说明一下。Java 最初的版本提供了与 C++ 中的同名的 Vector 作为一个用变长数组来存储对象的工具类,然而这个类它太过具体了,除了变长数组以外它不能是别的任何东西,例如链表。对于早期的代码,如果要更换存储对象的容器,将是一件非常麻烦的事,因为程序员可能不得不把代码中的 Vector 都改成另一种类型,甚至更糟糕的情况下,另一种类型可能与 Vector 有着完全不同的接口,这意味着以前使用 Vector 的每一行代码都需要修改,工作量可想而知。在 Java 1.2 中,设计者们创造了新的 Java 集合框架(JCF, Java Collection Framework),其中设计了一个崭新的类型 List。List 被定义为线性表,是一种泛用的抽象的数据结构的概念,例如数组就是一种线性表。在设计了 List 之后,设计者们又为其编写了一些常用的实现,例如 ArrayList 与 LinkedList,前者是顺序表,后者是链表。由此一来,Vector 的枷锁就被打破了,程序员能够使用线性数据结构的不仅是变长数组,而是任何的线性表。这些线性表可以是顺序式的,可以是链式的,甚至可以就是一个定长的数组。我们的新的代码也不需要区分他们究竟是什么,只需要关心 List 这个类型,只需要按照线性表的使用方式来存取数据即可。

  如果客户程序使用了泛化过的类型,那么在开发新的类型的时候会很容易不修改已有的客户代码便能嵌入新类型的对象,只要新类型不破坏原有类型的接口,并且客户程序使用了一些技巧来获得可能到来的新类型的对象(例如注入,或者是工厂方法)。这是面向对象的设计原则之一的开闭原则的体现,也是面向对象的可扩展性的基石。

特化

  特化(Realization)是指为概念增加特征,使得概念能够描述的事物更具体,范围更狭小。(关于Realization的翻译,本人没找到特别正式的词汇,可能翻译成具体化会比较好,但是为了和泛化对应,本人决定用特化这个词)与泛化正相反,泛化是从已经认识的事物中提取共性而获得能描述更多的事物的概念,而特化则是为了从现有概念中派生出一个更为具体的,能够描述未存在过的或者是新认识的事物的概念。

  在 OOP 中,特化的一个最常用的手段就是继承(Inheritance),通过扩展现有的类型派生出新的类型,新的类型会继承原有类型接口,这样客户程序可以像使用原有类型一样使用新类型,这也是在 OOP 语言中向上转型总是安全的原因。而除去继承下来的成员,新类型可以加入新的成员,甚至可以改写原有的方法的实现,这是面向对象的很重要的一个特性——多态(Polymorphism),到下一章再谈论。

  除了继承以外,还有一种可能并不会被经常提到的特化的手段,那就是组合(Composition)。关于组合,可以先说一个生活中的例子。我们都是人类,我们都是软件相关的职业,但是我们的具体职责不同,有些人负责设计原型,有些人负责开发业务,有些人负责编写测试,这些差异是怎么来的呢?尽管所有人都是 人类,但每个人所拥有的 知识 不同,因此就有了能够处理某个专门的方面的能力差异。回顾一下上一章中的 hello world 的例子,尽管所有的例子中使用的都是 PrintStream,但由于各自的对象构造所组合的另外一个 OutputStream 不同,就有了能够向各种不同的目标输出字符串的能力。组合比起继承要更灵活,这也是为什么面向对象的设计中提倡优先考虑组合而不是继承的原因,就像我们更倾向把 PrintStream 的对象和 FileOutputStream 的对象组合起来使用以向文件打印字符串,而不是再派生一个 FilePrintStream 的类型。继承是一种对所继承的类型的强依赖,并且几乎是最强的依赖,减少不必要的继承在解耦中是非常重要的。

小结

  OOP 的核心是为了提高代码的可重用性,我们通过泛化来使得代码变得容易扩展,通过特化来从现有的功能中创造出新的功能。不过泛化和特化仍旧只是思考问题的时候的方法,在实际的代码实现中还有更具体的方法,即面向对象的特性中的经常说到的封装与多态了,这个主题将在下一章中谈论。

© 2014-2020 Azige

知识共享许可协议
本站文字,除去已附加许可证的代码以外,均采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。