何时使用接口代替抽象类,反之亦然?


427

这可能是一个通用的OOP问题。我想根据接口和抽象类的用法进行通用比较。

什么时候要使用接口,什么时候要使用抽象类



1
除了下面的答案之外,这是一个不错的简短列表,其中列出了您可能希望使用界面的地方以及您可能不想使用的地方:何时使用界面:msdn.microsoft.com/zh-cn/library/3b5b8ezk(v=vs.80) .aspx
安东尼

当您不确定要做什么时,请使用摘要。如果需要,请使用界面。
维吾尔族Gümüşhan

我很想知道有多少不在Microsoft工作的开发人员在日常开发中定义和使用接口。
user1451111

Answers:


431

我写了一篇有关的文章:

抽象类和接口

总结:

当我们谈论抽象类时,我们正在定义对象类型的特征。指定什么是对象

当我们谈论一个接口并定义我们承诺提供的功能时,我们在谈论建立关于对象可以做什么的契约


114
这非常有帮助:Interfaces do not express something like "a Doberman is a type of dog and every dog can walk" but more like "this thing can walk"。谢谢
aexl 2014年


以下是Alex的解释:仅描述实现的功能与描述存储的状态之间的区别,似乎是对这个问题的更好回答,因为这些区别不只是哲学上的。
邓肯·马拉索克

1
Duncan Malashock,并非如此。豪尔赫的答案是更好的答案。亚历克斯的答案集中在力学上,而豪尔赫的答案更多地放在语义上。
Nazar Merza

8
我喜欢您指定的答案之前的声明:Use abstract classes and inheritance if you can make the statement “A is a B”. Use interfaces if you can make the statement “A is capable of [doing] as”
S1r-Lanzelot

433

抽象类可以具有共享状态或功能。接口仅是提供状态或功能的承诺。一个好的抽象类将减少必须重写的代码量,因为它的功能或状态可以共享。该接口没有要共享的已定义信息


65
对我来说,这是最好的答案,这是一个遗憾,没有得到较高的投票。是的,这两个概念之间在哲学上存在差异,但根本之处在于抽象类确保所有后代共享功能/状态,而接口仅确保公共键。
drharris 2011年

3
例如,抽象基类用于模板方法设计模式,而接口则用于策略设计模式。
2014年

1
我认为豪尔赫(Jorge)的摘要解释了两者的存在的主要但背后的原因,而亚历克斯(Alex)的回答是结果的差异。我希望可以将两者都标记为正确的答案,但我仍然更喜欢Jorge的答案。
基兰坦

这里例子的代码。
shaijut

对我来说,“一个好的抽象类将减少必须重写的代码量,因为它的功能或状态可以共享。” 陈述是答案的核心。
Div Tiwari

82

就个人而言,我几乎不需要编写抽象类。

大多数时候,我看到抽象类被(错误)使用,这是因为抽象类的作者正在使用“模板方法”模式。

“ Template method”的问题在于它几乎总是重入-“ derived”类不仅知道其正在实现的基类的“ abstract”方法,而且还知道该基类的public方法,即使大多数情况下也不需要调用它们。

(过于简化)示例:

abstract class QuickSorter
{
    public void Sort(object[] items)
    {
        // implementation code that somewhere along the way calls:
        bool less = compare(x,y);
        // ... more implementation code
    }
    abstract bool compare(object lhs, object rhs);
}

因此,在这里,此类的作者编写了一种通用算法,并希望人们通过提供自己的“挂钩”(在本例中为“比较”方法)来“专门化”它来使用它。

所以预期的用法是这样的:

class NameSorter : QuickSorter
{
    public bool compare(object lhs, object rhs)
    {
        // etc.
    }
}

问题在于您将两个概念过度结合在一起:

  1. 比较两个项目的方式(哪个项目应该先行)
  2. 对项目进行排序的方法(即,快速排序与合并排序等)

在上面的代码中,理论上,“比较”方法的作者可以 重新进入超类“排序”方法中……尽管实际上他们从不愿意或不需要这样做。

您为这种不必要的耦合付出的代价是很难更改超类,并且在大多数OO语言中,不可能在运行时进行更改。

另一种方法是改为使用“策略”设计模式:

interface IComparator
{
    bool compare(object lhs, object rhs);
}

class QuickSorter
{
    private readonly IComparator comparator;
    public QuickSorter(IComparator comparator)
    {
        this.comparator = comparator;
    }

    public void Sort(object[] items)
    {
        // usual code but call comparator.Compare();
    }
}

class NameComparator : IComparator
{
    bool compare(object lhs, object rhs)
    {
        // same code as before;
    }
}

现在注意:我们所拥有的只是接口,以及这些接口的具体实现。实际上,您实际上不需要其他任何东西即可进行高级OO设计。

为了“隐藏”我们已经通过使用“ QuickSort”类和“ NameComparator”实现“名称排序”的事实,我们仍然可以在某处编写一个工厂方法:

ISorter CreateNameSorter()
{
    return new QuickSorter(new NameComparator());
}

任何你有一个抽象类,你可以这样做的时候......即使当时是基类和派生类之间的自然重入的关系,它通常支付,使他们明确。

最后一个想法:上面我们所做的就是通过使用“ QuickSort”函数和“ NameComparison”函数来“组成”一个“ NameSorting”函数……在函数式编程语言中,这种编程风格变得更加自然,用更少的代码。


5
仅仅因为您可以使用Abstract类或Template Method模式并不意味着您需要避免使用它们。在本示例中,策略模式是针对不同情况的不同模式,但是在许多示例中,模板模式比策略更合适。
豪尔赫·科尔多瓦2009年

5
好吧,以我的经验,我从来没有遇到过它们(模板方法更可取的情况)……或者很少。这就是所有“抽象”的内容-对“模板方法”设计模式的语言支持。
Paul Hollingsworth,

好的,我曾经在一个专家系统中使用过一次,该过程类似于:获得1. FillTheParameters,2.在它们之间建立矢量乘积,3.对于每一对计算结果,4.联接结果,其中步骤1和3在基类中委托,在其中实现2和4。
豪尔赫·科尔多瓦2009年

10
我发现几乎所有对抽象类的使用都很难理解。(对于我来说)用相互沟通而不是继承关系的盒子来思考更容易(但对我而言)……但我也同意,当前的面向对象语言会强制执行太多样板工作……功能性将是遍及面向对象的方法
Paul Hollingsworth

4
滥用的例子很简单。它很少归结为诸如compare之类的漂亮剥离功能。更常见的情况是存在一些默认功能,派生类可以替换扩展这些功能(在后一种情况下,调用基类函数是完全有效的)。在您的示例中,没有默认功能,因此抽象类的使用没有理由。
SomeWittyUsername

41

如果您将Java视为OOP语言,

对于Java 8启动,“ 接口不提供方法实现 ”不再有效。现在,java在接口中为默认方法提供了实现。

简单来说,我想使用

接口:由多个不相关的对象实施合同。它提供“ HAS A ”功能。

抽象类:在多个相关对象之间实现相同或不同的行为。它建立“ IS A ”关系。

Oracle 网站提供了interfaceabstract类之间的主要区别。

如果满足以下条件,请考虑使用抽象类

  1. 您想在几个紧密相关的类之间共享代码。
  2. 您期望扩展您的抽象类的类具有许多通用方法或字段,或者需要除public(例如protected和private)之外的访问修饰符。
  3. 您要声明非静态或非最终字段。

如果满足以下条件,请考虑使用接口

  1. 您期望不相关的类将实现您的接口。例如,许多不相关的对象可以实现Serializable接口。
  2. 您想指定特定数据类型的行为,但不关心谁实现了它的行为。
  3. 您想利用类型的多重继承。

例:

抽象类(IS关系)

读者是一个抽象类。

BufferedReader是一个Reader

FileReader是一个Reader

FileReader并且BufferedReader用于一般目的:读取数据,并且它们通过Reader类是相关的。

接口(具有HAS A功能)

可序列化是一个接口。

假设您的应用程序中有两个类,它们正在实现Serializable接口

Employee implements Serializable

Game implements Serializable

在这里,您无法通过和Serializable之间的接口建立任何关系,这是出于不同的目的。两者都能够序列化状态,并且比较到此结束。EmployeeGame

看看这些帖子:

我应该如何解释接口和抽象类之间的区别?


40

好的,我本人只是“抱怨”了这-用外行的话来说(如果我错了,请随时纠正我)-我知道这个话题太过老了,但是有一天别人可能会偶然发现...

抽象类使您可以创建一个蓝图,并允许您另外构造所有后代要拥有的CONSTRUCT(实现)属性和方法。

另一方面,接口仅允许您声明要使具有给定名称的属性和/或方法存在于实现该接口的所有类中,但没有指定应如何实现。同样,一个类可以实现MANY接口,但只能扩展一个Abstract类。接口更像是一种高级架构工具(如果您开始掌握设计模式,它将变得更加清晰)-摘要在两个阵营中都有一席之地,并且也可以执行一些肮脏的工作。

为什么要使用一个?前者允许更具体的后代的定义-后者允许更大的多态性。最后一点对最终用户/编码器很重要,他们可以利用此信息以各种组合/形状来实现AP I(接口)以适应他们的需求。

我认为这对我来说是“灯泡”时刻-少考虑作者的角度,而不要考虑链中后来为项目添加实现或扩展 API 的任何编码人员的接口。


以此为基础:实现接口的对象采用TYPE。这很关键。因此,您可以将接口的不同变体传递给类,但可以使用接口的类型名称引用它们(及其方法)。因此,您无需进行切换或if / else循环。试试这个主题的本教程-它演示了通过策略模式使用界面的方法。 phpfreaks.com/tutorial/design-patterns---strategy-and-bridge/…–
sunwukung

我完全同意您的灯泡时刻:“ API(接口)具有多种组合/形状,可以满足他们的需求”!非常非常重要的一点。
特雷弗·博伊德·史密斯

39

我的两分钱:

接口基本上定义了一个契约,任何实现类都必须遵守(实现接口成员)。它不包含任何代码。

另一方面,抽象类可以包含代码,并且可能有一些标记为抽象的方法必须由继承类实现。

我使用抽象类的罕见情况是,当我具有某些默认功能时,继承类可能对覆盖某些专门类继承的抽象基类(例如抽象基类)不感兴趣。

示例(一个非常基本的一个!):考虑一个基类叫做客户具有抽象方法一样CalculatePayment()CalculateRewardPoints()和一些非抽象的方法,比如GetName()SavePaymentDetails()

RegularCustomer 和这样的专用类GoldCustomer将从Customer基类继承并实现自己的CalculatePayment()CalculateRewardPoints()方法逻辑,但重新使用GetName()SavePaymentDetails()方法。

您可以向抽象类(不是抽象方法)添加更多功能,而不会影响使用旧版本的子类。尽管向接口添加方法会影响所有实现该接口的类,因为它们现在需要实现新添加的接口成员。

具有所有抽象成员的抽象类将类似于接口。


1
为“ +1”表示“您可以向抽象类(非抽象方法)添加更多功能,而不会影响正在使用旧版本的子类。而向接口中添加方法会影响所有实现它的类,因为它们现在需要实现新添加的界面成员。”
Div Tiwari

接口可以具有“默认”方法,因此在接口中没有方法隐含是错误的主意。这里的“父母与孩子” IS-A关系是关键。此外,“共享属性”还是“共享属性”。例如,狗是动物。但是狗也可以“走路”
ha9u63ar

30

如果您有清晰的概念,那么什么时候做是一件非常简单的事情。

可以派生抽象类,而可以实现接口。两者之间有些区别。当您派生一个Abstract类时,派生类与基类之间的关系是“是”关系。例如,狗是动物,绵羊是动物,这意味着派生类正在继承基类的某些属性。

对于接口的实现,关系是“可以”。例如,狗可以是间谍狗。狗可以是马戏团狗。狗可以是赛狗。这意味着您实现了某些获取东西的方法。

我希望我清楚。


11

1.如果创建的东西可以为不相关的类提供通用功能,请使用接口。

2.如果要为层次结构中紧密相关的对象创建对象,请使用抽象类。



7

我认为最简洁的表达方式如下:

共享属性=>抽象类。
共享功能=>接口。

更简单地说...

抽象类示例:

public abstract class BaseAnimal
{
    public int NumberOfLegs { get; set; }

    protected BaseAnimal(int numberOfLegs)
    {
        NumberOfLegs = numberOfLegs;
    }
}

public class Dog : BaseAnimal
{
    public Dog() : base(4) { }
}

public class Human : BaseAnimal 
{
    public Human() : base(2) { }
}

由于动物具有共享的属性(在这种情况下为支腿数),因此使包含该共享属性的抽象类变得有意义。这也使我们能够编写在该属性上运行的通用代码。例如:

public static int CountAllLegs(List<BaseAnimal> animals)
{
    int legCount = 0;
    foreach (BaseAnimal animal in animals)
    {
        legCount += animal.NumberOfLegs;
    }
    return legCount;
}

接口示例:

public interface IMakeSound
{
    void MakeSound();
}

public class Car : IMakeSound
{
    public void MakeSound() => Console.WriteLine("Vroom!");
}

public class Vuvuzela : IMakeSound
{
    public void MakeSound() => Console.WriteLine("VZZZZZZZZZZZZZ!");        
}

请注意,Vuvuzelas和Cars是完全不同的东西,但是它们具有共享的功能:发出声音。因此,这里的接口是有意义的。此外,它将允许程序员在一个公共界面下将声音组合在一起IMakeSound。通过这种设计,您可以编写以下代码:

List<IMakeSound> soundMakers = new List<ImakeSound>();
soundMakers.Add(new Car());
soundMakers.Add(new Vuvuzela());
soundMakers.Add(new Car());
soundMakers.Add(new Vuvuzela());
soundMakers.Add(new Vuvuzela());

foreach (IMakeSound soundMaker in soundMakers)
{
    soundMaker.MakeSound();
}

你能说出什么吗?

最后,您可以将两者结合起来。

组合示例:

public interface IMakeSound
{
    void MakeSound();
}

public abstract class BaseAnimal : IMakeSound
{
    public int NumberOfLegs { get; set; }

    protected BaseAnimal(int numberOfLegs)
    {
        NumberOfLegs = numberOfLegs;
    }

    public abstract void MakeSound();
}

public class Cat : BaseAnimal
{
    public Cat() : base(4) { }

    public override void MakeSound() => Console.WriteLine("Meow!");
}

public class Human : BaseAnimal 
{
    public Human() : base(2) { }

    public override void MakeSound() => Console.WriteLine("Hello, world!");
}

在这里,我们要求所有人都BaseAnimal发出声音,但我们尚不知道它的实现。在这种情况下,我们可以抽象化接口实现并将其实现委托给其子类。

最后一点,还记得在抽象类示例中我们如何能够对不同对象的共享属性进行操作,而在接口示例中我们如何能够调用不同对象的共享功能?在最后一个示例中,我们可以同时进行。


7

什么时候比接口更喜欢抽象类?

  1. 如果有人计划在程序/项目的整个生命周期中更新基类,则最好让基类成为抽象类。
  2. 如果要尝试为层次结构中密切相关的对象构建主干,则使用抽象类非常有益

什么时候比抽象类更喜欢接口?

  1. 如果不处理大规模分层框架,接口将是一个不错的选择
  2. 因为抽象类不支持多重继承(钻石问题),所以接口可以节省时间

是什么让您认为一个将近十年的问题需要一个第二十二个答案?
jonrsharpe

3
相同的思维方式使我寻找问题的简单答案。
萨蒂亚

1
FWIW,我真的很喜欢这个答案。
布伦特·里滕豪斯

6

类只能从一个基类继承,因此,如果要使用抽象类为一组类提供多态性,则它们都必须都从该类继承。抽象类也可以提供已经实现的成员。因此,您可以通过抽象类确保一定数量的相同功能,但是不能通过接口来保证。

以下是一些建议,可以帮助您决定是使用接口还是抽象类为组件提供多态性。

  • 如果您打算创建组件的多个版本,请创建一个抽象类。抽象类提供了一种简便的方式来对组件进行版本控制。通过更新基类,所有继承的类都会随着更改自动更新。另一方面,以这种方式创建的接口不能更改。如果需要新版本的接口,则必须创建一个全新的接口。
  • 如果您要创建的功能将在各种不同的对象中有用,请使用界面。抽象类应主要用于紧密相关的对象,而接口最适合为不相关的类提供通用功能。
  • 如果您正在设计小的简洁功能,请使用接口。如果要设计大型功能单元,请使用抽象类。
  • 如果要在组件的所有实现之间提供通用的实现功能,请使用抽象类。抽象类允许您部分实现您的类,而接口不包含任何成员的实现。

复制自:http :
//msdn.microsoft.com/zh-cn/library/scsyfw1d%28v=vs.71%29.aspx


UML中没有什么可以排除多个类的继承。多重继承是由一种编程语言而不是由UML确定的。例如,Java和C#不允许多类继承,因为C ++允许多类继承。
BobRodes

@BobRodes:面向对象的框架可以提供多种功能,但是不能以所有组合提供。通用多重继承排除了某些其他有用的功能组合,包括将引用直接转换为实际实例的任何父类型或由此支持的任何接口类型的能力,以及独立编译基本类型和派生类型并在运行时将其联接的能力。
超级猫

@supercat Yours很好地解释了使用多重继承导致的一些问题。但是,UML中没有任何东西可以阻止图中的多个类继承。我对上面的“类只能从一个基类中继承...”的回答是不对的。
BobRodes

@BobRodes:这个问题被标记为Java。Java包括指示的功能,因此仅限于不能产生“致命钻石”的多重继承形式(尽管实际上,它们实现默认接口实现的方式使致命钻石成为可能)。
超级猫

@supercat哦,好的。我通常不看Java标记,因此在写这篇文章时,我至少以为我在评论UML答案。无论如何,我同意你的评论。
BobRodes

3

如果以下任何一种情况适用于您的情况,请考虑使用抽象类

  1. 您想在几个紧密相关的类之间共享代码。
  2. 您期望扩展您的抽象类的类具有许多公共方法或字段,或者需要除public(例如protected和private)之外的访问修饰符。
  3. 您要声明非静态或非最终字段。这使您能够定义可以访问和修改它们所属对象的状态的方法。

如果以下任何一种情况适用于您的情况,请考虑使用接口

  1. 您期望不相关的类将实现您的接口。例如,接口Comparable和Cloneable由许多不相关的类实现。
  2. 您想指定特定数据类型的行为,但不关心谁实现了它的行为。
  3. 您想利用多重继承。

资源


2

答案因语言而异。例如,在Java中,一个类可以实现(继承)多个接口,但只能从一个抽象类继承。因此,接口可为您提供更大的灵活性。但这在C ++中是不正确的。


2

对我来说,在很多情况下我都会使用接口。但是在某些情况下,我更喜欢抽象类。

OO中的类通常是指实现。当我想将一些实现细节强加给我使用接口的子类时,我使用抽象类。

当然,抽象类不仅在强制实施中有用,而且在许多相关类之间共享某些特定细节方面也很有用。


1

如果要提供一些基本的实现,请使用抽象类。


1
谢谢塞巴斯蒂安。但是,如果我不需要基本的实现该怎么办?如果抽象类和接口唯一的区别是,它们是否会相同?为什么有区别?
Chirantan

1
因为某些语言没有接口-C ++。
jmucchiello

1

在Java中,您可以从一个(抽象)类继承来“提供”功能,并且可以实现许多接口来“确保”功能


lil的提示:如果要继承抽象类和接口,请确保抽象类实现了该接口
Andreas Niedermair 2009年

1

纯粹在继承的基础上,您将使用一个Abstract来定义清晰的后代,抽象关系(即animal-> cat)和/或需要继承虚拟或非公共属性,尤其是共享状态(Interfaces不支持) )。

您应该尽力在可能的情况下通过继承来支持组合(通过依赖项注入),并且要注意,作为契约的接口以抽象无法实现的方式支持单元测试,关注点分离和(语言变化)多重继承。


1

接口比抽象类更好的一个有趣的地方是,当您需要向一组(相关或不相关)对象添加额外的功能时。如果您不能给他们一个基础抽象类(例如,他们已经sealed或已经有一个父类),则可以给他们一个虚拟(空)接口,然后为该接口编写扩展方法。


0

拨打电话可能非常困难...

我可以给出一个指针:一个对象可以实现多个接口,而一个对象只能继承一个基类(在像c#这样的现代OO语言中,我知道C ++具有多重继承-但这不是皱眉吗?)


多重继承使Mixin得以实现,而编写良好的Mixin则轻而易举,但是很难做到,而且很难在不失灵的情况下编写。尽管通过IMO,Mixin整体上还是很酷的。
Martijn Laarman,2009年

实际上我没有,多重继承确实是我们极客之间肯定会引发争论的地方,我认为绝对没有理由拒绝投票。实际上,我赞成您的回答。
Martijn Laarman,2009年

我要说明的唯一一点是,也可以使用具有单一继承的语言来混合Mixin(C#,PHP,javascript),但是会出现低级行为或俗气的语法。我喜欢Mixin的工作原理,但是我仍然不确定是否要继承多重继承。
Martijn Laarman,2009年

这个答案更多的是语法上的差异,而不是设计上的差异。我认为他正在寻求与众不同的设计
Pramod Setlur

0

抽象类可以具有实现。

接口没有实现,只是定义了一种契约。

也可能存在一些与语言有关的差异:例如C#没有多重继承,但是可以在一个类中实现多个接口。


当您说“一种合同”时,您是说像Web服务一样吗?
Chirantan

从技术上讲,Web服务不能与接口一起使用。对于合同,我的意思是对象的用户知道该对象上存在哪些方法。例如,接口IMouse将具有Move方法以及鼠标左键和右键事件。
Gerrie Schenck,2009年

-1

基本的经验法则是:对于“名词”,请使用Abstract类;对于“动词”,请使用接口

例如:car是一个抽象类drive,我们可以使其成为一个接口。


5
这没有道理,我们也可以drive在汽车中添加功能-这是一个抽象类。
Arslan Ali
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.