关于抽象类和接口的思考

抽象类和接口的定义以及异同网上已经有非常详尽的讲解了,这里我不再赘述,而是尝试从另一种角度来谈谈我的理解。

我们都知道面向对象语言有三大特点:封装、继承和多态。有的人会把抽象当作其第四个特点,对此我个人并不是特别认同。

因为在我看来,抽象是一个更宽泛的概念,SICP 中对抽象有一段非常精彩的描述:

  1. 将若干简单认识组合为一个复合的认识,由此产生出各种复杂的认识
  2. 将两个认识放在一起对照,不管他们如何简单或者复杂,都不能将其合而为一,由此得到有关他们的相互关系的认识
  3. 将有关认识与那些在实际中和它们同在的所有其它认识隔离开,这就是抽象

这个定义初看比较晦涩难懂,我目前也未能得其精髓,只能抛砖引玉谈谈自己的理解。

所谓简单的认识就是当前对于我们来说理所当然的认识,比如我们理所当然的知道这个四条腿走路,身上长毛的是狗。

复杂的认识是我们当前所不能立即理解的认识,但是却可以通过组合来理解,比如对我来说,德牧,拉布拉多等是我所知道的狗的品种,但是显然我并不知道世界上所有的品种,但是只要我通过简单的组合(将某类狗的长相特点和狗这个概念组合)就能立即产生比狗这个概念更加复杂的新的概念,即另一种品种的狗。

虽然我已经认识了很多不同品种的狗,但是我发现并不是所有这种四条腿走路,身上长毛的是狗,还有种动物叫做猫,不管是猫本身这个概念(简单)或是不同品种的猫(复杂)都和我之前所认识的狗之间有较明显的区别,它们不能归为一类。

除了猫之外,还有其他很多不能和狗归为一类的动物存在,由此我们得出能够区分出狗的更加通用的概念,比如四条腿,会汪汪叫等,也就是狗这个抽象。

值得注意的是,抽象最终还会变成简单认识,从而周而复始的产生更上层的抽象。这是人类心智活动的特点,当然也包括编程活动。

编程本身就是对现实世界抽象的体现,所以我才在文章开始时提到并不赞同将抽象当作面向对象的一大特性,因为所有语言都可抽象。(但是面向对象语言相比过程式语言更容易做抽象倒是真的)

讲了这么多铺垫,终于要到我们今天的主角们登场了。

抽象类,是对一群类的抽象,表示这一群类都属于这个抽象类;

接口,表示具有某些特质,也是对一群类的抽象,不同的是,它表示这群类都拥有这些特质。

举个例子吧:

abstract class Animal {
	abstract breath(): void;
}
interface Runnable {
	run(): void
}
class Dog extends Animal implements Runnable {
	breath() {}
	run() {}
}
class Cat extends Animal implements Runnable {
	breath() {}
	run() {}
}

Animal 是一个抽象类,代表动物这个种属, 包含一个抽象方法 breath 表示动物都可以呼吸;
Runnable 是一个接口,描述可以奔跑的这个特质;
Dog 和 Cat 都继承了 Animal 这个抽象类,表示 Dog 和 Cat 都属于动物这个种类;
Dog 和 Cat 都实现了 Runnable 这个接口,表示 Dog 和 Cat 都拥有奔跑这个特质;

虽然我已经尽可能的区分了 Animal 和 Runnable 的含义,但是有些人可能还是很疑惑,因为 Animal 既然包含了 breath 方法,是不是也表明 Animal 描述可以呼吸这个特质?从这点来看,抽象类和接口岂不是差不多?

有这种疑问很正常,因为它们确实都是在干同样的一件事:抽象。他们都是对 Dog 和 Cat 做抽象,只是抽象的角度不同而已,两者之间 有些类似却也正常,但是我们主要关注的还是它们抽象的角度而非结果,这决定了我们如何才能更好的使用它,虽然条条大路通罗马,但如果选择走陆路,使用船作为交通工具实非明智之举。

说到抽象角度,我们还得从最原始的类说起。

我们都知道类包含属性和方法,属性是构成类的最基本元素,方法是类提供的操作这些属性的接口。类的属性和对外的方法是决定这个类和其他类的本质区别(说了好像等于没说)。

抽象类也是类的一种,是为了解决代码复用的问题,Dog 和 Cat 对于其相同的部分可以在抽象类 Animal 中定义,无需在每个子类中都实现一遍,此处使用普通的类当作父类理论上也是可以的,但是抽象类有工程学上的优势,比如自动检测抽象方法是否实现,以及避免实例化的误用等。

接口是对不同对象的相同特质的描述,当然一般来说这个特质只能是方法而不能是属性,这也是和抽象类最大的不同,但是大家有没有想过为什么接口不能包含对属性的描述呢?其实我们可以反向思考:如果包含了会怎么样?如果我们的 Runnable 接口除了有 run 的能力特质之外,还要有 speed 这个属性特质会怎么样呢?这样的话我们的 Dog 和 Cat 在实现这个接口时就必须定义 speed 属性,并且必须要公开该属性,我们知道一旦公开属性,就可以对该属性进行读写,这很可能不是 Dog 和 Cat 所希望提供的能力,如果 Dog 不希望外部对其 speed 进行更改,或是想通过其他属性来计算 speed 将无计可施。而方法就不同了,实现类拥有全方位定制这个方法的能力;还有一点就是,方法完全可以替代属性,只要定义对应的 get/set 方法即可。因此在接口中添加属性一是没有必要,二是对实现类的侵入性更强,不方便解耦。

在面向对象设计中,有两种方式,自底向上和自顶向下。

抽象类本质还是父类,更多解决的是抽象中的复用问题,是一个自底向上的设计思路,先有子类,然后找出共同点抽象出父类。

接口相对类来说是一个全新的概念,其解决的是行为模式抽象问题,是一种自顶向下的设计思路,先有接口,然后扩展出不同的实现类。

我们在设计的最初阶段一般会选择自顶向下的设计思路,优先定义接口,而在设计的过程中会对一些类抽象出抽象类来解决复用问题。