面向接口编程

面向接口编程和面向对象编程的关系

面向接口编程和面向对象编程并不是平级的,面向接口编程并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其中的一部分。或者说,它是面向对象编程体系中的思想精髓之一。

面向接口编程综述

面向接口编程是在系统分析和架构设计中,分清层次和依赖关系,每个层级不直接向其上层提供服务(即不是直接实例化在上层中),而是通过定义一组接口,仅向上层暴露其接口功能,上层对于下层仅仅是接口依赖,而不依赖具体类。

这样做的好处是显而易见的,首先对系统灵活性大有好处。当下层需要改变时,只要接口及接口功能不变,则上层不用做任何修改。甚至可以在不改动上层代码时将下层整个替换掉。比如我们将一个 WD 的 60G 硬盘换成一个希捷的 160G 的硬盘,计算机其他地方不用做任何改动,只需要将原硬盘拔下来,新硬盘插上就行了,因为计算机其他部分不依赖具体硬盘,而只依赖一个 IDE 接口,只要硬盘实现了这个接口,就可以替换上去。从这里看,程序中的接口和现实中的接口极为相似。

使用接口的另一个好处就是不同部件或层次的开发人员可以并行开工,就像造硬盘的不用等造 CPU 的,也不用等造显示器的,只要接口一致,设计合理,完全可以并行进行开发,从而提高效率。

关于抽象类和接口

如果从具体代码来看,对这两个概念很容易模糊。而单从具体功能来看,甚至觉得接口就是多余的,因为除多重继承外,抽象类似乎能完全取代接口。

但接口的存在当然不只是为了实现多重继承。抽象类和接口的区别在于使用动机。使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。

Person 这个接口该不该定义,关键看具体应用中的使用情况。如果程序中有 Women 和 Man,都继承 Person,而且 Women 和 Man 绝大多数方法都相同,只有一个方法 ToWC() 不同,那么定义一个 AbstractPerson 抽象类比较合理,因为它可以把其他所有方法都包含进去,子类只定义 ToWC(),大大减少了重复代码量。

但是,如果程序中的 Women 和 Man 两个类基本没有共同代码,而且有一个 PersonHandle 类需要实例化他们,并且不希望知道他们是男是女,而只需把他们当作人看待,并实现多态,那么定义成接口就有必要了。

总而言之,接口与抽象类的区别主要在于使用的动机,而不在于其本身。而一个东西该定义成抽象类还是接口,要根据具体环境的上下文决定。

面向接口编程的代码优化示例

我们要实现老师教儿童背古诗的功能,代码如下:

 1// 老师 
 2class Teacher {
 3    public void teach(Children children){} // 教儿童背古诗
 4}
 5
 6// 儿童
 7class Children {
 8    public void listen(){} // 听课 
 9    public void recite(){} // 背诵 
10}
11
12/* 这是标准的面向对象编程,描述了对象间的关联 */

随着老师能力的提升,准备扩展服务对象,增加在校学生,代码如下:

 1// 老师 
 2class Teacher {
 3    public void teach(Children children){} // 教儿童背古诗
 4    public void teach(Students students){} // 教学生背古诗
 5}
 6
 7// 儿童
 8class Children {
 9    public void listen(){} // 听课 
10    public void recite(){} // 背诵 
11}
12
13// 学生
14class Students {
15    public void listen(){} // 听课
16    public void recite(){} // 朗读
17}
18
19/* 这同样是标准的面向对象编程,但不具备扩展性 */

继续扩展服务对象,增加外国人,这下老师又要研究新的业务?不如一步到位,提前掌握更宽泛的技能,代码如下:

 1// 老师 
 2class Teacher {
 3    public void teach(Person person){} // 教人背古诗
 4}
 5
 6// 抽象类 人
 7abstract class Person {
 8    public abstract void listen(); // 听课
 9    public abstract void recite(); // 朗读
10}
11
12// 儿童
13class Children extends Person {
14    public void listen(){} // 听课 
15    public void recite(){} // 背诵 
16}
17
18// 学生
19class Students extends Person {
20    public void listen(){} // 听课
21    public void recite(){} // 朗读
22}
23
24// 外国人
25class Foreigner extends Person {
26    public void listen(){} // 听课
27    public void recite(){} // 朗读
28}
29
30/* 这对面向对象编程进行了更深入的应用,具备了一定的扩展性,称为面向基类编程,通常基类会被声明为抽象类,所以又称面向抽象类编程 */

按照之前的经验,估计业务又要扩展了,不过即使再出现新的服务对象类型,只要是人,老师都可以应付。

但是这次新的服务对象是鹦鹉,现有的技能不匹配了。如果继续加宽服务对象范围为动物,很多动物又没办法服务,那怎么称呼这些服务对象呢?算了,不管这些对象是什么,我只需要考虑他们能干什么就好了。于是代码变成了这样:

 1// 老师 
 2class Teacher {
 3    public void teach(ListenAndReciteAble lar){} // 教背古诗
 4}
 5
 6// 接口 能听会读的
 7public interface ListenAndReciteAble{
 8    void listen(); // 听课
 9    void recite(); // 朗读
10}
11
12// 抽象类 人
13abstract class Person implements ListenAndReciteAble {
14    public abstract void listen(); // 听课
15    public abstract void recite(); // 朗读
16}
17
18// 儿童
19class Children extends Person {
20    public void listen(){} // 听课 
21    public void recite(){} // 背诵 
22}
23
24// 学生
25class Students extends Person {
26    public void listen(){} // 听课
27    public void recite(){} // 朗读
28}
29
30// 外国人
31class Foreigner extends Person {
32    public void listen(){} // 听课
33    public void recite(){} // 朗读
34}
35
36// 鹦鹉
37class Parrot implements ListenAndReciteAble {
38    public void listen(){} // 听课
39    public void recite(){} // 朗读
40}
41
42/* 要让一个依赖方达到最大的兼容性或扩展性,就要让依赖的对象类型尽量宽泛,当然 Object 是最宽泛的,但是这就不明确了,而且不能保证依赖方的需要。这时就要跳出一些定式,我们需要的是种类?还是他们会的功能?这就变成了面向接口编程 */

参考 & 扩展