接口与基类

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

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

如果我有一个狗和猫类。为什么我想实现IPet而不是PetBase?我可以理解为ISheds或IBarks(IMakesNoise?)提供接口,因为这些可以逐个宠物放置,但我不明白哪个用于通用Pet。

185686 次浏览

现代风格是定义IPet PetBase。

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

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

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

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

一个重要的区别是,你只能继承一个基类,但你可以实现许多接口。因此,你只想在绝对肯定的情况下使用一个基类,这样你就不需要同时继承不同的基类。此外,如果你发现你的接口越来越大,那么你应该开始寻求将其分解成几个定义独立功能的逻辑片段,因为没有规则你的类不能实现它们所有(或者你可以定义一个不同的接口来继承它们以对它们进行分组)。

这取决于你的需求。如果IPet足够简单,我宁愿实现它。否则,如果PetBase实现了大量你不想复制的功能,那就去做吧。

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

最后,. NET的单一继承杀了我。一个简单的例子:假设你正在制作一个用户控件,所以你继承了UserControl。但是,现在你被锁定在继承PetBase之外。这迫使你重新组织,例如创建一个PetBase类成员,而不是。

在需要之前,我通常都不会实现这两个基类。相对于抽象类,我更喜欢接口,因为这样能提供更大的灵活性。如果在某些继承类中存在共同行为,我会将其向上移动并做一个抽象基类。我认为没有必要同时使用两者,因为它们本质上服务于相同的目的,并且两者兼而有之是一种糟糕的代码气味,表明解决方案被过度设计了。

乔希·布洛赫在有效Java2D中说:

与抽象类相比,更喜欢接口

一些要点:

  • 可以很容易地改造现有类以实现新的接口。您所要做的就是添加所需的方法,如果他们还没有存在并添加一个implements子句类声明。

  • 接口是定义混合的理想选择。粗略地说,aMixin是一个类可以除其“主要”之外实施键入“以声明它提供一些可选的行为。例如,可比较的是一个混合接口允许一个类声明它的实例是针对其他可相互比较的对象。

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

  • 接口可实现安全、强大的功能增强通过包装类习语。如果您使用抽象类来定义类型,您留下想要添加的程序员功能别无选择,但来使用继承。

此外,你可以结合美德接口和抽象类提供抽象的骨骼与每个实现类一起使用您导出的重要接口。

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

附言:买这本书吧,详细得多。

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

IPet/PetBase风格假设您有多个实现,从而增加了PetBase的价值,因为它简化了实现。然而,如果您有相反的或两者的混合,您有多个客户端,在接口的使用中提供类帮助可以通过更容易地使用接口来降低成本。

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

抽象类是快捷方式。是否有PetBase的所有派生都共享的东西,您可以编写一次代码并完成?如果是,那么是时候使用抽象类了。

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

抽象类可能包含多个接口。您的PetBase抽象类可能实现IPet(宠物有所有者)和IDig的管理(宠物吃东西,或者至少它们应该吃东西)。然而,PetBase可能不会实现IMammal,因为不是所有的宠物都是哺乳动物,也不是所有的哺乳动物都是宠物。您可以添加一个扩展PetBase并添加IMammal的MammalPetBase。Fish Base可以有PetBase并添加IFish。IFish将有ISwim和IUnderwater Breather作为接口。

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

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

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

public abstract class Mammal

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

  • 信息流
  • Mate

所有这些都是在任一物种之间具有或多或少相同实现的行为。要定义它,你将有:

public class Dog : Mammalpublic class Cat : Mammal

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

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

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

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

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

上述契约的实现在猫和狗之间不会相同;将它们的实现放在抽象类中进行继承将是一个坏主意。

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

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

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

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

接口

  • 大多数语言允许您实现多个接口
  • 修改接口是一个重大的更改。所有的实现都需要重新编译/修改。
  • 所有成员都是公共的。实现必须实现所有成员。
  • 接口有助于解耦。您可以使用模拟框架来模拟接口背后的任何东西
  • 接口通常表示一种行为
  • 接口实现相互解耦/隔离

基类

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

这是非常特定于. NET的,但是框架设计指南这本书认为,一般来说,类在不断发展的框架中提供了更大的灵活性。一旦一个接口发布,你就没有机会在不破坏使用该接口的代码的情况下更改它。然而,对于一个类,你可以修改它,而不会破坏链接到它的代码。只要你做出正确的修改,包括添加新功能,你就能够扩展和发展你的代码。

Krzysztof Cwalina在第81页说:

在. NET Framework的三个版本的开发过程中,我和我们团队中相当多的开发人员讨论过这个指南。他们中的许多人,包括那些最初不同意指南的人,都说他们后悔发布了一些API作为接口。我甚至没有听说过有人后悔发布了一个类。

话虽如此,接口肯定有一席之地。作为一般准则,如果没有其他任何东西,总是提供接口的抽象基类实现作为实现接口的方法的示例。在最好的情况下,基类将节省大量工作。

基类的继承者应该有一个“is a”关系。接口表示一个“实现”关系。所以只有当你的继承者将保持是关系时才使用基类。

运用你自己的判断力,聪明一点。不要总是照别人(比如我)说的去做。你会听到“更喜欢接口而不是抽象类”,但这真的取决于。这取决于类是什么。

在上面提到的情况下,我们有一个对象的层次结构,接口是一个好主意。接口有助于处理这些对象的集合,它也有助于实现使用层次结构中任何对象的服务。你只需定义一个使用层次结构中对象的契约。

另一方面,当你实现一堆共享一个公共功能的服务时,你可以将公共功能分离到一个完整的单独类中,或者你可以将它移动到一个公共基类中并使其抽象,这样就没有人可以实例化基类。

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

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

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

因此,您可能最终得到的不是IPet或PetBase,而是一个具有IFurBe这辆参数的Pet。IFurBe这辆参数是由PetFactory的CreateDog()方法设置的。正是这个参数被调用来用于棚()方法。

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

即使在多重继承语言中,我也推荐这种模式。

接口有一个明显的优点,那就是可以对类进行“热插拔”。将一个类从一个父类更改为另一个父类通常会导致大量的工作,但接口通常可以被删除和更改,而不会对实现类产生太大影响。这在你有几个狭窄的行为集的情况下特别有用,你“可能”希望一个类实现。

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

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

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

胡安,

我喜欢将接口视为描述类的一种方式。一个特定的狗品种类,比如YorkshireTerrier,可能是父狗类的后代,但它也实现了IFurry、IStubby和IyippieDog。所以类定义了类是什么,但接口告诉我们关于它的事情。

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

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

这个想法和每个类都应该有接口并且你应该根据这个接口编码的想法是背道而驰的。那很好,但是你最终会得到很多类的一对一接口,这会让事情变得混乱。我理解这个想法并不需要任何成本,现在你可以轻松地交换东西了。然而,我发现我很少这样做。大多数时候我只是在原地修改现有的类,如果该类的公共接口需要更改,我会遇到完全相同的问题,除了我现在必须在两个地方进行更改。

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

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

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

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

它们甚至需要连接在一起吗?仅仅知道它们都是IPettable就足够了吗?

我经常会在确实只需要一个类的时候,有一种渴望,想要推导出一个完整的类层次结构。我推导这个类是因为预计有一天我可能会需要它,但通常我都不会这样做。即使我需要它,我通常也发现我必须做很多事情来修复它。这是因为我创建的第一个类不是Dog,我没有那么幸运,它是鸭嘴兽。现在我的整个类层次结构都是基于这个奇怪的case,并且我有很多浪费的代码。

你可能会在某个时候发现并非所有的Cats都是IPettable(比如那个无毛的)。现在你可以将该接口移动到所有适合的派生类。你会发现一个不那么破坏性的变化,突然之间Cats不再派生自PettableBase。

之前关于使用抽象类进行通用实现的评论绝对是对的。我还没有看到提到的一个好处是,使用接口可以更容易地实现用于单元测试的模拟对象。正如Jason Cohen所描述的定义IPet和PetBase使你能够轻松模拟不同的数据条件,而不会产生物理数据库的开销(直到你决定测试真实的东西)。

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

公共继承在OOD中被过度使用,并且比大多数开发人员意识到或愿意实现的要多得多。参见Liskov替代性原理

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

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

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

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

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

继承(基类)表示“is-a”关系。例如,一只狗或一只猫“is-a”宠物。这种关系始终代表类的(单个)目的(与"单一责任原则"结合)。

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

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

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

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

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

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

在动态语言中,对象的接口通常是隐式假设的。要么通过内省对象并询问它提供了什么方法(三思而后行),要么最好只是尝试在对象上使用所需的接口,并在对象不提供接口请求原谅比请求许可容易)时捕获异常。这可能导致“误报”,两个接口具有相同的方法名称,但语义不同。然而,权衡是你的代码更灵活,因为你不需要预先过度指定来预测代码的所有可能用途。

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

  • 基类=对象继承。
  • 接口=函数继承。

欢呼

这在Java世界文章中得到了很好的解释。

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

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

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

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


接口vs.抽象类

选择接口和抽象类不是一个非此即彼的命题。如果你需要改变你的设计,让它成为一个接口。但是,你可能有提供一些默认行为的抽象类。抽象类是应用程序框架内的优秀候选者。

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

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

类vs.接口

有人说你应该根据接口来定义所有的类,但我认为推荐似乎有点极端。当我看到我的设计中的某些东西会经常改变时,我会使用接口。

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

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

这是使用接口的绝佳地方。我们使用了Strategy模式,它清楚地表明了设计中将要改变的地方。因此,你应该将策略定义为接口。当你希望对象具有某种类型时,通常应该支持接口而不是继承;在这种情况下,MediaStrategy。依赖继承来获得类型身份是危险的;它会将你锁定在特定的继承层次结构中。Java不允许多重继承,所以你不能扩展给你有用实现或更多类型身份的东西。

另一个要记住的选项是使用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); }}

是的,这个例子表明以这种方式做事会有很多代码重复和缺乏优雅。但我们也应该意识到,这有助于保持Dog和Cat与Pet类的解耦(因为Dog和Cat无法访问Pet的私有成员),并为Dog和Cat从其他东西(可能是Mammal类)继承留下了空间。

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

在继承、组合和接口之间,没有一种方法是永远正确的,考虑如何协调使用这三个选项会有所帮助。在这三个选项中,继承通常是最不应该使用的选项。

接口上的基类的情况在Submain. NET编码指南中解释得很好:

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

因为接口不支持实现继承,该适用于类的模式确实不适用于接口。添加接口的方法是等效的向基础添加抽象方法类;任何实现接口将中断,因为类不实现新方法。接口适用于以下情况:

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

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

为了进一步构建在这个特定问题中大量使用的示例,有很多的事物是petable的-女朋友,汽车,模糊毯子…-所以我可能有一个Petable类提供这种常见行为,以及从它继承的各种类。

然而,可抚摸并不是这些物体的本质的一部分。还有一些更重要的概念对它们的本质至关重要——女朋友是人,汽车是陆地车辆,猫是哺乳动物……

行为应该首先分配给接口(包括类的默认接口),并且只有当它们是(a)作为较大类的子集的一大群类共有时才提升为基类-就像“cat”和“man”是“哺乳动物”的子集一样。

问题是,在你比我一开始更了解面向对象设计之后,你通常会自动这样做,甚至不用考虑它。因此,“接口代码,而不是抽象类”这句话的赤裸裸的真相变得如此明显,以至于你很难相信有人会费心去说它——并开始试图从中解读其他含义。

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

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

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

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

我有个粗略的经验法则

功能性:可能在所有部分都不同:接口。

数据和功能,部分将基本相同,部分不同:抽象类。

数据和功能,实际工作,如果扩展只有轻微的变化:普通(混凝土)类

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

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

这是非常粗略和准备好的,根本没有严格定义,但是有一个从接口到所有内容都打算更改为枚举的范围,其中所有内容都被固定,有点像只读文件。

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

接口类型与抽象基类

改编自Pro C#5.0和. NET 4.5 Framework的书。

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

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#不支持类的多重继承。因此,如果你想创建一个MiniVan,它既是汽车又是克隆类型,你无法这样做:

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

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

public interface ICloneable{object Clone();}

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

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

C#是一种在过去14年中成熟和发展的美妙语言。这对我们开发人员来说很棒,因为成熟的语言为我们提供了大量可供我们使用的语言功能。

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

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

这一切都很好,并且提供了很好的代码重用,并遵守软件开发的DRY(不要重复自己)原则。当您有“is a”关系时,抽象类非常适合使用。

例如:金毛猎犬“是一种”类型的狗。贵宾犬也是如此。它们都可以吠叫,就像所有的狗一样。然而,你可能想声明贵宾犬公园与“默认”狗叫有很大不同。因此,你可以实现以下内容:

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 classpublic 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而不是特例实现时调用基类实现。GoldenRetriver、Boxer、Lab等类都可以免费继承“default”(bass类)Bark,仅仅因为它们实现了Dog抽象类。

但我相信你已经知道了。

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

这到底是什么意思呢?举个例子:人不是鸭……鸭也不是人。很明显。然而,鸭和人都有游泳的“能力”(因为人在一年级就通过了游泳课 :) ). 此外,由于鸭不是人,反之亦然,这不是“是”的关系,而是“有能力”的关系,我们可以用一个接口来说明:

// Create ISwimable interfacepublic interface ISwimable{public void Swim();}
// Have Human implement ISwimable Interfacepublic class Human : ISwimable
public void Swim(){//Human's implementation of SwimConsole.WriteLine("I'm a human swimming!");}
// Have Duck implement ISwimable interfacepublic class Duck: ISwimable{public void Swim(){// Duck's implementation of SwimConsole.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,这样你就不必编写多个调用对象的方法来预制相同的核心函数(ShowHowHuman Swims(人类)、ShowHowDuckSwims(鸭子)等)。

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

总结:

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

另一方面,当你没有“是”关系,但有共享“能力”做某事或拥有某事的类型时,使用接口(例如,鸭子“不是”人。然而,鸭子和人类共享“能力”游泳)。

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

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

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

希望这能为一些人澄清一些事情!

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

列出你的对象必须是、拥有或做的事情,以及你的对象可以(或可能)是、拥有或做的事情。必须表示你的基本类型,可以表示你的接口。

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

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

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

你应该使用接口如果

  1. 你有纯abstract方法,没有非抽象方法
  2. 您没有non abstract方法的默认实现(除了Java8语言,其中接口方法提供默认实现)
  3. 如果您使用的是Java8,现在接口将为一些非抽象方法提供默认实现。这将使interfaceabstract类更有用。

查看此SE问题以了解更多详细信息。

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

是的。它更好更清晰。即使你有一个带有一些抽象方法的基类,让我们通过接口扩展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?)提供接口,因为这些可以逐个宠物放置,但我不明白哪个用于通用Pet。

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

 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. 您可以通过接口为基类提供不变性。请看这篇文章

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

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

与抽象类相比,更喜欢接口

基本原理需要考虑的要点[这里已经提到的两个]是:

  • 接口更加灵活,因为一个类可以实现多个由于Java没有多重继承,因此使用抽象类阻止您的用户使用任何其他类层次结构。<强>一般来说,当没有默认值时,更喜欢接口Java集合提供了很好的例子this(Map、Set等)。
  • 抽象类的优点是允许更好的转发兼容性。一旦客户端使用接口,您就不能更改它;如果他们使用抽象类,您仍然可以添加行为而无需破坏现有代码。<强>如果兼容性是一个问题,请考虑使用抽象类。
  • 即使你有默认实现或内部状态,考虑提供一个接口和它的抽象实现这将有助于客户,但仍然允许他们更大的自由,如果[1]
    当然,这个问题已经讨论了很久其他地方[2,3]。

[1]当然,它增加了更多的代码,但如果简洁是你主要关心的问题,你可能应该首先避免Java!

[2]约书亚·布洛赫,有效Java,第16-18项。

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

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

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{
@overridepublic walksUpright(){throw new NotSupportedException("Walks Upright not implemented");}
@overridepublic hasNipples(){return true}
@overridepublic hasHair(){return true}

然后具体的类仅仅定义它们直立行走。

public class Ape extends Mammal(){
@overridepublic walksUpright(return true)}
public class Catextends Mammal(){
@overridepublic walksUpright(return false)}

当有很多具体类时,这种设计很好,你不想仅仅为了对接口进行编程而维护样板。如果将新方法添加到接口中,它会破坏所有生成的类,所以你仍然获得了接口方法的优势。

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