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

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

如果我有狗和猫的课。为什么我要实现IPet而不是PetBase?我可以理解为ISheds或IBarks(IMakesNoise?)提供接口,因为它们可以逐个宠物放置,但我不知道该为普通宠物使用哪个接口。


当前回答

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

为了进一步建立这个问题中经常使用的例子,有很多东西都是可拍的——女朋友、汽车、毛毯所以我可能有一个Petable类提供了这种常见的行为,以及从中继承的各种类。

然而,可拍不是这些物体的本质的一部分。还有很多更重要的概念对他们的天性至关重要——女朋友是人,汽车是陆地车辆,猫是哺乳动物。。。

行为应该首先被分配给接口(包括类的默认接口),并且只有当它们(a)对于作为更大类的子集的一大群类来说是公共的时,才应该被提升为基类——在相同的意义上,“猫”和“人”是“哺乳动物”的子集。

问题是,当你比我一开始更了解面向对象设计之后,你通常会自动完成这项工作,甚至不用考虑它。因此,“代码到接口,而不是抽象类”这句话的真实性变得如此明显,你很难相信任何人都会不厌其烦地说出它,并开始尝试将其他含义解读到其中。

我想补充的另一点是,如果一个类是纯抽象的-没有非抽象的、非继承的成员或方法暴露给子级、父级或客户端-那么为什么它是一个类?它可以被替换,在某些情况下被接口替换,在其他情况下被Null替换。

其他回答

在这篇Java World文章中对其进行了很好的解释。

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

我会有一个类实现一个或多个接口,这并不罕见。

抽象类是我用来做其他事情的基础。

以下是上述文章JavaWorld.com文章的摘录,作者Tony Sintes,2001年4月20日


接口与抽象类选择接口和抽象类不是一个非此即彼的命题。如果你需要改变你的设计,让它成为一个界面。然而,您可能有提供一些默认行为的抽象类。抽象类是应用程序框架中的优秀候选者。抽象类允许您定义一些行为;它们迫使您的子类提供其他子类。例如,如果您有一个应用程序框架,抽象类可以提供默认服务,如事件和消息处理。这些服务允许应用程序插入应用程序框架。但是,有些特定于应用程序的功能只有您的应用程序才能执行。此类功能可能包括启动和关闭任务,这些任务通常依赖于应用程序。因此,抽象基类可以声明抽象的关闭和启动方法,而不是试图定义该行为本身。基类知道它需要这些方法,但抽象类让您的类承认它不知道如何执行这些操作;它只知道它必须发起行动。当需要启动时,抽象类可以调用启动方法。当基类调用此方法时,Java调用子类定义的方法。许多开发人员忘记了定义抽象方法的类也可以调用该方法。抽象类是创建计划继承层次结构的极好方法。对于类层次结构中的非叶类,它们也是一个很好的选择。类与接口

有人说你应该用接口来定义所有的类,但我认为这个建议有点极端。当我看到设计中的某些内容会频繁更改时,我会使用接口。

例如,Strategy模式允许您在程序中交换新的算法和过程,而不改变使用它们的对象。媒体播放器可能知道如何播放CD、MP3和wav文件。当然,您不希望将这些回放算法硬编码到播放器中;这将使添加像AVI这样的新格式变得困难。此外,您的代码将充斥着无用的case语句。为了增加伤害,您需要在每次添加新算法时更新这些案例陈述。总而言之,这不是一种非常面向对象的编程方式。使用Strategy模式,您可以简单地将算法封装在对象后面。如果这样做,您可以随时提供新的媒体插件。让我们将插件类称为MediaStrategy。该对象将有一个方法:playStream(流)。所以为了添加一个新的算法,我们只需扩展我们的算法类。现在,当程序遇到新的媒体类型时,它只是将流的播放委托给我们的媒体策略。当然,您需要一些管道来正确实例化所需的算法策略。这是一个使用界面的好地方。我们使用了Strategy模式,它清楚地表明设计中的一个位置将发生变化。因此,您应该将策略定义为一个接口。当您希望某个对象具有某种类型时,通常应该支持接口而不是继承;在本例中为MediaStrategy。依赖类型标识的继承是危险的;它将您锁定到特定的继承层次结构中。Java不允许多重继承,因此您不能扩展能够为您提供有用实现或更多类型标识的内容。

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

接口类型与抽象基类

改编自Pro C#5.0和.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#不支持类的多重继承。因此,如果您想创建一辆小型货车,它是一辆汽车,并且是一种可克隆类型,那么您无法做到:

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

正如您所猜测的,接口类型起到了拯救作用。定义接口后,可以可以由任何类或结构、任何层次结构、任何命名空间或任何程序集中实现(用任何.NET编程语言编写)。正如您所看到的,接口是高度多态的。考虑System命名空间中定义的名为ICloneable的标准.NET接口。这接口定义了一个名为Clone()的方法:

public interface ICloneable
{
object Clone();
}

如果除了您的类型成员之外,其他开发人员确实没有任何理由希望使用自己的基类,并且您预见到版本控制问题,那么您应该使用基类(请参见http://haacked.com/archive/2008/02/21/versioning-issues-with-abstract-base-classes-and-interfaces.aspx).

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

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

公共继承在OOD中被过度使用,表达的内容比大多数开发人员意识到或愿意实现的要多得多

简而言之,如果A“是一个”B,那么对于它公开的每个方法,A需要的不超过B,交付的不少于B。

从概念上讲,接口用于正式和半正式地定义对象将提供的一组方法。形式上表示一组方法名称和签名,半形式上表示与这些方法相关的人类可读文档。

接口只是API的描述(毕竟,API代表应用程序编程接口),它们不能包含任何实现,也不可能使用或运行接口。它们只明确约定你应该如何与对象交互。

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

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

关于何时使用接口、抽象基类还是仅使用类的意见将根据您正在开发的内容以及您正在使用的语言而大不相同。接口通常仅与静态类型语言(如Java或C#)相关,但动态类型语言也可以有接口和抽象基类。例如,在Python中,Class和对象之间的区别很明显,前者声明它实现了一个接口,后者是一个类的实例,据说提供了该接口。在动态语言中,两个都是同一类实例的对象可以声明它们提供完全不同的接口。在Python中,这仅适用于对象属性,而方法在类的所有对象之间共享状态。然而,在Ruby中,对象可以具有每实例方法,因此同一类的两个对象之间的接口可能会根据程序员的需要而变化(然而,Ruby没有任何明确的接口声明方式)。

在动态语言中,对象的接口通常是隐式假设的,可以通过内省对象并询问它提供了什么方法(在跳转之前查看),也可以通过简单地尝试在对象上使用所需的接口并在对象不提供该接口时捕捉异常(请求原谅比请求许可更容易)。这可能导致“误报”,即两个接口具有相同的方法名称,但语义不同。然而,权衡是代码更加灵活,因为您不需要预先过度指定以预测代码的所有可能用途。