面向对象的核心特性包括封装、继承和多态。

封装

封装指的是把数据和与这些数据相关的行为进行绑定,并对外提供访问接口。在 C++ 和 Java 等语言有 private 关键字,可以把成员变量或成员函数隐藏起来,不允许使用者从外部进行访问。封装是一种实现“信息隐藏”的手段,它可以把数据的组织方式和行为的具体实现隐藏起来,使得外部调用者无需依赖行为的具体实现方式。例如队列类 queue ,在使用时,我们不需要知道数据是用数组储存的还是用链表储存的,也不需要知道 push_back 的具体操作是怎么做的,这就实现了行为与接口的解耦。封装还有助于进行模块化编程,只要接口定义良好,不同的类可以由不同的程序员维护,测试也可以单独进行。为了达到行为与接口的完全解耦,部分 OOP 语言引入了 private 等关键字,这样可以禁止调用者访问除了接口以外的数据和方法。

继承和多态

我理解的继承有两种。

一种是对现有类进行扩展,子类可以复用父类的代码,可以重写部分代码,增加一些成员,提供更多接口等。在这种继承的语境下,子类和父类应该视为同一级别的类,他们的关系就像普通汽车和装了车载音响的汽车。装了车载音响的汽车虽然继承自普通汽车,但两者并没有逻辑上的父子关系,他们对外提供的服务都是类似的。

第二种继承是接口继承,指的是实现了某个接口的类,即可被视为继承自该类。这种继承和多态有千丝万缕的联系,我们没法孤立地看待它们。在动态语言如 Python 中,这种接口继承又叫鸭子类型,意为“只要它会像鸭子一样叫,我就把它看作鸭子”。而在 Java 里,我们通常不说“继承”一个接口,而说“实现”一个接口,这是因为子类并没有从接口的定义中“继承”到任何代码。从集合论的角度看,如果把实例化的对象看作集合中的元素,那么类和接口都是对象的集合。一个类 A 如果实现了接口 I ,那么集合 A 就是集合 I 的子集。实际上,集合 I 是所有实现了接口 I 的类对应集合的并集。其他继承模式如 mixin 模式和 trait 模式都和接口继承模式类似,但又有细微的差别, mixin 强调代码的复用,把小的功能组件插入其他类,让其他类也具有这样的功能,典型的应用场景是对日志系统的支持, 一个日志类通过 mixin 的方式混入其他需要日志功能的类,使得它们也具有记录日志的功能。 Trait 和接口(Interface) 类似,可以自带一些默认实现,和 C++ 的 abstract class 在语法层面上差别不大。但如果你只是把它理解为 abstract class ,那么你有极大的可能会滥用它。还是应该从子类型的角度去理解和应用 Trait 。

在面向对象编程中,还有一些设计原则。

  • 开闭原则:对扩展开放,对修改封闭
  • 里式替换原则:程序中的所有用基类的地方,都可以用子类代替
  • 依赖倒转原则:依赖于抽象而不依赖于具体
  • 接口隔离原则:将大接口分散成小接口
  • 单一指责原则:一个类的功能尽量
  • 单一最小知识原则:一个对象应该尽量少的了解其他对象

用上文提到的汽车和带音响的汽车作为例子,开闭原则要求你给汽车加音响的时候,不能对原来的车进行大的改动;里式替换原则要求任何可以用汽车地方,你的带音响的车都可以开过去替代;依赖倒转原则指的是你当需要车作为交通工具的时候,应该依赖的是一个可以载人的工具,而不是某一辆没有音响的内燃机汽车;接口隔离原则指的是当你的汽车既可以是载人工具,也可以是一坨可回收的物品时,你不应该定义一个接口为可回收的载人工具,而应该把它们拆开;单一职责原则和最小知识原则比较简单,不再赘述。

了解面向对象编程思想的基本内容之后,让我们回到程序设计本身。我们引入面向对象思想到底是要解决什么问题?或者说,我们的需求是什么?

一个需求是复用,包括代码的复用和思维的复用。代码的复用可以降低开发和维护的难度,增强代码可读性。思维的复用指的是你可以很轻松地复用你以前的经验,例如你对某个接口很熟悉,所以你对这个接口的所有经验就能轻松地应用到这个接口的所有实例上。很多时候我们说一个语言或框架优雅,大都是因为它的架构、设计、行为、命名等都具有一致性,可以让你轻松地复用你的思维,这使得你只需要记忆很少的东西,便可以玩转一个复杂的框架。

另一个需求是合作,大型项目是不可能一个人写完的,那么怎样才能把大家的工作组合在一起呢,这就需要模块化编程了。在面向对象中,一个类就是可以是一个模块,不同的类可以由不同的程序员来完成。

当然还有一个需求是解耦,这个需求既是为复用的需求服务,也是为合作的需求服务。面向对象中的封装可以实现行为和接口的解耦,但这只是解耦的一种形式,在其他编程思想中有其他的解耦方式,例如函数式编程就并不强调接口和行为的解耦,它强调数据和行为的解耦,而且由于高阶函数的大量应用,你可以用简单的函数组合出复杂的行为,所以行为不需要也不应该和数据绑定在一起。

总结一下,面向对象思想只是一种方法,封装、继承和多态也并不是程序设计中要追求的终极目标,我们根本的需求是复用、合作和解耦,为了面向对象而滥用面向对象,反而会影响复用、合作和解耦,这一点是需要我们牢记在心的。