接口与基类

时间:2008-09-11 15:20:31

标签: oop interface language-agnostic base-class static-typing

我应该何时使用接口,何时应该使用基类?

如果我不想实际定义方法的基本实现,它应该始终是一个接口吗?

如果我有狗和猫类。为什么我要实现IPet而不是PetBase?我可以理解有ISheds或IBarks(IMakesNoise?)的接口,因为那些可以基于宠物放在宠物上,但我不明白哪个用于通用宠物。

39 个答案:

答案 0 :(得分:481)

让我们以你的Dog和Cat类为例,让我们用C#来说明:

狗和猫都是动物,特别是四足动物的哺乳动物(动物过于笼统)。让我们假设你有两个抽象类Mammal:

public abstract class Mammal

此基类可能具有默认方法,例如:

  • 饲料
  • 伴侣

所有这些都是在两个物种之间具有或多或少相同实施的行为。要定义它,您将拥有:

public class Dog : Mammal
public class Cat : Mammal

现在让我们假设还有其他哺乳动物,我们通常会在动物园看到它们:

public class Giraffe : Mammal
public class Rhinoceros : Mammal
public class Hippopotamus : Mammal

这仍然有效,因为功能Feed()Mate()的核心仍然是相同的。

然而,长颈鹿,犀牛和河马并不是你可以养宠物的动物。这就是界面有用的地方:

public interface IPettable
{
    IList<Trick> Tricks{get; set;}
    void Bathe();
    void Train(Trick t);
}

上述合同的实施在猫与狗之间是不一样的;将他们的实现放在一个抽象类中继承将是一个坏主意。

您的狗和猫的定义现在应该如下:

public class Dog : Mammal, IPettable
public class Cat : Mammal, IPettable

理论上你可以从更高的基类覆盖它们,但实际上一个接口允许你只需要在类中添加你需要的东西而不需要继承。

因此,因为您通常只能从一个抽象类继承(在大多数静态类型的OO语言中,例外包括C ++)但能够实现多个接口,它允许您在严格的根据要求基础。

答案 1 :(得分:139)

嗯,Josh Bloch在Effective Java 2d中说自己:

首选接口而不是抽象类

一些要点:

  
      
  • 现有的课程可以轻松改装以实施新课程   接口即可。你所要做的就是添加   如果他们还没有必要的方法   存在并添加一个implements子句   类声明。

  •   
  • 接口非常适合定义mixins 。松散地说,一个   mixin是一个类可以的类型   实施除了“主要的   输入“声明它提供   一些可选行为。例如,   Comparable是一个mixin接口   允许类声明它的   实例是按照命令排序的   其他相互比较的对象。

  •   
  • 接口允许构造非分层类型   框架即可。类型层次结构是   非常适合组织一些事情,但是   其他的东西不会整齐地落入一个   严格的等级。

  •   
  • 接口可通过以下方式启用安全,强大的功能增强   包装成语。如果你使用   你定义类型的抽象类   离开想要添加的程序员   功能无可奈何   使用继承。

  •   
     

此外,你可以结合美德   接口和抽象类的   提供抽象的骨架   每个实现类   您导出的非平凡界面。

另一方面,接口很难发展。如果向接口添加方法,它将破坏它的所有实现。

PS:买这本书。它更加详细。

答案 2 :(得分:110)

现代风格是定义IPet PetBase。

接口的优点是其他代码可以使用它而与其他可执行代码无任何关系。完全“干净”。接口也可以混合使用。

但是基类对于简单实现和常用实用程序很有用。因此,提供一个抽象基类以节省时间和代码。

答案 3 :(得分:103)

接口和基类代表两种不同形式的关系。

继承(基类)表示“is-a”关系。例如。狗或猫“是一只”宠物。此关系始终表示类的(单个)目的(与"single responsibility principle"一起使用)。

另一方面,

接口代表了类的附加功能。我称之为“is”关系,就像“Foo是一次性的”,因此C#中的IDisposable接口。

答案 4 :(得分:61)

接口

  • 定义2个模块之间的合同。不能有任何实施。
  • 大多数语言允许您实现多个接口
  • 修改界面是一个重大变化。所有实现都需要重新编译/修改。
  • 所有成员都是公开的。实现必须实现所有成员。
  • 接口有助于解耦。您可以使用模拟框架来模拟界面背后的任何内容
  • 接口通常表示一种行为
  • 接口实现彼此分离/隔离

基类

  • 允许您添加一些默认实现,您可以通过派生免费获得
  • 除C ++外,您只能从一个类派生。即使可以来自多个班级,通常也是一个坏主意。
  • 更改基类相对容易。衍生品不需要做任何特别的事情
  • 基类可以声明可以通过派生访问的受保护和公共函数
  • 抽象基类不能像接口
  • 那样容易模拟
  • 基类通常表示类型层次结构(IS A)
  • 类派生可能依赖于某些基本行为(具有对父实现的复杂知识)。如果你改变一个人的基础实现并打破其他人,事情就会变得混乱。

答案 5 :(得分:57)

通常,您应该优先于抽象类的接口。使用抽象类的一个原因是,如果您在具体类中有共同的实现。当然,您仍然应该声明一个接口(IPet)并使用一个抽象类(PetBase)实现该接口。使用小的,不同的接口,您可以使用倍数来进一步提高灵活性。接口允许跨边界的类型具有最大的灵活性和可移植性。跨越边界传递引用时,始终传递接口而不是具体类型。这允许接收端确定具体实现并提供最大的灵活性。当以TDD / BDD方式编程时,这是绝对正确的。

“四人帮”在他们的书中指出“因为继承将子类暴露给其父类实现的细节,所以通常会说'继承会破坏封装”。我相信这是真的。

答案 6 :(得分:48)

这非常适合.NET,但“框架设计指南”一书认为,通用类在不断发展的框架中提供了更大的灵活性。一旦发布了接口,您就没有机会在不破坏使用该接口的代码的情况下进行更改。但是,对于类,您可以修改它,而不是破坏链接到它的代码。只要您做出正确的修改(包括添加新功能),您就可以扩展和改进代码。

Krzysztof Cwalina在第81页说:

  

在.NET Framework的三个版本的过程中,我与我们团队中的不少开发人员讨论了这个指南。他们中的许多人,包括那些最初不同意这些指南的人,都表示他们后悔将一些API作为接口发布。我甚至没有听说过有人因为他们发了一堂课而感到遗憾。

话虽如此,肯定有接口的地方。作为一般指导,总是提供接口的抽象基类实现,如果没有别的,作为实现接口的方式的示例。在最好的情况下,基类将节省大量的工作。

答案 7 :(得分:19)

涓,

我喜欢将接口视为表征类的一种方式。一个特定的狗品种,比如YorkshireTerrier,可能是父狗类的后代,但它也实现了IFurry,IStubby和IYippieDog。所以这个类定义了类是什么,但接口告诉我们它的内容。

这样做的好处是它允许我收集所有的IYippieDog并将它们扔进我的Ocean系列。所以现在我可以跨越一组特定的对象,找到符合我正在查看的标准的对象,而不会过于仔细地检查课程。

我发现接口确实应该定义一个类的公共行为的子集。如果它定义了所有实现的类的所有公共行为,那么它通常不需要存在。他们没有告诉我任何有用的东西。

这个想法虽然与每个类都应该有一个接口并且你应该编写接口的想法背道而驰。这很好,但是你最终会遇到很多一对一的接口,这会让事情变得混乱。我知道这个想法是它不需要做任何事情,现在你可以轻松地交换内容。但是,我发现我很少这样做。大多数时候我只修改现有的类,并且如果该类的公共接口需要更改,我总是会遇到完全相同的问题,除了我现在必须在两个地方更改它。

所以,如果你像我一样认为你肯定会说Cat和Dog是IPettable。这是一种与它们相匹配的特征。

另一部分虽然它们应该具有相同的基类吗?问题是他们需要被广泛地视为同一件事。当然它们都是动物,但这是否符合我们将它们一起使用的方式。

假设我想收集所有Animal类并将它们放入我的Ark容器中。

或者他们需要成为哺乳动物吗?也许我们需要某种跨动物挤奶工厂?

他们甚至需要联系在一起吗?只知道它们都是IPettable就足够了吗?

当我真正需要一个班级时,我常常觉得有必要得出一个完整的类层次结构。我希望总有一天我可能需要它,通常我永远不会这样做。即使我这样做,我也常常发现我必须做很多事来解决它。那是因为我创作的第一堂课不是狗,我不是那么幸运,而是Platypus。现在我的整个类层次结构基于奇怪的情况,我有很多浪费的代码。

您可能还会发现,并非所有Cats都是IPettable(就像那个无毛的那样)。现在,您可以将该接口移动到适合的所有派生类。你会发现一个不那么突破的变化,突然之间的猫不再来自PettableBase。

答案 8 :(得分:16)

以下是接口和基类的基本和简单定义:

  • 基类=对象继承。
  • Interface = functional inheritance。

欢呼声

答案 9 :(得分:12)

我建议尽可能使用合成而不是继承。使用接口但使用成员对象进行基本实现。这样,您可以定义一个工厂,构造您的对象以某种方式运行。如果要更改行为,则创建一个新的工厂方法(或抽象工厂),以创建不同类型的子对象。

在某些情况下,如果在辅助对象中定义了所有可变行为,您可能会发现主要对象根本不需要接口。

因此,您最终可能会使用具有IFurBehavior参数的Pet,而不是IPet或PetBase。 IFurBehavior参数由PetFactory的CreateDog()方法设置。这个参数是为shed()方法调用的。

如果你这样做,你会发现你的代码更灵活,你的大多数简单对象都处理非常基本的系统行为。

我甚至在多重继承语言中推荐这种模式。

答案 10 :(得分:12)

在此Java World article

中详细说明

我个人倾向于使用接口来定义接口 - 即系统设计的一部分,用于指定应该如何访问某些内容。

我将有一个实现1个或更多接口的类。

抽象类我用作其他东西的基础。

以下是上述文章JavaWorld.com article, author Tony Sintes, 04/20/01

的摘录
  

接口与抽象类

     

选择接口和抽象类不是一个/或命题。如果您需要更改设计,请将其设为界面。但是,您可能具有提供某些默认行为的抽象类。抽象类是应用程序框架内的优秀候选者。

     

抽象类可以让你定义一些行为;他们强迫你的子类提供其他人。例如,如果您有应用程序框架,则抽象类可以提供默认服务,例如事件和消息处理。这些服务允许您的应用程序插入您的应用程序框架。但是,有一些特定于应用程序的功能,只有您的应用程序才能执行。此类功能可能包括启动和关闭任务,这些任务通常取决于应用程序。因此,抽象基类可以声明抽象关闭和启动方法,而不是尝试自己定义该行为。基类知道它需要那些方法,但是抽象类允许你的类承认它不知道如何执行这些操作;它只知道它必须启动行动。在启动时,抽象类可以调用启动方法。当基类调用此方法时,Java会调用子类定义的方法。

     

许多开发人员忘记了定义抽象方法的类也可以调用该方法。抽象类是创建计划继承层次结构的绝佳方法。对于类层次结构中的非叶类,它们也是一个很好的选择。

     

类与接口

     

有人说你应该用接口来定义所有类,但我认为推荐看起来有点极端。当我看到设计中的某些内容经常发生变化时,我会使用接口。

     

例如,策略模式允许您将新算法和流程交换到您的程序中,而无需更改使用它们的对象。媒体播放器可能知道如何播放CD,MP3和wav文件。当然,您不希望将这些播放算法硬编码到播放器中;这将使添加像AVI这样的新格式变得困难。此外,您的代码将充斥着无用的case语句。并且为了增加侮辱伤害,每次添加新算法时都需要更新这些案例语句。总而言之,这不是一种非常面向对象的编程方式。

     

使用策略模式,您可以简单地将算法封装在对象后面。如果这样做,您可以随时提供新的媒体插件。我们来调用插件类MediaStrategy。该对象将有一个方法:playStream(Stream s)。因此,为了添加新算法,我们只需扩展我们的算法类。现在,当程序遇到新的媒体类型时,它只是将流的播放委托给我们的媒体策略。当然,您需要一些管道来正确实例化您需要的算法策略。

     

这是使用界面的绝佳场所。我们使用了策略模式,它清楚地表明了设计中将会发生变化的地方。因此,您应该将策略定义为接口。当您希望对象具有某种类型时,通常应该优先选择接口而不是继承。在这种情况下,MediaStrategy。依赖继承进行类型识别是危险的;它将您锁定到特定的继承层次结构中。 Java不允许多重继承,因此您无法扩展为您提供有用实现或更多类型标识的内容。

答案 11 :(得分:10)

另外请记住,不要在OO(see blog)中一扫而空,并始终根据所需的行为建模对象,如果您正在设计一个应用程序,其中您需要的唯一行为是通用名称和物种动物那么你只需要一个具有名称属性的动物类,而不是世界上每种可能动物的数百万个类。

答案 12 :(得分:9)

我有一个粗略的经验法则

功能:可能在所有部分都有所不同:界面。

数据和功能,部分大致相同,部分不同:抽象类。

数据和功能,实际工作,如果只是略有改变而扩展:普通(具体)类

数据和功能,未计划更改:具有最终修饰符的普通(具体)类。

数据,也许功能:只读:枚举成员。

这是非常粗略和准备好的,并没有严格定义,但有一个接口的频谱,其中所有内容都要更改为枚举,其中所有内容都修复有点像只读文件。

答案 13 :(得分:7)

接口应该很小。真的很小。如果您真的打破了对象,那么您的接口可能只包含一些非常具体的方法和属性。

抽象类是快捷方式。 PetBase的所有衍生品是否共享,您可以编码一次并完成?如果是,那么是抽象课的时间。

抽象类也是限制性的。虽然它们为您提供了生成子对象的快捷方式,但任何给定对象都只能实现一个抽象类。很多时候,我发现这是Abstract类的限制,这就是我使用大量接口的原因。

抽象类可能包含多个接口。你的PetBase抽象类可以实现IPet(宠物拥有者)和IDigestion(宠物吃,或至少他们应该)。然而,PetBase可能不会实施IMammal,因为并非所有宠物都是哺乳动物,并非所有哺乳动物都是宠物。您可以添加一个扩展PetBase并添加IMammal的MammalPetBase。 FishBase可以拥有PetBase并添加IFish。 IFish会将ISwim和IUnderwaterBreather作为接口。

是的,我的例子对于简单的例子来说过于复杂,但这是关于接口和抽象类如何协同工作的一部分。

答案 14 :(得分:6)

来源http://jasonroell.com/2014/12/09/interfaces-vs-abstract-classes-what-should-you-use/

C#是一种很好的语言,在过去的14年里已经成熟和发展。这对我们的开发人员来说非常棒,因为成熟的语言为我们提供了大量的语言功能。

然而,用更多的力量变得更有责任感。其中一些功能可能被滥用,或者有时很难理解为什么选择使用一个功能而不是另一个功能。多年来,我看到许多开发人员都在努力解决的一个功能是何时选择使用接口或选择使用抽象类。两者都有优点和缺点以及正确的时间和地点。但是我们如何决定???

两者都提供了类型之间共同功能的重用。最明显的区别是接口不提供其功能的实现,而抽象类允许您实现一些“基本”或“默认”行为,然后能够在必要时使用类派生类型“覆盖”此默认行为

这一切都很好,并提供了很好的代码重用,并坚持软件开发的DRY(不要重复自己)原则。当你有“是一种”关系时,抽象类很有用。

例如:金毛猎犬“是一种”狗。一只贵宾犬也是如此。它们都可以像所有狗一样吠叫。但是,您可能想要声明贵宾犬公园与“默认”狗皮大不相同。因此,您可以按照以下方式实施:

public abstract class Dog
{
      public virtual void Bark()
      {
        Console.WriteLine("Base Class implementation of Bark");
      }
}

public class GoldenRetriever : Dog
{
   // the Bark method is inherited from the Dog class
}

public class Poodle : Dog
{
  // here we are overriding the base functionality of Bark with our new implementation
  // specific to the Poodle class
  public override void Bark()
  {
     Console.WriteLine("Poodle's implementation of Bark");
  }
}

// Add a list of dogs to a collection and call the bark method.

void Main()
{
    var poodle = new Poodle();
    var goldenRetriever = new GoldenRetriever();

    var dogs = new List<Dog>();
    dogs.Add(poodle);
    dogs.Add(goldenRetriever);

    foreach (var dog in dogs)
    {
       dog.Bark();
    }
}

// Output will be:
// Poodle's implementation of Bark
// Base Class implementation of Bark

// 

正如您所看到的,这将是保持代码DRY并允许在任何类型只依赖于默认Bark而不是特殊情况实现时调用基类实现的好方法。像GoldenRetriever,Boxer,Lab这样的类都可以免费继承“默认”(bass class)Bark,因为它们实现了Dog抽象类。

但我相信你已经知道了。

您在这里是因为您想了解为什么您可能想要在抽象类上选择接口,反之亦然。您可能想要在抽象类上选择接口的一个原因是您没有或想要阻止默认实现。这通常是因为实现接口的类型与“是”关系不相关。实际上,除了每种类型“能够”或“有能力”做某事或有某事之外,它们根本不需要相关。

现在这意味着什么?嗯,例如:人类不是鸭子......鸭子不是人类。很明显。然而,鸭子和人类都有“游泳能力”(考虑到人类通过了他在一年级的游泳课:))。此外,由于鸭子不是人类,反之亦然,这不是“是一种”关系,而是“能够”关系,我们可以使用界面来说明:

// Create ISwimable interface
public interface ISwimable
{
      public void Swim();
}

// Have Human implement ISwimable Interface
public class Human : ISwimable

     public void Swim()
     {
        //Human's implementation of Swim
        Console.WriteLine("I'm a human swimming!");
     }

// Have Duck implement ISwimable interface
public class Duck: ISwimable
{
     public void Swim()
     {
          // Duck's implementation of Swim
          Console.WriteLine("Quack! Quack! I'm a Duck swimming!")
     }
}

//Now they can both be used in places where you just need an object that has the ability "to swim"

public void ShowHowYouSwim(ISwimable somethingThatCanSwim)
{
     somethingThatCanSwim.Swim();
}

public void Main()
{
      var human = new Human();
      var duck = new Duck();

      var listOfThingsThatCanSwim = new List<ISwimable>();

      listOfThingsThatCanSwim.Add(duck);
      listOfThingsThatCanSwim.Add(human);

      foreach (var something in listOfThingsThatCanSwim)
      {
           ShowHowYouSwim(something);
      }
}

 // So at runtime the correct implementation of something.Swim() will be called
 // Output:
 // Quack! Quack! I'm a Duck swimming!
 // I'm a human swimming!

使用上面代码之类的接口将允许您将对象传递给“能够”执行某些操作的方法。代码并不关心它是如何做到的......它只知道它可以在该对象上调用Swim方法,并且该对象将根据其类型知道在运行时采取的行为。

再次,这有助于您的代码保持DRY,这样您就不必编写多个调用该对象的方法来执行相同的核心功能(ShowHowHumanSwims(human),ShowHowDuckSwims(duck)等)

在此处使用接口允许调用方法不必担心行为的实现类型或方式。它只知道给定接口,每个对象都必须实现Swim方法,因此可以安全地在自己的代码中调用它,并允许在自己的类中处理Swim方法的行为。

要点:

因此,当您想要为类层次结构实现“默认”功能或/和您正在使用的类或类型共享“是一种”关系时,我的主要经验法则是使用抽象类(例如poodle) “是一种”狗“。

另一方面,当你没有“是一种”关系时,使用一个界面,但是有一些类型分享“能力”做某事或有某事(例如鸭子“不是”人类。但是,鸭子和人类分享“游泳的能力”。

抽象类和接口之间需要注意的另一个区别是,类可以实现一个到多个接口,但是一个类只能从一个抽象类(或任何类)继承。是的,您可以嵌套类并具有继承层次结构(许多程序都应该这样做)但您不能在一个派生类定义中继承两个类(此规则适用于C#。在其他一些语言中,您可以执行此操作,通常只是因为这些语言中缺少接口。)

还要记住使用接口遵守接口隔离原则(ISP)。 ISP声明不应该强迫任何客户端依赖它不使用的方法。因此,接口应该专注于特定任务,并且通常非常小(例如IDisposable,IComparable)。

另一个提示是,如果您正在开发小巧,简洁的功能,请使用接口。如果您正在设计大型功能单元,请使用抽象类。

希望这能为某些人解决问题!

此外,如果你能想到更好的例子或想要指出一些事情,请在下面的评论中这样做!

答案 15 :(得分:6)

Submain .NET编码指南

中详细解释了基于接口的基类的情况
  

基类与接口   接口类型是部分的   可能的值的描述   由许多对象类型支持。使用   基类而不是接口   只要有可能。从版本控制   透视,课程更灵活   比接口。有了课,你就可以   发布版本1.0,然后发布版本   2.0为类添加一个新方法。只要方法不抽象,   任何现有的派生类继续   功能不变。

     

因为接口不支持   实现继承,   适用于类的模式   不适用于接口。添加一个   接口的方法是等价的   向基础添加抽象方法   类;任何实现的类   接口会因为类而中断   没有实现新方法。   接口适用于   以下情况:

     
      
  1. 几个不相关的类想要支持该协议。
  2.   
  3. 这些类已经建立了基类(for   例,   一些是用户界面(UI)控件,   还有一些是XML Web服务。)
  4.   
  5. 汇总不合适或不切实际。在所有其他   的情况下,   类继承是一种更好的模型。
  6.   

答案 16 :(得分:5)

一个重要的区别是您只能继承一个基类,但您可以实现多个接口。因此,如果您绝对确定,您只需要使用基类,您也不需要继承其他基类。此外,如果您发现您的界面变得越来越大,那么您应该开始将其分解为一些定义独立功能的逻辑部分,因为没有规则您的类无法全部实现(或者您可以定义不同的接口只是将它们全部继承为对它们进行分组。)

答案 17 :(得分:4)

当我第一次开始学习面向对象编程时,我犯了一个简单而且可能是使用继承来分享常见行为的常见错误 - 即使这种行为对于对象的本质并不重要。

为了进一步建立一个在这个特定问题中使用的例子,有很多可以接受的东西 - 女朋友,汽车,模糊毛毯...... - 所以我可能有一个Petable类提供了这种常见行为,以及从中继承的各种类。

但是,容易接触不属于任何这些对象的性质。有很多重要的概念 对他们的本性至关重要 - 女朋友是一个人,汽车是陆地车辆,猫是哺乳动物......

应首先将行为分配给接口(包括类的默认接口),并且只有当它们(a)对于作为较大类的子集的大型类是通用的时才提升为基类 - 同样的感觉,“猫”和“人”是“哺乳动物”的子集。

问题是,在你完全理解面向对象设计之后,你通常会在没有考虑它的情况下自动执行此操作。所以“代码接口而不是抽象类”这一陈述的明显事实变得如此明显,你很难相信任何人都会费心去说 - 并开始尝试将其他含义读入其中。

我要添加的另一件事是,如果一个类纯粹抽象 - 没有非抽象的,非继承的成员或方法暴露给子,父或客户端 - 那么为什么呢一类?它可以在某些情况下由接口替换,在其他情况下由Null替换。

答案 18 :(得分:4)

首选接口而不是抽象类

理由, 要考虑的要点[这里已经提到过两个]是:

  • 接口更灵活,因为一个类可以实现多个 接口。由于Java没有多重继承,因此使用 抽象类可以防止用户使用任何其他类 层次结构。 通常,在没有默认值时更喜欢接口 实现或状态。 Java集合提供了很好的例子 这(地图,集等)。
  • 抽象类具有允许更好的前进的优点 兼容性。客户端使用界面后,您无法更改它; 如果他们使用抽象类,您仍然可以添加行为 破坏现有代码。 如果兼容性是一个问题,请考虑使用 抽象类。
  • 即使您有默认实施或内部状态, 考虑提供一个接口及其抽象实现。 这将有助于客户,但仍然允许他们更大的自由,如果 期望的[1] 当然,这个问题已经详细讨论过了 别的[2,3]。

[1]当然,它增加了更多的代码,但如果简洁是您最关心的问题,那么您可能应该首先避免使用Java!

[2] Joshua Bloch,Effective Java,第16-18项。

[3] http://www.codeproject.com/KB/ar ...

答案 19 :(得分:3)

除非您知道它的含义,否则不要使用基类,并且在这种情况下它适用。如果适用,请使用它,否则,使用接口。但请注意关于小接口的答案。

公共继承在OOD中被过度使用,并且表达了比大多数开发人员意识到或愿意实现的更多。请参阅Liskov Substitutablity Principle

简而言之,如果A“是一个”B,那么对于它所暴露的每种方法,A只需要不超过B并且不超过B。

答案 20 :(得分:3)

要记住的另一个选择是使用“has-a”关系,又称“是按照”或“组合”实现的。有时,这比使用“is-a”继承更简洁,更灵活地构建事物。

从逻辑上讲,狗和猫都“拥有”宠物可能没有多大意义,但它避免了常见的多重继承陷阱:

public class Pet
{
    void Bathe();
    void Train(Trick t);
}

public class Dog
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

public class Cat
{
    private Pet pet;

    public void Bathe() { pet.Bathe(); }
    public void Train(Trick t) { pet.Train(t); }
}

是的,这个例子表明,这样做有很多代码重复和缺乏优雅。但是人们也应该理解,这有助于使狗和猫与宠物类脱钩(因为狗和猫无法接触宠物的私人成员),并且它为狗和猫留下了继承其他东西的空间 - - 可能是哺乳动物班。

当不需要私人访问且您不需要使用通用Pet引用/指针引用Dog和Cat时,组合是首选。接口为您提供了通用的引用功能,可以帮助减少代码的冗长,但是当它们组织不良时,它们也可能会混淆。当您需要私有成员访问权限时,继承非常有用,并且在使用它时,您承诺将您的Dog和Cat类高度耦合到您的Pet类,这是一个非常高的代价。

在继承,组合和接口之间,没有一种方法总是正确的,并且有助于考虑如何协调使用所有三个选项。在这三者中,继承通常是应该最少使用的选项。

答案 21 :(得分:3)

从概念上讲,接口用于正式和半正式地定义对象将提供的一组方法。形式上是指一组方法名称和签名,半正式地表示与这些方法相关的人类可读文档。接口只是API的描述(毕竟,API代表Application Programmer 接口),它们不能包含任何实现,也不可能使用或运行接口。他们只明确说明你应该如何与一个对象进行交互。

类提供了一个实现,他们可以声明它们实现了零个,一个或多个接口。如果要继承Class,则约定是在Class类名前加上“Base”。

基类和抽象基类(ABC)之间存在区别。 ABCs将接口和实现混合在一起。计算机编程之外的摘要意味着“摘要”,即“抽象==接口”。然后,抽象基类可以描述接口,以及要继承的空,部分或完整实现。

关于何时使用接口与抽象基类和仅基于类的观点将根据您正在开发的内容以及您正在开发的语言而有很大差异。接口通常仅与静态类型语言(如Java或Java)相关联。 C#,但动态类型语言也可以有Interfaces和Abstract Base Classes。例如,在Python中,一个Class声明了实现一个接口,而一个对象是一个Class的实例,并且被称为提供< / strong>那个界面。在动态语言中,两个对象都是同一个类的实例,可以声明它们提供完全不同的接口。在Python中,这仅适用于对象属性,而方法是类的所有对象之间的共享状态。但是在Ruby中,对象可以有每个实例的方法,因此同一个类的两个对象之间的接口可能会随着程序员的需要而变化(但是,Ruby没有任何明确的方式来声明接口)。 / p>

在动态语言中,通常隐式假设对象的接口,或者通过内省对象并询问它提供了哪些方法(Look Before You Leap),或者最好只是尝试在对象上使用所需的接口并捕获异常如果对象没有提供该接口(更容易请求宽恕而不是权限)。这可能导致“误报”,其中两个接口具有相同的方法名称但在语义上不同,但是权衡是您的代码更灵活,因为您不需要预先指定预先考虑所有可能的用途你的代码。

答案 22 :(得分:3)

关于使用抽象类进行常规实现的先前注释肯定是标记。我还没有看到的一个好处是,使用接口可以更容易地实现模拟对象以进行单元测试。如Jason Cohen所描述的那样定义IPet和PetBase使您可以轻松地模拟不同的数据条件,而无需物理数据库的开销(直到您决定测试真实数据库的时间)。

答案 23 :(得分:2)

我发现了一种接口模式&gt;摘要&gt;具体适用于以下用例:

1.  You have a general interface (eg IPet)
2.  You have a implementation that is less general (eg Mammal)
3.  You have many concrete members (eg Cat, Dog, Ape)

抽象类定义具体类的默认共享属性,但强制执行接口。例如:

public interface IPet{

    public boolean hasHair();

    public boolean walksUprights();

    public boolean hasNipples();
}

现在,由于所有哺乳动物都有头发和乳头(AFAIK,我不是动物学家),我们可以将其推广到抽象基类

public abstract class Mammal() implements IPet{

     @override
     public walksUpright(){
         throw new NotSupportedException("Walks Upright not implemented");
     }

     @override
     public hasNipples(){return true}

     @override
     public hasHair(){return true}

然后具体的课程只是定义他们直立行走。

public class Ape extends Mammal(){

    @override
    public walksUpright(return true)
}

public class Catextends Mammal(){

    @override
    public walksUpright(return false)
}

当有许多具体类时,这种设计很好,并且您不希望仅仅为了编程接口而维护样板。如果在接口中添加了新方法,它将破坏所有生成的类,因此您仍然可以获得接口方法的优势。

在这种情况下,摘要也可以具体;然而,抽象的名称有助于强调这种模式正在被采用。

答案 24 :(得分:2)

这取决于您的要求。如果IPet足够简单,我宁愿实现它。否则,如果PetBase实现了大量功能,您不想复制,那么请使用它。

实现基类的缺点是需要override(或new)现有方法。这使得它们成为虚拟方法,这意味着您必须小心如何使用对象实例。

最后,.NET的单一继承会让我失望。一个简单的例子:假设你正在建立一个用户控件,所以你继承了UserControl。但是,现在你被锁定也继承了PetBase。这会强制您重新组织,例如改为成为PetBase类成员。

答案 25 :(得分:2)

@Joel:某些语言(例如C ++)允许多重继承。

答案 26 :(得分:2)

通过def,接口提供了一个与其他代码通信的层。默认情况下,类的所有公共属性和方法都实现隐式接口。我们还可以将接口定义为角色,当任何类需要扮演该角色时,它必须实现它,根据实现它的类,为其提供不同的实现形式。因此,当你谈论接口时,你谈论的是多态性,当你谈论基类时,你谈论的是继承。 oops的两个概念!!!

答案 27 :(得分:2)

在我需要之前,我通常都不会实施。我更喜欢接口而不是抽象类,因为它提供了更多的灵活性。如果某些继承类中存在共同行为,我会将其移动并创建一个抽象基类。我没有看到两者的需要,因为它们本质上服务于相同的目的,并且两者都是一个糟糕的代码味道(imho),解决方案已被过度设计。

答案 28 :(得分:2)

关于C#,在某种意义上,接口和抽象类可以互换。但是,不同之处在于:i)接口无法实现代码; ii)因此,接口无法进一步调用堆栈到子类; iii)只有抽象类可以在类上继承,而多个接口可以在类上实现。

答案 29 :(得分:1)

使用接口强制执行合同ACROSS不相关类的系列。例如,您可能对表示集合的类具有公共访问方法,但包含完全不同的数据,即一个类可能表示查询的结果集,而另一个类可能表示库中的图像。此外,您可以实现多个接口,从而允许您混合(和表示)类的功能。

当类具有共同关系时使用继承,因此具有类似的结构和行为特征,即汽车,摩托车,卡车和SUV都是可能包含多个轮子的公路车辆类型,最高速度

答案 30 :(得分:1)

基类的继承者应该具有“是一种”关系。接口表示“实现”关系。 因此,只有在继承者维持关系时才使用基类。

答案 31 :(得分:1)

列出您的对象必须所拥有或拥有的内容,以及您的对象可以(或可能)的内容,拥有或做。 必须表示您的基本类型,可以表示您的接口。

,例如,您的PetBase 必须呼吸,您的IPet 可能 DoTricks。

分析您的问题域将帮助您定义精确的层次结构。

答案 32 :(得分:1)

  

我何时应该使用接口?何时应该使用基类?

如果

,您应该使用interface
  1. 您拥有纯abstract方法,并且没有非抽象方法
  2. 您没有non abstract方法的默认实现(Java 8语言除外,其中接口方法提供默认实现)
  3. 如果您使用的是Java 8,现在接口将为某些非抽象方法提供默认实现。与interface类相比,这将使abstract更有用。
  4. 有关详细信息,请查看此SE question

      

    如果我不想实际定义方法的基本实现,它应该始终是一个接口吗?

    是。它更好,更清洁。即使你有一个带有一些抽象方法的基类,让我们的基类通过接口扩展abstract方法。您可以在以后更改界面而无需更改基类。

    java中的示例:

    abstract class PetBase implements IPet {
    // Add all abstract methods in IPet interface and keep base class clean. 
       Base class will contain only non abstract methods and static methods.
    }
    
      

    如果我有狗和猫类。为什么我要实现IPet而不是PetBase?我可以理解有ISheds或IBarks(IMakesNoise?)的接口,因为那些可以基于宠物放在宠物上,但我不明白哪个用于通用宠物。

    我更喜欢让基类实现接口。

     abstract class PetBase implements IPet {
     // Add all abstract methods in IPet
     }
    
     /*If ISheds,IBarks is common for Pets, your PetBase can implement ISheds,IBarks. 
      Respective implementations of PetBase can change the behaviour in their concrete classes*/
    
     abstract class PetBase implements IPet,ISheds,IBarks {
     // Add all abstract methods in respective interfaces
     }
    

    <强>优点:

    1. 如果我想在现有接口中再添加一个抽象方法,我可以简单地更改接口而不需要触及抽象基类。如果我想更改合同,我将更改界面和没有触及基类的实现类。

    2. 您可以通过接口为基类提供不变性。看看这个article

    3. 有关详细信息,请参阅此相关的SE问题:

      How should I have explained the difference between an Interface and an Abstract class?

答案 33 :(得分:0)

除了那些提及IPet / PetBase实现的评论之外,还有一些情况是提供访问者助手类非常有价值。

IPet / PetBase样式假设您有多个实现,从而增加了PetBase的值,因为它简化了实现。但是,如果您有两个客户端的反向或混合,那么提供类帮助以帮助使用界面可以通过使界面更容易使用来降低成本。

答案 34 :(得分:0)

感谢answering Jon Limjap,但我想为接口和抽象基类的概念添加一些解释

界面类型与抽象基类

改编自Pro C# 5.0 and the .NET 4.5 Framework书。

接口类型可能看起来与抽象基类非常相似。召回 当一个类被标记为抽象时,它可以定义任意数量的抽象成员来提供一个 所有派生类型的多态接口。但是,即使一个类确实定义了一组抽象 成员,它也可以自由定义任意数量的构造函数,字段数据,非抽象成员(使用 实施),等等。另一方面,接口仅包含抽象成员定义。 由抽象父类建立的多态接口受到一个主要限制 因为只有派生类型支持抽象父类定义的成员。但是,更大 在软件系统中,开发没有共同父级的多个类层次结构是很常见的 超越System.Object。假设抽象基类中的抽象成员仅适用于派生类 类型,我们无法在不同的层次结构中配置类型以支持相同的多态 接口。举例来说,假设您已定义以下抽象类:

public abstract class CloneableType
{
// Only derived types can support this
// "polymorphic interface." Classes in other
// hierarchies have no access to this abstract
// member.
   public abstract object Clone();
}

鉴于此定义,只有扩展CloneableType的成员才能支持Clone() 方法。如果您创建一组不扩展此基类的新类,则无法获得此类 多态接口。此外,您可能还记得C#不支持类的多重继承。 因此,如果您想创建一个CarVan并且是CloneableType的MiniVan,则无法执行此操作:

// Nope! Multiple inheritance is not possible in C#
// for classes.
public class MiniVan : Car, CloneableType
{
}

正如您所猜测的,接口类型可以解决。定义接口后,它可以 由任何类或结构,任何层次结构,任何命名空间或任何程序集中实现 (用任何.NET编程语言编写)。如您所见,接口具有高度多态性。 考虑在System命名空间中定义的名为ICloneable的标准.NET接口。这个 interface定义了一个名为Clone()的方法:

public interface ICloneable
{
object Clone();
}

答案 35 :(得分:0)

用你自己的判断聪明。不要总是做别人(像我一样)说的话。您将听到“更喜欢抽象类的接口”,但它确实取决于。这取决于班级是什么。

在上面提到的具有对象层次结构的情况下,界面是一个好主意。接口有助于处理这些对象的集合,并且在实现使用层次结构的任何对象的服务时也有帮助。您只需定义用于处理层次结构中对象的合同。

另一方面,当您实现一组共享一个共同功能的服务时,您可以将常用功能分成一个完整的单独类,也可以将其移动到一个公共基类中并使其抽象化,以便没有人可以实例化基类。

还要考虑这个如何随着时间的推移支持你的抽象。接口是固定的:您将接口作为任何类型可以实现的一组功能的合同发布。基类可以随着时间的推移而扩展。这些扩展成为每个派生类的一部分。

答案 36 :(得分:0)

接口具有明显的优势,即对类有些“热插拔”。将类从一个父类更改为另一个类通常会导致大量工作,但通常可以删除和更改接口,而不会对实现类产生很大影响。这在您可能“希望”要实现类的几个狭窄行为集的情况下尤其有用。

这在我的领域特别有效:游戏编程。基类可能会因为继承对象“可能”需要的大量行为而变得臃肿。通过接口,可以轻松,轻松地向对象添加或删除不同的行为。例如,如果我为想要反映损坏的对象创建“IDamageEffects”的接口,那么我可以轻松地将其应用于各种游戏对象,并在以后轻松改变我的想法。假设我设计了一个我想用于“静态”装饰物的初始类,我最初认为它们是不可破坏的。稍后,我可能会认为如果它们爆炸会更有趣,所以我改变了类来实现“IDamageEffects”界面。这比切换基类或创建新的对象层次结构要容易得多。

答案 37 :(得分:0)

继承还有其他优点 - 例如变量能够保存父类或继承类的对象的能力(不必将其声明为泛型,如“Object”) 。

例如,在.NET WinForms中,大多数UI组件都派生自System.Windows.Forms.Control,因此声明为可以“保存”任何UI元素的变量 - 无论是按钮,ListView还是什么有你。现在,已经授予,您将无法访问该项目的所有属性或方法,但您将拥有所有基本内容 - 这可能很有用。

答案 38 :(得分:0)

如果除了您的类型成员之外没有任何其他开发人员希望使用他们自己的基类,您应该使用基类(请参阅{{3 }})。

如果继承开发人员有任何理由使用自己的基类来实现类型的接口,并且您没有看到接口发生变化,那么请使用接口。在这种情况下,为了方便起见,您仍然可以引入实现接口的默认基类。