我应该何时使用接口,何时使用基类?
如果我不想实际定义方法的基本实现,它应该始终是一个接口吗?
如果我有狗和猫的课。为什么我要实现IPet而不是PetBase?我可以理解为ISheds或IBarks(IMakesNoise?)提供接口,因为它们可以逐个宠物放置,但我不知道该为普通宠物使用哪个接口。
我应该何时使用接口,何时使用基类?
如果我不想实际定义方法的基本实现,它应该始终是一个接口吗?
如果我有狗和猫的课。为什么我要实现IPet而不是PetBase?我可以理解为ISheds或IBarks(IMakesNoise?)提供接口,因为它们可以逐个宠物放置,但我不知道该为普通宠物使用哪个接口。
现代风格是定义IPet和PetBase。
该接口的优点是其他代码可以使用它,而与其他可执行代码没有任何联系。完全“干净”。界面也可以混合。
但是基类对于简单的实现和通用实用程序很有用。因此,还提供一个抽象基类,以节省时间和代码。
通常,您应该喜欢接口而不是抽象类。使用抽象类的一个原因是,如果在具体类之间有共同的实现。当然,您仍然应该声明一个接口(IPet),并让一个抽象类(PetBase)实现该接口。使用小的、不同的接口,您可以使用多个来进一步提高灵活性。接口允许最大限度的灵活性和跨边界类型的可移植性。当跨越边界传递引用时,始终传递接口而不是具体类型。这允许接收端确定具体实施,并提供最大的灵活性。当以TDD/BDD方式编程时,这是绝对正确的。
“四人帮”在他们的书中说:“因为继承将子类暴露于其父类实现的细节,所以人们常说‘继承破坏了封装’。”。我相信这是真的。
一个重要的区别是,只能继承一个基类,但可以实现多个接口。因此,如果您绝对确定不需要同时继承不同的基类,那么只需要使用基类。此外,如果你发现你的接口越来越大,那么你应该开始把它分成几个逻辑块来定义独立的功能,因为没有规则规定你的类不能实现所有的功能(或者你可以定义一个不同的接口来继承所有的功能)。
这取决于您的要求。如果IPet足够简单,我更愿意实现它。否则,如果PetBase实现了大量您不想复制的功能,那么就试试看。
实现基类的缺点是需要重写(或新的)现有方法。这使它们成为虚拟方法,这意味着您必须小心如何使用对象实例。
最后,.NET的单一继承让我很头疼。一个天真的例子:假设你正在创建一个用户控件,那么你就继承了UserControl。但是,现在你被禁止继承PetBase。这迫使您重新组织,例如创建PetBase类成员。
我通常在需要之前都不会实现。与抽象类相比,我更喜欢接口,因为这提供了更多的灵活性。如果在某些继承类中有共同的行为,我会将其上移并创建一个抽象基类。我不认为两者都有必要,因为它们本质上服务于相同的目的,而且两者都有一种糟糕的代码气味(imho),解决方案被过度设计了。
Josh Bloch在《有效的Java 2d》中说道:
首选接口而非抽象类
一些要点:
可以很容易地对现有类进行修改,以实现新的界面你所要做的就是添加所需的方法(如果还没有)存在并将implements子句添加到类声明。接口是定义混合的理想选择。粗略地说mixin是类可以使用的类型除了“主要”键入“”以声明它提供一些可选行为。例如Comparable是一个mixin接口允许类声明其实例的排序依据其他相互可比较的对象。接口允许构造非分层类型框架。类型层次结构为很适合组织一些事情,但是其他的事情并不是很简单刚性层次。接口通过每类包装的习惯用法。如果您使用抽象类来定义类型留下想要添加的程序员功能,别无选择使用继承。此外,你可以结合这些优点接口和抽象类提供抽象骨架实现类导出的非平凡接口。
另一方面,接口很难发展。如果您向接口添加一个方法,它将破坏它的所有实现。
PS:买这本书。它要详细得多。
除了那些提到IPet/PetBase实现的注释之外,在某些情况下,提供访问器帮助器类可能非常有价值。
IPet/PetBase风格假设您有多个实现,从而增加了PetBase的价值,因为它简化了实现。然而,如果您有多个客户端,则提供一个类帮助来帮助使用接口,可以通过使接口更容易使用来降低成本。
接口应较小。真的很小。如果你真的要分解你的对象,那么你的接口可能只包含一些非常具体的方法和财产。
抽象类是快捷方式。PetBase的所有衍生产品都有你可以编写一次代码并完成的东西吗?如果是的话,那么是抽象类的时候了。
抽象类也是有限的。虽然它们为您提供了生成子对象的绝佳捷径,但任何给定对象只能实现一个抽象类。很多时候,我发现这是抽象类的局限性,这也是我使用大量接口的原因。
抽象类可以包含多个接口。您的PetBase抽象类可以实现IPet(宠物有主人)和IDigestion(宠物吃,或者至少它们应该吃)。然而,PetBase可能不会实施IMammal,因为并非所有宠物都是哺乳动物,也并非所有哺乳动物都是宠物。您可以添加一个扩展PetBase的MammalPetBase并添加IMammal。FishBase可以有PetBase并添加IFish。IFish将使用ISwim和IUnderwaterBreaker作为接口。
是的,对于简单的示例来说,我的示例非常复杂,但这是接口和抽象类如何协同工作的一部分。
让我们以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++),但能够实现多个接口,因此它允许您严格按照需要构造对象。
接口
大多数语言允许您实现多个接口修改接口是一项突破性的更改。所有实现都需要重新编译/修改。所有成员都是公共的。实现必须实现所有成员。接口有助于解耦。您可以使用模拟框架模拟接口后面的任何内容接口通常表示一种行为接口实现彼此分离/隔离
基类
允许您添加一些通过派生免费获得的默认实现(从C#8.0通过接口可以获得默认实现)除了C++,您只能从一个类派生。即使可以从多个类中选择,这通常也是一个坏主意。更改基类相对容易。派生不需要做任何特殊的事情基类可以声明可由派生访问的受保护函数和公共函数抽象基类不能像接口那样容易被模仿基类通常表示类型层次结构(IS A)类派生可能会依赖于一些基本行为(具有复杂的父实现知识)。如果你对一个人的基本实现进行更改并打破其他人,事情可能会很混乱。
这是非常特定于.NET的,但《框架设计指南》一书认为,一般来说,类在不断发展的框架中提供了更多的灵活性。一旦一个接口交付,您就没有机会在不破坏使用该接口的代码的情况下更改它。然而,对于类,你可以修改它,而不是破坏链接到它的代码。只要你做了正确的修改,包括添加新的功能,你就可以扩展和发展你的代码。
Krzysztof Cwalina在第81页上说:
在.NETFramework的三个版本中,我已经与我们团队中的许多开发人员讨论了这一指南。他们中的许多人,包括那些最初不同意该指南的人,都表示他们后悔将一些API作为接口提供。我甚至没有听说过一个案例,其中有人后悔他们运送了一节课。
也就是说,当然有接口的地方。作为一般准则,始终提供接口的抽象基类实现,作为实现接口的方法的示例。在最佳情况下,基类将节省大量工作。
用你自己的判断,要聪明。不要总是照别人(像我)说的做。你会听到“更喜欢接口而不是抽象类”,但这取决于实际情况。这取决于班级。
在上面提到的情况下,我们有一个对象层次结构,接口是一个好主意。接口有助于处理这些对象的集合,在实现服务时也有助于使用层次结构中的任何对象。您只需定义一个用于处理层次结构中的对象的契约。
另一方面,当您实现一组共享公共功能的服务时,您可以将公共功能分离为一个完整的单独类,也可以将其移动到一个公共基类中,并使其抽象化,以便没有人可以实例化基类。
还要考虑如何随时间推移支持抽象。接口是固定的:您将一个接口作为任何类型都可以实现的一组功能的契约发布。基类可以随着时间的推移而扩展。这些扩展成为每个派生类的一部分。
我建议尽可能使用组合而不是继承。使用接口,但对基本实现使用成员对象。这样,您可以定义一个工厂,该工厂构造对象以某种方式运行。如果要更改行为,则创建一个新的工厂方法(或抽象工厂),以创建不同类型的子对象。
在某些情况下,如果所有可变行为都在帮助对象中定义,那么您可能会发现主对象根本不需要接口。
因此,您可能会得到一个带有IFurBehavior参数的宠物,而不是IPet或PetBase。IFurBehavior参数由PetFactory的CreateDog()方法设置。shed()方法调用的就是这个参数。
如果你这样做,你会发现你的代码更加灵活,你的大多数简单对象都处理非常基本的系统范围行为。
即使在多种继承语言中,我也推荐这种模式。
接口有一个明显的优点,即对类来说有点“热交换”。将一个类从一个父类更改为另一个父级通常会导致大量工作,但通常可以删除和更改接口,而不会对实现类产生很大影响。这在您有几个“可能”希望类实现的窄行为集的情况下尤其有用。
这在我的领域尤其适用:游戏编程。基类可能会因继承对象“可能”需要的大量行为而变得臃肿。通过接口,可以轻松地向对象添加或删除不同的行为。例如,如果我为想要反映伤害的对象创建了“IDamageEffects”界面,那么我可以很容易地将其应用于各种游戏对象,并在以后很容易地改变主意。假设我设计了一个要用于“静态”装饰对象的初始类,我最初决定它们是不可破坏的。稍后,我可能会决定,如果它们可以爆炸,那会更有趣,所以我修改了类以实现“IDamageEffects”接口。这比切换基类或创建新的对象层次结构要容易得多。
继承还有其他优点,例如变量能够保存父类或继承类的对象(无需将其声明为泛型,如“object”)。
例如,在.NET WinForms中,大多数UI组件都是从System.Windows.Forms.Control派生的,因此声明为的变量可以“容纳”几乎所有UI元素,无论是按钮、ListView还是其他元素。当然,现在你不能访问项目的所有财产或方法,但你可以拥有所有基本的东西——这可能很有用。
胡安,
我喜欢将接口视为一种描述类的方式。一个特定的犬类,比如约克郡梗,可能是母犬类的后代,但它也执行IFurry、IStubby和IYippieDog。所以类定义了类是什么,但接口告诉了我们关于它的事情。
这样做的好处是,例如,它允许我收集所有的IYippieDog,并将它们放入我的海洋收藏。因此,现在我可以跨越一组特定的对象,找到符合我所关注的标准的对象,而不必过于仔细地检查类。
我发现接口确实应该定义类的公共行为的子集。如果它为实现的所有类定义了所有公共行为,那么它通常不需要存在。他们没有告诉我任何有用的东西。
但这种想法与每个类都应该有一个接口的想法背道而驰,您应该编写接口代码。这很好,但最终会有很多一对一的类接口,这会让事情变得混乱。我知道这样做并不需要花费任何费用,现在你可以轻松地交换东西。然而,我发现我很少这样做。大多数时候,我只是在原地修改现有的类,如果该类的公共接口需要更改,我会遇到完全相同的问题,但我现在必须在两个地方更改它。
所以,如果你像我一样思考,你肯定会说《猫和狗》是IPettable的。这是一个符合两者的特征。
但另一个问题是,它们是否应该有相同的基类?问题是,它们是否需要被广泛地视为同一事物。当然,它们都是动物,但这是否符合我们将如何一起使用它们。
假设我想收集所有动物类并将它们放在我的方舟容器中。
或者它们需要是哺乳动物吗?也许我们需要某种跨动物挤奶工厂?
他们甚至需要联系在一起吗?仅仅知道它们都是IPettable就足够了吗?
当我真的只需要一个类时,我常常会感到想要派生出一个完整的类层次结构。我做这件事是为了期待有一天我可能会需要它,但通常我从来不会这样做。即使我做了,我通常也会发现我必须做很多事情来修复它。这是因为我创建的第一个类不是狗,我没有那么幸运,而是鸭嘴兽。现在,我的整个类层次结构都基于这个奇怪的案例,我有很多浪费的代码。
你可能还会发现,并不是所有的猫都是IPettable的(比如那个无毛的猫)。现在您可以将该接口移动到所有适合的派生类。你会发现,突然之间,猫不再是PettableBase的产物,这是一个小得多的变化。
以前关于将抽象类用于公共实现的评论无疑是正确的。我还没有看到提到的一个好处是,使用接口可以更容易地实现用于单元测试的模拟对象。如Jason Cohen所描述的那样定义IPet和PetBase,使您能够轻松模拟不同的数据条件,而无需物理数据库的开销(直到您决定是时候测试真实数据为止)。
不要使用基类,除非您知道它的含义,并且它适用于这种情况。如果适用,使用它,否则使用接口。但请注意关于小接口的答案。
公共继承在OOD中被过度使用,表达的内容比大多数开发人员意识到或愿意实现的要多得多
简而言之,如果A“是一个”B,那么对于它公开的每个方法,A需要的不超过B,交付的不少于B。
如果除了您的类型成员之外,其他开发人员确实没有任何理由希望使用自己的基类,并且您预见到版本控制问题,那么您应该使用基类(请参见http://haacked.com/archive/2008/02/21/versioning-issues-with-abstract-base-classes-and-interfaces.aspx).
如果继承开发人员有任何理由使用自己的基类来实现类型的接口,并且您没有看到接口发生变化,那么就使用接口。在这种情况下,为了方便起见,您仍然可以加入实现接口的默认基类。
还请记住,不要在OO中被卷走(请参见博客),并始终基于所需的行为来建模对象,如果您设计的应用程序中唯一需要的行为是动物的通用名称和物种,那么您只需要一个类animal,而不是世界上每种可能的动物都需要数百万个类。
接口和基类表示两种不同形式的关系。
继承(基类)表示“is-a”关系。例如,狗或猫是宠物。这种关系始终代表着班级的(单一)目的(结合“单一责任原则”)。
另一方面,接口表示类的其他特性。我将其称为“是”关系,如“Foo是一次性的”,因此C#中的IDisposable接口。
从概念上讲,接口用于正式和半正式地定义对象将提供的一组方法。形式上表示一组方法名称和签名,半形式上表示与这些方法相关的人类可读文档。
接口只是API的描述(毕竟,API代表应用程序编程接口),它们不能包含任何实现,也不可能使用或运行接口。它们只明确约定你应该如何与对象交互。
类提供了一个实现,它们可以声明它们实现了零个、一个或多个接口。如果要继承类,则惯例是在类名前面加上“Base”。
基类和抽象基类(ABC)之间有区别。ABC将接口和实现混合在一起。计算机编程之外的抽象意味着“摘要”,即“抽象==接口”。然后抽象基类可以描述接口,也可以描述要继承的空的、部分的或完整的实现。
关于何时使用接口、抽象基类还是仅使用类的意见将根据您正在开发的内容以及您正在使用的语言而大不相同。接口通常仅与静态类型语言(如Java或C#)相关,但动态类型语言也可以有接口和抽象基类。例如,在Python中,Class和对象之间的区别很明显,前者声明它实现了一个接口,后者是一个类的实例,据说提供了该接口。在动态语言中,两个都是同一类实例的对象可以声明它们提供完全不同的接口。在Python中,这仅适用于对象属性,而方法在类的所有对象之间共享状态。然而,在Ruby中,对象可以具有每实例方法,因此同一类的两个对象之间的接口可能会根据程序员的需要而变化(然而,Ruby没有任何明确的接口声明方式)。
在动态语言中,对象的接口通常是隐式假设的,可以通过内省对象并询问它提供了什么方法(在跳转之前查看),也可以通过简单地尝试在对象上使用所需的接口并在对象不提供该接口时捕捉异常(请求原谅比请求许可更容易)。这可能导致“误报”,即两个接口具有相同的方法名称,但语义不同。然而,权衡是代码更加灵活,因为您不需要预先过度指定以预测代码的所有可能用途。
在这篇Java World文章中对其进行了很好的解释。
就我个人而言,我倾向于使用接口来定义接口,即系统设计中指定如何访问某些内容的部分。
我会有一个类实现一个或多个接口,这并不罕见。
抽象类是我用来做其他事情的基础。
以下是上述文章JavaWorld.com文章的摘录,作者Tony Sintes,2001年4月20日
接口与抽象类选择接口和抽象类不是一个非此即彼的命题。如果你需要改变你的设计,让它成为一个界面。然而,您可能有提供一些默认行为的抽象类。抽象类是应用程序框架中的优秀候选者。抽象类允许您定义一些行为;它们迫使您的子类提供其他子类。例如,如果您有一个应用程序框架,抽象类可以提供默认服务,如事件和消息处理。这些服务允许应用程序插入应用程序框架。但是,有些特定于应用程序的功能只有您的应用程序才能执行。此类功能可能包括启动和关闭任务,这些任务通常依赖于应用程序。因此,抽象基类可以声明抽象的关闭和启动方法,而不是试图定义该行为本身。基类知道它需要这些方法,但抽象类让您的类承认它不知道如何执行这些操作;它只知道它必须发起行动。当需要启动时,抽象类可以调用启动方法。当基类调用此方法时,Java调用子类定义的方法。许多开发人员忘记了定义抽象方法的类也可以调用该方法。抽象类是创建计划继承层次结构的极好方法。对于类层次结构中的非叶类,它们也是一个很好的选择。类与接口
有人说你应该用接口来定义所有的类,但我认为这个建议有点极端。当我看到设计中的某些内容会频繁更改时,我会使用接口。
例如,Strategy模式允许您在程序中交换新的算法和过程,而不改变使用它们的对象。媒体播放器可能知道如何播放CD、MP3和wav文件。当然,您不希望将这些回放算法硬编码到播放器中;这将使添加像AVI这样的新格式变得困难。此外,您的代码将充斥着无用的case语句。为了增加伤害,您需要在每次添加新算法时更新这些案例陈述。总而言之,这不是一种非常面向对象的编程方式。使用Strategy模式,您可以简单地将算法封装在对象后面。如果这样做,您可以随时提供新的媒体插件。让我们将插件类称为MediaStrategy。该对象将有一个方法:playStream(流)。所以为了添加一个新的算法,我们只需扩展我们的算法类。现在,当程序遇到新的媒体类型时,它只是将流的播放委托给我们的媒体策略。当然,您需要一些管道来正确实例化所需的算法策略。这是一个使用界面的好地方。我们使用了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); }
}
是的,这个例子表明,在以这种方式做事时存在大量的代码重复和缺乏优雅。但我们也应该意识到,这有助于保持狗和猫与宠物类的分离(因为狗和猫不能访问宠物的私人成员),并为狗和猫从其他东西(可能是哺乳动物类)继承留下了空间。
当不需要私人访问并且您不需要使用通用的宠物参考/指针来参考狗和猫时,合成是最好的。接口为您提供了通用的引用功能,可以帮助减少代码的冗长,但当组织不好时,它们也会使事情变得模糊。当您需要私人成员访问时,继承是有用的,在使用它时,您将致力于将您的狗和猫类与您的宠物类高度耦合,这是一个巨大的成本。
在继承、组合和接口之间,没有一种方法总是正确的,考虑如何协调地使用这三种选项是有帮助的。在这三个选项中,继承通常是最不常用的选项。
在Submain.NET编码指南中很好地解释了接口上基类的情况:
基类与接口接口类型是部分值的描述,可能受许多对象类型支持。使用基类而不是接口只要可能。从版本控制透视,类更灵活而不是接口。有了课,你可以发布版本1.0,然后发布版本2.0向类中添加新方法。只要该方法不是抽象的,任何现有派生类都将继续功能不变。因为接口不支持实现继承应用于类的模式不适用于接口。添加方法与接口等效向基中添加抽象方法班实现接口将中断,因为类不实现新方法。接口适用于以下情况:几个不相关的类希望支持该协议。这些类已经建立了基类(对于实例一些是用户界面(UI)控件,一些是XMLWeb服务)。聚合不合适或不可行。在所有其他情况,类继承是更好的模型。
当我第一次开始学习面向对象编程时,我犯了一个简单的、可能也是常见的错误,即使用继承来共享公共行为——即使这种行为对对象的本质来说并不重要。
为了进一步建立这个问题中经常使用的例子,有很多东西都是可拍的——女朋友、汽车、毛毯所以我可能有一个Petable类提供了这种常见的行为,以及从中继承的各种类。
然而,可拍不是这些物体的本质的一部分。还有很多更重要的概念对他们的天性至关重要——女朋友是人,汽车是陆地车辆,猫是哺乳动物。。。
行为应该首先被分配给接口(包括类的默认接口),并且只有当它们(a)对于作为更大类的子集的一大群类来说是公共的时,才应该被提升为基类——在相同的意义上,“猫”和“人”是“哺乳动物”的子集。
问题是,当你比我一开始更了解面向对象设计之后,你通常会自动完成这项工作,甚至不用考虑它。因此,“代码到接口,而不是抽象类”这句话的真实性变得如此明显,你很难相信任何人都会不厌其烦地说出它,并开始尝试将其他含义解读到其中。
我想补充的另一点是,如果一个类是纯抽象的-没有非抽象的、非继承的成员或方法暴露给子级、父级或客户端-那么为什么它是一个类?它可以被替换,在某些情况下被接口替换,在其他情况下被Null替换。
关于C#,在某些意义上,接口和抽象类可以互换。然而,区别在于:i)接口不能实现代码;ii)由于这一点,接口不能进一步调用堆栈上的子类;以及iii)抽象类只能在类上继承,而多个接口可以在类上实现。
使用接口在不相关类的ACROSS家族中强制执行契约。例如,对于表示集合但包含完全不同数据的类,您可能有通用的访问方法,即一个类可能表示查询的结果集,而另一个类则可能表示库中的图像。此外,您可以实现多个接口,从而允许您混合(并表示)类的功能。
当类具有共同关系,因此具有相似的结构和行为特征时,使用继承,即汽车、摩托车、卡车和SUV都是可能包含多个车轮、最高速度的道路车辆
我有一个粗略的经验法则
功能:可能在所有部分都不同:界面。
数据和功能,部分基本相同,部分不同:抽象类。
数据和功能,实际工作,如果只进行了轻微的修改:普通(具体)类
数据和功能,没有计划的更改:带有最终修饰符的普通(具体)类。
数据,可能还有功能:只读:枚举成员。
这是非常粗略和准备好的,并且根本没有严格定义,但是有一个范围是从所有内容都要更改的接口到所有内容都像只读文件一样固定的enums。
感谢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();
}
通过def,接口提供了一个与其他代码通信的层。默认情况下,类的所有公共财产和方法都实现隐式接口。我们也可以将接口定义为一个角色,当任何类都需要扮演这个角色时,它必须实现它,根据实现它的类,给它不同的实现形式。oops的两个概念!!!
资料来源: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
//
正如您所看到的,这将是一种保持代码干燥的好方法,并允许在任何类型只能依赖默认Bark而不是特殊情况实现时调用基类实现。GoldenRetriever、Boxer、Lab等类都可以免费继承“默认”(低音类)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方法,并且该对象将根据其类型知道在运行时采取的行为。
再次,这有助于代码保持干燥,这样您就不必编写多个调用对象的方法来预成型相同的核心函数(ShowHowHumanSwims(人类)、ShowHowDuckSwims(鸭子)等)
在这里使用接口允许调用方法不必担心行为是什么类型或如何实现的。它只是知道,给定接口,每个对象都必须实现Swim方法,因此在自己的代码中调用它是安全的,并允许在自己的类中处理Swim方法的行为。
摘要:
因此,我的主要经验法则是,当你想为一个类层次结构或/和你正在处理的类或类型实现一个“默认”功能时,使用一个抽象类。
另一方面,如果你没有“是一种”关系,但类型共享“能力”来做某事或拥有某事(例如,鸭子“不是”人类“。然而,鸭子和人类共享“游泳能力”),则使用界面。
抽象类和接口之间需要注意的另一个区别是,一个类可以实现一对多接口,但一个类只能从一个抽象类(或任何类)继承。是的,您可以嵌套类并具有继承层次结构(许多程序都这样做,也应该这样做),但不能在一个派生类定义中继承两个类(这一规则适用于C#。在其他一些语言中,您可以这样做,通常是因为这些语言中缺少接口)。
还请记住,使用接口时要遵守接口隔离原则(ISP)。ISP表示,不应强迫任何客户端依赖它不使用的方法。因此,接口应该专注于特定的任务,并且通常非常小(例如IDisposable、IComparable)。
另一个提示是,如果您正在开发小而简洁的功能,请使用接口。如果要设计大型功能单元,请使用抽象类。
希望这能为一些人扫清障碍!
此外,如果你能想出任何更好的例子或想指出什么,请在下面的评论中这样做!
列出你的对象必须要做的事情,你的对象可以做的事情。必须指明你的基类型,可以指明你的接口。
例如,您的PetBase必须呼吸,而您的IPet可能会DoTricks。
对问题域的分析将帮助您定义精确的层次结构。
我应该何时使用接口,何时使用基类?
如果
你有纯抽象方法,没有非抽象方法您没有非抽象方法的默认实现(除了Java 8语言,其中接口方法提供默认实现)若您使用的是Java8,现在接口将为一些非抽象方法提供默认实现。与抽象类相比,这将使接口更可用。
请查看此SE问题以了解更多详细信息。
如果我不想实际定义方法的基本实现,它应该始终是一个接口吗?
对它更好更干净。即使您有一个带有一些抽象方法的基类,让我们的基类通过接口扩展抽象方法。您可以在将来更改接口而不更改基类。
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
}
优势:
如果我想在现有接口中再添加一个抽象方法,我只需简单地更改接口,而不需要接触抽象基类。如果我想更改契约,我将在不接触基类的情况下更改接口和实现类。您可以通过接口为基类提供不变性。看看这篇文章
有关详细信息,请参阅此相关SE问题:
我应该如何解释接口和抽象类之间的区别?
首选接口而非抽象类
根本原因需要考虑的要点[这里已经提到了两个]是:
接口更灵活,因为一个类可以实现多个接口。由于Java没有多重继承,因此使用抽象类防止用户使用任何其他类等级制度通常,当没有默认值时,首选接口实现或状态。Java集合提供了这(地图、集合等)。抽象类的优点是允许更好的转发兼容性。一旦客户端使用了一个接口,就不能更改它;如果它们使用抽象类,您仍然可以添加行为而不使用破坏现有代码。如果兼容性是一个问题,请考虑使用抽象类。即使您有默认实现或内部状态,考虑提供一个接口及其抽象实现。这将有助于客户,但如果所需[1]。当然,这个问题已经讨论了很长时间其他地方[2,3]。
[1] 当然,它增加了更多的代码,但如果简洁是您的主要关注点,那么您可能应该首先避免使用Java!
[2] Joshua Bloch,《有效的Java》,第16-18项。
[3] http://www.codeproject.com/KB/ar...
我发现Interface>Abstract>Concrete的模式适用于以下用例:
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)
}
当有很多具体的类时,这种设计是很好的,并且您不希望维护样板只是为了编程到接口。如果向接口中添加了新方法,则会破坏所有生成的类,因此您仍然可以获得接口方法的优势。
在这种情况下,抽象也可以是具体的;然而,抽象的名称有助于强调这种模式正在被使用。