接口(interface)vs. 抽象类(abstract class)

本文内容

  • 概述
  • 设计理念
  • 应用场合
  • interface vs. abstract class
  • 修改记录

 

概述


网上大多数资料,在比较 interface 和 abstract class 区别时,往往是先从语法,然后实现(编程),最后是设计理念和应用场合。我觉得这样不妥!设计理念才决定了,它们在语法、编程和应用上的差异。

另外,作为 C# 程序员的我,开始会忘记——继承 abstract class,实现 interface 接口。为什么?编程语言决定的。因为,C# 中,不区分继承,还是实现。无论是 abstract class,还是 interface,.net 都采用相同语法。这样做可以理解,但这也会隐藏事物的本质。而 Java 区分得很清楚,继承 extends 类,实现 implement 接口。如下所示,定义抽象类 M 和接口 IM。

abstract class M
{
    abstract void Method();
}
interface IM
{
    void Method();
}

C#

class Mo : M
{
    override void Method()
    {
        //TO DO
    }
}
class Mo : IM
{
    void Method()
    {
        //TO DO
    }
}

Java

class Mo extends M {
    void Method() {
        // ……
    }
}
 
class Mo implements IM {
    public void Method() {
        // ……
    }
}

设计理念


比如,你周围的同事,张三和李四。虽然张三和李四是两个不同的人(即便他们是同卵双胞胎),但他们都是“人(人类)”吧。人类具有的“特征”和“行为”,在张三和李四身上都能看到。这样,如果我们创建一个 Human 类,让张三和李四分别继承这个类,是很合理的。

但现实世界是复杂的。像“张三是人”、“李四是人”,这样明显的“是”的关系,显然过于简单。某些事物可以具有相同的“行为”或“特性”,但这个“行为”的含义完全不同。也就是说,不存在明显的继承关系。这就是接口存在意义。

设计阶段,我们思考的开始点往往是:将“可变的部分”和“不可变的部分”区分开。可变的部分考虑用接口,而不可变的部分,考虑用抽象类。而如果对“可变的部分”采用继承抽象类,那么可能会涉及 override 抽象类的方法,特别是新增一个类时。否则,就有可能出现意想不到的结果。代码会处于不可控的状态。

abstract class 的设计体现了 "is-a" 关系;而 interface 体现了 "has-a" 关系。

设计模式,在其第一个模式——策略模式,就向我们展示了:

“如果仅仅是了代码复用而采用继承,结局往往并不完美;“有一个”比“是一个”更好。”

理解它们的设计理念仅仅是个开始,还需要在项目中不断体会。

应用场合


  • 何时使用接口

接口可以让其他开发人员实现你的接口,或是可能实现你接口的主要目的与你当初的想法完全不同。对他们来说,你的接口只是附带的,必须添加到他们的代码里,才能使用你的包(Java 是包,而在 .Net 中则是项目——程序集)。

比如,我们使用 .Net Framework 3.5+ 时,不是所有的数据结构都可以使用其 Linq 功能。若想对自己的类使用 Linq 功能,必须要继承 IEnumerable 或 IQueryable 接口。

也就是说,你若想扩展我给你东西,就必须实现必需的接口

  • 何时使用抽象类

相比之下,抽象类提供更多的结构。它通常定义一些默认的实现,并提供一些对一个完整实现很有用处的工具。美中不足的是,使用这些代码必须继承你的类。如果其他想使用你的包(.NET 项目)的开发人员已经开发了他们自己类的体系结构,这可能就极为不方便。在 Java 和 C # 中,一个只能继承一个基类(C++可以多重继承)。

也就是说,如果你有一个类,它提供了很多通用功能,只要继承你的类,就可以使用。现在其他开发人员想使用你的这些功能,但问题是他们已经有了自己的类的继承关系的体系结构。我们知道,在 Java 和 C# 中,是不允许继承多个类的,所以他们不能继承你的类,不能使用你的类,至少不能方便的使用。

  • 何时使用两者

你最好同时提供接口和抽象类。如果他们选择,实施者可以忽略你的抽象类。但缺点是,通过他们的接口名调用其方法要比通过抽象类类名调用稍微慢些。

Interfaces vs. Abstract Classes


它们看上去太像了,以至于我们有时会迷茫,是选择抽象类,还是接口。如果选错了,那也许就是噩梦。抽象类和接口都属于某种程度的抽象。

  • 抽象类中抽象的成员方法,可实现(提供默认的实现),也可不实现;
  • 而接口里不允许有实现;
  • 它们都不允许实例化。

下面比较一下抽象类和接口。

1) 多重继承

  • 接口。一个类可以实现(或者说继承)多个接口。
  • 抽象类。一个类只能继承一个抽象类。

2) 默认实现

  • 接口。一个接口不能提供任何实现的代码。
  • 抽象类。一个抽象类可以在其方法中提供完整的实现代码(默认的实现),或仅仅提供一个方法的声明,再在继承该抽象类的子类中重载(overridden)该方法。比如,在 C# 中,可以为一个抽象类中的一个方法提供默认实现,也可以用 abstract 关键字来修饰,在继承该抽象类的类中用 override 关键字重构该方法。

3) 常量

  • 接口。只有 static final 常量无需在实现该接口的类中验证就可以使用。可另一方面,这些无需验证的名字破坏了命名空间。你可以使用它们,它们来自于哪里不太明显,因为验证是可选的。
  • 抽象类。可以使用实例常量和静态常量。静态和实例初始化器代码都可以计算常量。

4) 第三方

  • 接口。一个接口的实现可以被添加到任何现存的第三方类中。只要你的类实现了第三方提供的接口,你就可以使用第三方提供的包(项目)中的特性。比如,.Net Framework 的 foreach 关键字遍历继承 System.Collections.IEnumerable 接口集合很方便。如果你自己的类,继承了该接口,那么对你自己定义的集合就可以使用 foreach 关键字。
  • 抽象类。只能从抽象类扩展,第三方类必须重写。如果你的类必须要继承抽象类,但是你的类已经有了自己的继承关系,在这种情况下,你必须重写你的类。

5) "is-a" 关系与 "has-a" 关系

  • 接口。接口通常用来描述类的外部能力,而不是它的核心身份。例如,一个汽车类可能实现一个关于回收的 Recyclable 接口,这个接口也可以应用于很多其他完全没有任何关系的对象(类)。
  • 抽象类。一个抽象类定义其后代的核心身份。如果定义了一个 Dog 抽象类,那么 Dalmatian (斑点狗)的后代也是狗,这是可行的。实现接口列举一个类可以去做的通用方法。在 Java 中,用户通常应实现 Runnable 接口,而不是继承 Thread 类,因为它们对新线程的功能不感兴趣,通常只是希望一些代码具有独立运行的能力。他们想创建可以在线程中运行的代码,而不是一个新类型的线程。当你决定是继承还是委托时,"is-a" 和 "has-a" 相似性就会出现。

6) 插件

  • 接口。你可以为接口写一个新的替换模块,该新模块不包含现存实现的任何代码。当你实现这个接口时,你就从头开始,这给了你很大的自由度去实现一个与之前完全不同的设计。
  • 抽象类。你必须继承抽象类,而无论这个抽象类是好,还是坏,你都接受。抽象类的作者把结构强加于你。你只能依赖抽象类作者的聪明程度了,也许好,也许坏。

也就是说,如果你提供一个接口,那么其他开发人员可以针对这个接口开发新的模块,实现可以与你完全不同,而且可以随意设计自己的代码,只要符合你的接口,就可以扩展你包(项目)的功能。

但是抽象类不同,如果你有一个抽象类,其他开发人员通过继承使用该类的方法,那么他们只能受限于你的结构,好的要接受,不好的也得接受。

7) 同质性(一致性)

  • 接口。如果所有不同的实现都是通过一个方法的声明,那么这个接口就会很好地工作。
  • 抽象类。如果不同的实现都是一类的,并且共享共同的状态和行为,那么通常一个抽象类会很好地工作。另一个重要的问题是“异质的(不一致的)与同质的(一致的)”。如果实现者(子类)是同质的,那么倾向于使用抽象基类。如果它们是异质的,就应该使用接口。

也就是说,如果子类与其父类,具有明显的“是一个”的关系,那么当然考虑使用抽象类最合适,否则就应该使用接口。

8) 维护性

  • 如果你的代码只是与接口或抽象类有关,那么通过工厂方法,可以很容易地改变它后面的具体实现。一个工厂方法是一种通用的构造器,用来产生多种对象,可以用静态方法现实,而不是构造函数。

9) 速度

  • 接口。接口的速度慢,需要额外的间接方式在真正的类中查找相应的方法。毕竟,当前程序运行时,接口使用的是哪个类是运行时检查的。
  • 抽象类。抽象类的速度很快。

10) 简洁性

  • 接口。在接口中声明的常量都被推定是 public static final,因此,你就可以不用管这些了。你不能调用任何方法计算常量的初始值。你无需声明一个接口的个别方法。它们都被假定的。
  • 抽象类。如果你不能把共享的代码放在接口中,那么就可以放在抽象类中。如果接口要共享代码,那么你必须写程序来安排。你可以使用的方法来计算你常量和变量的初始值,无论是实例的,还是静态的。你必须声明一个抽象类中所有的单个方法。

11) 增加功能

  • 接口。如果向接口增加一个新方法,那么,你必须查看所有实现这个接口的类,并且提供一个该方法的具体实现。
  • 抽象类。如果向抽象类增加一个新方法,那么可以提供一个默认的实现,也可以不提供。所有现存的代码无需调整可以继续工作。

 

修改记录


  • 2012-1-8   [UPDATE]
  • 2015-1-15 [UPDATE]
posted @ 2011-09-04 11:14  船长&CAP  阅读(1270)  评论(3编辑  收藏  举报
免费流量统计软件