接口与基类


767

什么时候应该使用接口,什么时候应该使用基类?

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

如果我有猫狗班。为什么我要实现IPet而不是PetBase?我可以理解具有用于ISheds或IBarks(IMakesNoise?)的接口,因为可以将它们逐个放置在每个宠物上,但是我不知道该为通用Pet使用哪个接口。


11
我认为您应该考虑这一点-接口可能会带来一些限制,您可能直到很晚才意识到。例如,使用.NET,您无法序列化接口成员变量,因此,如果您具有类Zoo和IAnimals的成员变量数组,则将无法序列化Zoo(这意味着编写WebServices或其他需要序列化的操作将痛苦)。
synhershko

1
这个问题可能有助于理解接口的概念。stackoverflow.com/q/8531292/1055241
gprathour 2014年

我只是好奇。我在CLR中通过C#遇到了以下摘录:I tend to prefer using the interface technique over the base type technique because the base type technique doesn’t allow the developer to choose the base type that works best in a particular situation.。我不明白摘录中的意思。我们可以创建一些基本类型并为它们中的任何一个创建派生类型,以便开发人员可以选择基本类型。有人可以解释一下,我想念什么?我相信这可能是这个问题的一部分。还是我应该再发布一份有关特定摘录的文章?
qqqqqqq

Answers:


501

让我们以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 ++),但是能够实现多个接口,因此它允许您严格根据需要构造对象。


149
我认为不是那么简单。您已稍微更改了问题(要求),以使界面更有意义。您应该始终问自己是要定义合同(接口)还是共享实现(基类)。
David Pokluda,

18
该接口是合同。您仅公开服务需要的合同部分。如果您拥有“ PettingZoo”,那么您当然不希望向用户公开“配合”!
Anthony Mastrean's

10
@David Touche,尽管我这样做是为了更好地说明接口的用途以及相对于他的理解的抽象类。狗和猫似乎不是严格的要求!
乔恩·林贾普

2
应当注意,在解释的JIT-ing环境(尤其是JVM)中,根据上下文,虚拟方法调用比接口方法调用快得多。我强调“上下文”,因为JVM通常可以优化慢速方法查找。(例如,如果接口继承者的列表往往是同一类的实例。令人遗憾的是,很难对基准进行基准测试。)如果要优化对性能敏感的内容,只有在这种情况下,才应考虑这一点。
菲利普·金

14
而且,界面允许宠物岩石,使其沐浴和传授技巧,但不支持喂食和交配,因为这对于岩石来说是荒谬的。
eclux

146

好吧,Josh Bloch在有效Java 2d中说

优先于抽象类的接口

一些要点:

  • 现有的类可以很容易地改装以实现新的接口。您所要做的就是添加所需的方法(如果尚不存在),然后在类声明中添加Implements子句。

  • 接口是定义mixin的理想选择。松散地说,mixin是一种类,除了其“主要类型”之外,还可以实现它来声明它提供了一些可选的行为。例如,Comparable是一个mixin接口,它允许类声明其实例相对于其他相互可比较的对象而排序。

  • 接口允许构造非分层类型的框架。类型层次结构非常适合组织某些事物,但其他事物并不能整齐地归入严格的层次结构。

  • 接口通过包装类成语来实现安全,强大的功能增强。如果使用抽象类定义类型,那么您将不得不添加功能的程序员只能使用继承。

而且,您可以通过提供抽象的骨架实现类与导出的每个非平凡接口一起使用,来结合接口和抽象类的优点。

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

PS .:买书。它要详细得多。


71
每当需要对接口进行更改时,这样做的不间断方法是创建一个继承自旧接口的新接口。这样可以保留现有的实现,并让您在新的实现中做任何想做的事情。
Scott Lawrence

5
我们有“接口隔离原则”。该原则教会我们注意如何编写接口。编写接口时,应注意仅添加应该存在的方法。如果我们添加不应包含的方法,则实现该接口的类也必须实现这些方法。例如,如果我们创建一个名为Worker的接口并添加方法午休,则所有worker都必须实现它。如果工人是机器人怎么办?结论包含非特定方法的接口称为污染接口或胖接口。
Eklavyaa '16

从Java 8开始,默认方法使您可以向接口添加新功能,并确保向后兼容实现该接口的现有类。如果未在实现类中覆盖默认方法,则默认调用默认方法。所有的实现类可以覆盖默认方法,或者他们可以使用instance.defaultMethod直接调用()
Arpit Rastogi

118

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

继承(基类)表示“是”关系。例如,狗或猫是宠物。这种关系始终代表着班级的(单一)目的(结合“单一责任原则”)。

另一方面,接口表示类的其他功能。我将其称为“是”关系,就像“ Foo是一次性的”中一样,因此是IDisposableC#中的接口。


10
出所有的答案,这个人给简洁的最佳混合,而不透明度损失
马特Wilko

曾经有人告诉我在存在“ has-a”关系时使用接口。我不确定这是否总是正确的。我的笔记本电脑有一个屏幕,所以笔记本电脑应该实现IScreen还是具有Screen属性?后者对我来说似乎更自然。
Berend 2014年

1
@berend有趣的是,您写道这是因为屏幕是通过VGA,HDMI等接口实现的
Konstantin

仅仅因为它是面向对象的,我们就不得不扩展我们的想象力来跟随现实生活中的场景。它并不总是适用。
犯罪记录'18

110

现代风格是定义IPet PetBase。

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

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


吃你的蛋糕也吃!
达伦·科普

3
接口定义其他类如何使用您的代码。基类可帮助实现者实现您的接口。出于两个不同目的的两个不同事物。
比尔·米歇尔

27
这里没有任何“现代”的东西。具有相同API的基类和接口只是多余的。在某些情况下,您可以使用此方法,但不能一概而论!
smentek

3
有趣的是,对第二个受支持的答案的最受支持的评论实际上与答案本身不同。我必须说,我看到许多接口和基类共存的例子。从这种意义上讲,这是“现代”方式。例如,在MVVM模式中,实际上存在ViewModelBase类,该类实现了INotifyPropertyChanged。但是当我的同事问我为什么要有基类,而不是在每个视图模型中都实现接口时,我不知道如何说服他
tete 2012年

1
正确。这不是1或另一个的问题。它们的存在是为了解决2个非常不同的问题。接口是实现类必须解决的契约。他们发现IoC和TDD方面最近受到青睐(有时是狂热的)。抽象/基类用于对层次结构的通用逻辑和属性进行分组。它减少了重复的代码,从而提高了解决方案的可维护性,并减少了出错的可能性。
ComeIn

63

介面

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

基类

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

注意:框架设计指南建议使用基类(而不是接口),因为它们的版本更好。添加新的方法来在vNext一个抽象基类是一种非破变化..
Gishu

59

通常,与抽象类相比,您应该更喜欢接口。使用抽象类的原因之一是具体类之间是否具有通用实现。当然,您仍然应该声明一个接口(IPet)并使用一个抽象类(PetBase)来实现该接口。使用小的独特接口,您可以使用倍数来进一步提高灵活性。接口可以最大程度地实现跨边界类型的灵活性和可移植性。跨边界传递引用时,始终传递接口,而不传递具体类型。这使接收端可以确定具体的实现方式并提供最大的灵活性。当以TDD / BDD方式编程时,这绝对是正确的。

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


esh 我个人认为这是倒霉的事情。接口应该保留某种类型的最基本功能,而基类应该提供一个丰富的框架,可以在该框架上构建自定义。将其放在一个接口中,就很难实现。

没有人说您的界面需要很大。较小的多个接口和丰富的基类构成了一个不错的API。
Kilhoffer

3
是我一个人,还是大多数“普通工人”类共享一个共同的实现?在这种情况下,它违反了您偏爱接口的一般规则。我将概括归纳为2个概括:那些不包含或包含很少逻辑的通用类应实现一个接口。那些包含“大量”逻辑的通用类应该从基类派生(因为它们很可能会共享功能)。
goku_da_master 2012年

@Kilhoffer“接口允许跨类型的最大灵活性和可移植性”,请详细说明此声明。
JAVA

49

这是.NET特有的,但是《框架设计指南》一书认为,一般而言,类在不断发展的框架中具有更大的灵活性。交付接口后,您将无法在不破坏使用该接口的代码的情况下对其进行更改。但是,使用一个类,您可以修改它,而不会破坏链接到它的代码。只要您进行了正确的修改(包括添加新功能),便可以扩展和发展代码。

Krzysztof Cwalina在第81页上说:

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

话虽这么说,接口肯定存在。作为一般准则,如果没有其他作为实现接口方法的示例,请始终提供接口的抽象基类实现。在最好的情况下,基类将节省大量工作。


19

胡安

我喜欢将接口视为表征类的一种方式。特定的犬种类别(例如YorkshireTerrier)可能是父犬类别的后代,但它也实现了IFurry,IStubby和IYippieDog。因此,类定义了什么是类,但是接口告诉我们有关它的信息。

这样做的好处是,例如,它使我可以收集所有IYippieDog,并将它们扔到我的Ocean收藏中。因此,现在我可以遍历一组特定的对象并找到符合我正在查看的标准的对象,而无需仔细检查类。

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

这种想法与每个类都应该有一个接口,而您应该对该接口进行编码的想法背道而驰。很好,但是最终会产生大量的类一对一接口,这会使事情变得混乱。我了解这个想法是,它实际上并不需要花费任何时间,现在您可以轻松地进行进出交换。但是,我发现我很少这样做。大多数时候,我只是修改现有的类,并且遇到与我完全相同的问题,如果需要更改该类的公共接口,我总​​是会做同样的事情,除了我现在必须在两个地方进行更改。

因此,如果您像我一样认为,您肯定会说猫和狗是IPettable。这是将两者匹配的特征。

另一个问题是它们是否应该具有相同的基类?问题是,是否需要将它们广泛地视为同一件事。当然它们都是动物,但是这与我们将它们一起使用的方式相适应。

假设我想收集所有动物类并将它们放入我的方舟容器中。

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

它们甚至根本不需要链接在一起吗?仅知道它们都是IPetable就足够了吗?

当我真的只需要一个班级时,我常常感到渴望获得整个班级的层次结构。我有一天会做我可能会需要的东西,但通常我从不这样做。即使我这样做,我通常也必须做很多工作来修复它。那是因为我创建的第一堂课不是狗,我不是那么幸运,而是鸭嘴兽。现在,我的整个类层次结构都是基于奇怪的情况,而且我有很多浪费的代码。

在某些时候,您可能还会发现并非所有的猫都是IPetable的(就像那只无毛的猫一样)。现在,您可以将该接口移动到所有适合的派生类。您会发现,几乎没有那么大的变化,突然之间Cats不再衍生自PettableBase。


18

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

  • 基类=对象继承。
  • 接口=功能继承。

干杯


12

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

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

因此,代替IPet或PetBase,您可能最终得到带有IFurBehavior参数的Pet。IFurBehavior参数由PetFactory的CreateDog()方法设置。shed()方法将调用此参数。

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

即使在多继承语言中,我也建议使用此模式。


12

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

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

我会有一个实现一个或多个接口的类并不少见。

我将抽象类用作其他内容的基础。

以下是上述文章JavaWorld.com文章的作者Tony Sintes的摘录,04/20/01


接口与抽象类

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

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

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

类与接口

有人说您应该根据接口定义所有类,但是我认为建议似乎有些极端。当我发现设计中的某些东西会经常变化时,我会使用接口。

例如,使用策略模式,您可以将新算法和过程交换到程序中,而无需更改使用它们的对象。媒体播放器可能知道如何播放CD,MP3和WAV文件。当然,您不想将这些播放算法硬编码到播放器中;这将使添加AVI等新格式变得困难。此外,您的代码中会堆满无用的case语句。为了增加侮辱性伤害,您每次添加新算法时都需要更新这些案例陈述。总而言之,这不是一种非常面向对象的编程方式。

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

这是使用界面的绝佳场所。我们使用了策略模式,该模式清楚地表明了设计中将会改变的位置。因此,您应该将策略定义为接口。当您希望对象具有某种类型时,通常应该优先考虑接口而不是继承。在这种情况下,是MediaStrategy。依靠继承获得类型标识是危险的;它将您锁定在特定的继承层次结构中。Java不允许多重继承,因此您不能扩展某些可以提供有用的实现或更多类型标识的东西。


2
+1。“依靠继承进行类型标识是危险的;它将您锁定在特定的继承层次结构中。” 那句话完美地描述了我偏爱接口的原因。
工程师

不仅如此,它没有扩展,而是在接口的每种方法后面构成实现。
工程师

10

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


10

我有一个粗略的经验法则

功能:各个部分可能有所不同:接口。

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

数据和功能,如果仅进行了少量更改就可以实际使用:普通(具体)类

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

数据,可能还有功能:只读:枚举成员。

这是非常粗糙且准备就绪的,并且根本没有严格定义,但是从接口的频谱可以将所有内容更改为枚举,其中将所有内容都固定为只读文件。


7

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

抽象类是快捷方式。是否有PetBase的所有派生产品共享的东西,您只需编写一次即可完成?如果是,那么该是抽象类的时候了。

抽象类也是有限制的。尽管它们为您提供了生成子对象的捷径,但是任何给定的对象只能实现一个抽象类。很多时候,我发现这是Abstract类的局限性,这就是为什么我使用大量接口的原因。

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

是的,对于简单的示例,我的示例过于复杂。但这是接口和抽象类如何协同工作的重要内容。


7

资料来源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类)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方法的行为。

摘要:

因此,我的主要经验法则是当您要为类层次结构或/和要使用的类或类型实现“默认”功能时使用抽象类,例如,“贵宾犬”是狗的类型)。

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

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

还记得使用接口遵守接口隔离原则(ISP)时的情况。ISP指出,不应强迫任何客户端依赖其不使用的方法。因此,接口应专注于特定任务,并且通常很小(例如IDisposable,IComparable)。

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

希望这可以为某些人清除一切!

另外,如果您能想到任何更好的示例或要指出一些问题,请在下面的评论中这样做!


6

Submain .NET编码指南中很好地解释了基于接口的基类的情况:

基类与接口 接口类型是值的部分描述,可能由许多对象类型支持。尽可能使用基类而不是接口。从版本控制的角度来看,类比接口更灵活。有了一个类,您可以发布1.0版,然后在2.0版中向该类添加一个新方法。只要方法不是抽象的,任何现有的派生类将继续保持不变的功能。

因为接口不支持实现继承,所以适用于类的模式不适用于接口。向接口添加方法等同于向基类添加抽象方法。实现该接口的任何类都会中断,因为该类未实现new方法。接口在以下情况下适用:

  1. 几个不相关的类希望支持该协议。
  2. 这些类已经建立了基类(例如,一些是用户界面(UI)控件,有些是XML Web服务)。
  3. 聚合是不适当或不可行的。在所有其他情况下,类继承是一个更好的模型。

我觉得这个答案应该引起更多关注。在这里,它与许多答案背道而驰。我不会说我完全同意,但是这里有很多要点。
kayleeFrye_onDeck 2015年

5

一个重要的区别是您只能继承一个基类,但是可以实现许多接口。因此,仅在绝对确定不需要继承其他基类的情况下,才希望使用基类。此外,如果发现接口越来越大,则应开始将其分解为几个定义独立功能的逻辑部分,因为不存在类不能全部实现的规则(或者可以定义一个不同的类)。只继承它们以对它们进行分组的接口)。


4

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

为了进一步建立在这个特定问题上经常使用的示例的基础上,有很多可修饰的东西-女友,汽车,模糊的毯子...-所以我可能有一个可修饰的类提供了这种常见的行为,并且各个类都继承了从中。

但是,可宠物不是这些对象中任何一个的本质的一部分。有大大说比较重要的概念其性质至关重要-女友是一个人,车是陆上交通工具,猫是哺乳动物...

应该先将行为分配给接口(包括类的默认接口),然后将行为提升为基类,前提是它们是(a)作为较大类的子集的一大类类所共有的-在相同的意义上, “猫”和“人”是“哺乳动物”的子集。

问题是,在您比我一开始对面向对象的设计有更好的理解之后,通常您会自动进行此操作而无需考虑它。因此,“编码到接口,而不是抽象类”语句的基本事实变得如此显而易见,以至于很难相信任何人都愿意说出来-并开始尝试将其他含义读入其中。

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


一个纯抽象类可以为方法提供默认行为。当您的具体类都将共享通用的方法时,这对于重复实现是多余的,这是很有用的。
亚当·休斯

4

优先于抽象类的接口

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

  • 接口更加灵活,因为一个类可以实现多个接口。由于Java没有多重继承,因此使用抽象类可防止您的用户使用任何其他类层次结构。通常,在没有默认实现或状态时,首选接口。Java集合为此提供了很好的示例(Map,Set等)。
  • 抽象类的优点是可以实现更好的前向兼容性。客户端一旦使用了接口,就无法更改它。如果他们使用抽象类,您仍然可以在不破坏现有代码的情况下添加行为。如果需要考虑兼容性,请考虑使用抽象类。
  • 即使您确实具有默认的实现或内部状态,也可以 考虑提供一个接口及其抽象实现。这将为客户提供帮助,但如果需要,他们仍然可以给予他们更大的自由度[1]。
    当然,已经在其他地方详细讨论了该主题[2,3]。

[1]当然,它添加了更多的代码,但是如果简洁是您的首要考虑,那么您可能首先应该避免使用Java!

[2] Joshua Bloch,《有效的Java》,第16-18条。

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


3

以前有关将抽象类用于常见实现的评论肯定是正确的。我还没有提到的一个好处是,使用接口可以更轻松地实现模拟对象以进行单元测试。如Jason Cohen所述定义IPet和PetBase,使您能够轻松模拟不同的数据条件,而无需物理数据库的开销(直到您决定是时候测试真实的东西了)。


3

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

公共继承在OOD中被过度使用,并且表达的内容超出了大多数开发人员意识到或愿意实践的范围。参见李斯科夫可替代性原则

简而言之,如果A“是a” B,则对于它公开的每种方法,A所要求的不超过B且所传递的不少于B。


3

要记住的另一种选择是使用“具有”关系,也就是“根据”或“组成”实现。有时候,与使用“是”继承相比,这是一种结构更清晰,更灵活的方法。

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

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); }
}

是的,此示例表明,以这种方式执行操作涉及很多代码重复并且缺乏优雅。但是,也应该意识到,这有助于使Dog and Cat与Pet类脱钩(因为Dog and Cat无法访问Pet的私有成员),并且为Dog and Cat继承从其他事物继承的空间- -可能是哺乳动物类。

当不需要私人访问并且您不需要使用通用Pet引用/指针来引用Dog和Cat时,最好使用组合。接口为您提供了通用的参考功能,可以帮助减少代码的冗长性,但是当它们的组织性很差时,它们也可以使事情变得模糊。当您需要私有成员访问时,继承很有用,并且在使用继承时,您将致力于将Dog和Cat类与Pet类高度结合,这是一笔不小的费用。

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


3

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

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

类提供了一个实现,它们可以声明实现了零个,一个或多个接口。如果要继承一个,则约定是用“ Base”作为类名的前缀。

基类抽象基类(ABC)之间有区别。ABC将接口和实现混合在一起。计算机编程之外的抽象含义是“摘要”,即“抽象==接口”。一个抽象基类然后可以描述输入接口,以及其旨在被继承了一个空的,部分或完全实现。

关于何时使用接口抽象基类以及仅使用类的观点将根据您所开发的内容以及所使用的语言而发生巨大的变化。接口通常仅与静态类型的语言(例如Java或C#)相关联,但是动态类型语言也可以具有接口抽象基类。在Python例如,区分明确一类,其声明它之间实现一个接口,和一个对象,它是一个实例的类,并且被认为提供接口。在动态语言中,两个都是同一实例的对象可能会声明它们提供完全不同的接口。在Python中,这仅适用于对象属性,而方法是类的所有对象之间的共享状态。但是,在Ruby中,对象可以具有按实例的方法,因此同一类的两个对象之间的接口可能会发生程序员所需的变化(但是,Ruby没有任何明确的声明接口的方法)。

在动态语言中,通常是通过隐式假定对象的接口,方法是对对象进行内省并询问其提供了什么方法(在跳转之前先看一下),或者最好是简单地尝试在对象上使用所需的接口并捕获异常(如果该对象是对象)不提供该界面要求宽恕比允许权限容易)。这可能导致“误报”,其中两个接口具有相同的方法名称,但在语义上不同。但是,需要权衡的是您的代码更加灵活,因为您无需预先指定太多就可以预期代码的所有可能用途。


2

这取决于您的要求。如果IPet很简单,我宁愿实现它。否则,如果PetBase实现了大量功能,而您不想重复,那么就可以使用它。

实现基类的不利之处是override(或new)现有方法的要求。这使它们成为虚拟方法,这意味着您必须谨慎使用对象实例。

最后,.NET的单一继承使我丧命。一个简单的例子:假设您要创建一个用户控件,所以您继承了UserControl。但是,现在您无法继承PetBase。这迫使您重新组织,例如成为PetBase班级成员。


2

在需要一个之前,我通常都不会实施。与抽象类相比,我更喜欢使用接口,因为它提供了更多的灵活性。如果某些继承类中有常见的行为,我将其向上移动并创建一个抽象基类。我看不到两者都需要,因为它们本质上具有相同的用途,并且两者都有一个不好的代码味道(恕我直言),说明该解决方案已经过精心设计。


2

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


2

通过def,接口提供了与其他代码进行通信的层。默认情况下,类的所有公共属性和方法都实现隐式接口。我们还可以将接口定义为角色,当任何类需要扮演该角色时,都必须实现该接口,具体取决于实现该接口的类,使其具有不同的实现形式。因此,当您谈论接口时,您在谈论多态性;当您谈论基类时,您在谈论继承。哎呀的两个概念!


2

我发现接口>抽象>具体的模式在以下用例中有效:

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)
}

当有许多具体的类,并且您不想仅为了编程到接口而维护样板时,这种设计就很好。如果将新方法添加到接口,它将破坏所有结果类,因此您仍将获得接口方法的优点。

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



1

使用接口可以执行合同ACROSS无关类的族。例如,对于代表集合的类,您可能具有通用的访问方法,但是它们包含截然不同的数据,即,一个类可能表示查询的结果集,而另一个类可能表示图库中的图像。另外,您可以实现多个接口,从而允许您混合(并表示)该类的功能。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.