为什么在多态中使用基类和接口?

时间:2008-12-18 21:12:14

标签: language-agnostic

在多态的C#示例中,有一个Cat类,它继承了一个名为AnimalBase的类和一个名为IAnimal的接口。

相关链接是:http://en.wikipedia.org/wiki/Polymorphism_in_object-oriented_programming

我的问题是,为什么要使用基类和接口?为什么不是一个或另一个?我认为实现多态性只需要一个抽象类。

由于

7 个答案:

答案 0 :(得分:7)

如果要重用BEHAVIOR

,则使用基类

当您想要控制类与其他对象的INTERACTS时,使用接口。它以精确的方式定义了交互。

根据我的经验,您希望控制类交互方式的次数使您希望重用行为的时间相形见绌。

答案 1 :(得分:7)

“从基类继承允许继承BEHAVIOR,而实现接口只允许您指定INTERACTION”的语句绝对正确。

但更重要的是,接口允许静态类型语言继续支持多态。面向对象的纯粹主义者会坚持认为语言应该提供继承,封装,模块化和多态性,以便成为一个功能齐全的面向对象语言。在动态类型 - 或鸭类型 - 语言(如Smalltalk)中,多态性是微不足道的;然而,在静态类型语言(如Java或C#)中,多态性远非微不足道(事实上,从表面上看,它似乎与强类型的概念不一致。)

让我演示一下:

在动态类型(或鸭子类型)语言(如Smalltalk)中,所有变量都是对象的引用(没有更多,仅此而已。)因此,在Smalltalk中,我可以这样做:

|anAnimal|    
anAnimal := Pig new.
anAnimal makeNoise.

anAnimal := Cow new.
anAnimal makeNoise.

那段代码:

  1. 声明一个名为anAnimal的局部变量(请注意,我们不指定变量的TYPE - 所有变量都是对象的引用,不多也不少。)
  2. 创建名为“Pig”
  3. 的类的新实例
  4. 将Duck的新实例分配给变量anAnimal。
  5. 将信息makeNoise发送给猪。
  6. 使用奶牛重复整个事情,但将其分配给与Pig相同的确切变量。
  7. 相同的Java代码看起来像这样(假设Duck和Cow是Animal的子类:

    Animal anAnimal = new Pig();
    duck.makeNoise();
    
    anAnimal = new Cow();
    cow.makeNoise();
    

    这一切都很好,直到我们介绍类蔬菜。蔬菜与动物有一些相同的行为,但不是全部。例如,动物和蔬菜都可能生长,但显然蔬菜不会产生噪音,动物也无法收获。

    在Smalltalk中,我们可以这样写:

    |aFarmObject|
    aFarmObject := Cow new.
    aFarmObject grow.
    aFarmObject makeNoise.
    
    aFarmObject := Corn new.
    aFarmObject grow.
    aFarmObject harvest.
    

    这在Smalltalk中运行得非常好,因为它是鸭子型的(如果它像鸭子一样走路,像鸭子一样呱呱叫 - 它是一只鸭子。)在这种情况下,当一条消息被发送到一个对象时,一个查找在接收者的方法列表上执行,如果找到匹配的方法,则调用它。如果没有,则抛出某种NoSuchMethodError异常 - 但它都是在运行时完成的。

    但是在Java中,一种静态类型语言,我们可以为变量分配什么类型?玉米需要从蔬菜中继承,以支持生长,但不能从动物身上继承,因为它不会产生噪音。 Cow需要继承Animal以支持makeNoise,但不能继承VEG,因为它不应该实现收获。看起来我们需要多重继承 - 从多个类继承的能力。但事实证明这是一个非常困难的语言特性,因为弹出的所有边缘情况(当多个并行超类实现相同的方法时会发生什么?等等。)

    接下来的接口......

    如果我们制作动物和蔬菜类,每个实施Growable,我们都可以声明我们的牛是动物而我们的玉米是蔬菜。我们还可以宣称动物和蔬菜都是可以生长的。这让我们写这个来增长一切:

    List<Growable> list = new ArrayList<Growable>();
    list.add(new Cow());
    list.add(new Corn());
    list.add(new Pig());
    
    for(Growable g : list) {
       g.grow();
    }
    

    它让我们这样做,发出动物的声音:

    List<Animal> list = new ArrayList<Animal>();
    list.add(new Cow());
    list.add(new Pig());
    for(Animal a : list) {
      a.makeNoise();
    }
    

    duck-typed语言的最大优点是你获得了非常好的多态性:所有类都必须提供行为提供方法(还有其他权衡,但在讨论打字时这是最重要的。)只要每个人都玩得很好,并且只发送符合定义方法的消息,一切都很好。缺点是下面的错误类型直到运行时才被捕获:

    |aFarmObject|
    aFarmObject := Corn new.
    aFarmObject makeNoise. // No compiler error - not checked until runtime.
    

    静态类型语言提供了更好的“按合同编程”,因为它们将在编译时捕获下面的两种错误:

    Animal farmObject = new Corn();  // Compiler error: Corn cannot be cast to Animal.
    farmObject makeNoise();
    

    -

    Animal farmObject = new Cow();
    farmObject.harvest(); // Compiler error: Animal doesn't have the harvest message.
    

    所以....总结一下:

    1. 接口实现允许您指定对象可以执行的操作(交互),而类继承允许您指定应该如何完成(实现)。

    2. 接口为我们提供了许多“真正的”多态性的好处,而不会牺牲编译器类型检查。

答案 2 :(得分:3)

拥有一个抽象类可以让你以一种常见的方式实现一些/大多数成员。拥有一个接口意味着当你想将它用于多态时,你不仅限于使用该抽象基类的

我认为两者并不矛盾。

答案 3 :(得分:1)

接口使您能够跨类heirachy进行多态行为。缺点是您不能(直接)继承默认实现。使用类多态性,您只能在类heirachy中获得该多态行为,但您可以继承常见/默认行为。通过提供和提供接口,您可以提供合同(接口),但获得继承的简单实现优势,同时仍允许其他人支持基类限制之外的“合同”。

答案 4 :(得分:1)

接口强制执行“行为”。声明实现指定接口的任何类必须实现具有在接口中声明的签名的成员。即,它们必须公开地指定行为......它们不一定必须以相同的方式实现行为,但它们musrt能够有相同的行为......即鸟和蠕虫“CanMove”,所以他们都必须实现“能够”移动“的行为”,指明它们都必须强制接口ICanMove这样做......  他们如何做到这一点是实施的功能。

基类用于重用“实施”......

这就是为什么接口的命名约定建议在IEnumerable,ICanMove,ICanLog等中使用“I [Verb / Adverb]”。

您可以使用基类将常见实现放在一个位置。如果抽象基类在任何成员中都没有实现,那么它的功能与接口相同,后者无法实现

答案 5 :(得分:0)

基类和接口实际上主要是无关的目的。基类的主要目的是让继承类能够导入一些常用功能。接口的主要目的是让其他类能够提出问题,“这个对象是否支持接口X”?

答案 6 :(得分:0)

所以我想我可以继承动物类的常见行为,比如走路,睡觉等,然后有一个专门的界面(对于狮子,也许),它会包含特定的行为和属性 - 就像动物是危险的,吃人等等。