我何时应该使用接口,何时应该使用基类?
如果我不想实际定义方法的基本实现,它应该始终是一个接口吗?
如果我有狗和猫类.为什么我要实现IPet而不是PetBase?我可以理解有ISheds或IBarks(IMakesNoise?)的接口,因为那些可以基于宠物放在宠物上,但我不明白哪个用于通用Pet.
让我们以你的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 Tricks{get; set;}
void Bathe();
void Train(Trick t);
}
上述合同的实施在猫与狗之间是不一样的; 将他们的实现放在一个抽象类中继承将是一个坏主意.
您的狗和猫定义现在应该如下所示:
public class Dog : Mammal, IPettable
public class Cat : Mammal, IPettable
从理论上讲,您可以从更高的基类覆盖它们,但实际上,一个接口允许您只需要将所需的内容添加到类中而无需继承.
因此,因为您通常只能从一个抽象类继承(在大多数静态类型的OO语言中,例外包括C++)但能够实现多个接口,它允许您严格按要求构造对象.
好吧,Josh Bloch在Effective Java 2d中说自己:
一些要点:
可以轻松地对现有类进行改装以实现新接口.您所要做的就是添加所需的方法(如果它们尚不存在)并将一个implements子句添加到类声明中.
接口是定义mixins的理想选择.松散地说,mixin是类可以实现的类型,除了它的"主类型"之外,它声明它提供了一些可选行为.例如,Comparable是一个mixin接口,允许类声明其实例相对于其他可相互比较的对象进行排序.
接口允许构造非分层类型框架.类型层次结构非常适合组织某些事物,但其他事情并不能完全落入严格的层次结构中.
接口通过包装类习惯用法实现安全,强大的功能增强.如果您使用抽象类来定义类型,那么您可以让想要添加功能的程序员别无选择,只能使用继承.
此外,您可以通过提供抽象骨架实现类来组合接口和抽象类的优点,以与您导出的每个非平凡接口一起使用.
另一方面,接口很难发展.如果向接口添加方法,它将破坏它的所有实现.
PS:买这本书.它更加详细.
接口和基类代表两种不同形式的关系.
继承(基类)表示"is-a"关系.例如,狗或猫"是 - "宠物.这种关系总是代表阶级的(单一)目的(与"单一责任原则"相结合).
另一方面,接口代表类的附加功能.我称之为"is"关系,就像" Foo
是一次性的",因此是IDisposable
C#中的界面.
现代风格是定义IPet 和 PetBase.
该接口的优点是其他代码可以使用它而与其他可执行代码无任何关系.完全"干净".接口也可以混合使用.
但是基类对于简单实现和常用实用程序很有用.因此,提供一个抽象基类以节省时间和代码.
定义2个模块之间的合同.不能有任何实施.
大多数语言允许您实现多个接口
修改界面是一个重大变化.所有实现都需要重新编译/修改.
所有成员都是公开的.实现必须实现所有成员.
接口有助于解耦.您可以使用模拟框架来模拟界面背后的任何内容
接口通常表示一种行为
接口实现彼此分离/隔离
允许您添加一些通过派生免费获得的默认实现
除C++外,您只能从一个类派生.即使可以来自多个班级,通常也是一个坏主意.
更改基类相对容易.派生不需要做任何特别的事情
基类可以声明可以通过派生访问的受保护和公共函数
抽象基类不能像接口那样容易模拟
基类通常表示类型层次结构(IS A)
类派生可能依赖于某些基本行为(具有对父实现的复杂知识).如果你改变一个人的基础实现并打破其他人,事情就会变得混乱.
通常,您应该优先于抽象类的接口.使用抽象类的一个原因是,如果您在具体类中有共同的实现.当然,你仍然应该声明一个接口(IPet)并有一个抽象类(PetBase)实现该接口.使用小的,不同的接口,你可以使用倍数来进一步提高灵活性.接口允许跨边界的类型具有最大的灵活性和可移植性.跨越边界传递引用时,始终传递接口而不是具体类型.这允许接收端确定具体实现并提供最大的灵活性.当以TDD/BDD方式编程时,这是绝对正确的.
"四人帮"在他们的书中指出"因为继承将子类暴露给其父级实现的细节,所以通常会说'继承会破坏封装".我相信这是真的.
这是非常特定于.NET的,但是框架设计指南书认为,通用类在不断发展的框架中提供了更大的灵活性.一旦发布了接口,您就没有机会在不破坏使用该接口的代码的情况下进行更改.但是,对于类,您可以修改它,而不是破坏链接到它的代码.只要您做出正确的修改(包括添加新功能),您就可以扩展和改进代码.
Krzysztof Cwalina在第81页说:
在.NET Framework的三个版本的过程中,我与我们团队中的不少开发人员讨论了这个指南.他们中的许多人,包括那些最初不同意这些指南的人,都表示他们后悔将一些API作为接口发布.我甚至没有听说过有人后悔他们发了一堂课.
话虽这么说肯定有接口的地方.作为一般指导,总是提供接口的抽象基类实现,如果没有别的,作为实现接口的方式的示例.在最好的情况下,基类将节省大量的工作.
胡安,
我喜欢将接口视为表征类的一种方式.一个特定的狗品种,比如YorkshireTerrier,可能是父狗类的后代,但它也实现了IFurry,IStubby和IYippieDog.所以这个类定义了类是什么,但接口告诉我们它的内容.
这样做的好处是它允许我收集所有的IYippieDog并将它们扔进我的Ocean系列.因此,现在我可以跨越一组特定的对象,找到符合我正在查看的标准的对象,而无需过于仔细地检查课程.
我发现接口确实应该定义一个类的公共行为的子集.如果它定义了所有实现的类的所有公共行为,那么它通常不需要存在.他们没有告诉我任何有用的东西.
这个想法虽然与每个类都应该有一个接口并且你应该编写接口的想法背道而驰.这很好,但是你最终会遇到很多一对一的接口,这会让事情变得混乱.我知道这个想法是它不需要做任何事情,现在你可以轻松地交换内容.但是,我发现我很少这样做.大多数时候我只是修改现有的类,并且如果该类的公共接口需要更改,我总是会遇到完全相同的问题,除了我现在必须在两个地方更改它.
因此,如果你像我一样思考,你肯定会说Cat和Dog是IPettable.这是一种与两者相匹配的特征.
另一部分是他们应该有相同的基类吗?问题是他们需要被广泛地视为同一件事.当然它们都是动物,但这是否符合我们将它们一起使用的方式.
假设我想收集所有Animal类并将它们放入我的Ark容器中.
还是他们需要成为哺乳动物?也许我们需要某种跨动物挤奶工厂?
他们甚至需要联系在一起吗?只知道它们都是IPettable就足够了吗?
当我真正需要一个班级时,我常常觉得有必要得出整个班级.我希望总有一天我可能需要它,通常我永远不会这样做.即使我这样做,我也常常发现我必须做很多事来解决它.那是因为我创作的第一堂课不是狗,我不是那么幸运,而是Platypus.现在我的整个类层次结构基于奇怪的情况,我有很多浪费的代码.
您可能还会发现,并非所有Cats都是IPettable(就像那个无毛的那样).现在,您可以将该接口移动到适合的所有派生类.你会发现一个不那么突破的变化,突然之间的Cats不再来自PettableBase.
这是接口和基类的基本和简单定义:
基类=对象继承.
接口=功能继承.
干杯
我建议尽可能使用合成而不是继承.使用接口但使用成员对象进行基本实现.这样,您可以定义一个工厂,构造您的对象以某种方式运行.如果要更改行为,则创建一个新的工厂方法(或抽象工厂),以创建不同类型的子对象.
在某些情况下,如果在辅助对象中定义了所有可变行为,您可能会发现主要对象根本不需要接口.
因此,您最终可能会使用具有IFurBehavior参数的Pet,而不是IPet或PetBase.IFurBehavior参数由PetFactory的CreateDog()方法设置.这个参数是为shed()方法调用的.
如果你这样做,你会发现你的代码更灵活,你的大多数简单对象都处理非常基本的系统行为.
即使在多重继承语言中,我也推荐这种模式.
在这篇Java World文章中做了很好的解释
我个人倾向于使用接口来定义接口 - 即系统设计的一部分,用于指定应该如何访问接口.
我将有一个实现1个或更多接口的类并不罕见.
抽象类我用作其他东西的基础.
以下是上述文章JavaWorld.com文章,作者Tony Sintes,04/20/01的摘录
接口与抽象类
选择接口和抽象类不是一个或两个命题.如果您需要更改设计,请将其设为界面.但是,您可能具有提供某些默认行为的抽象类.抽象类是应用程序框架内的优秀候选者.
抽象类让你定义一些行为; 他们强迫你的子类提供其他人.例如,如果您有应用程序框架,则抽象类可以提供默认服务,例如事件和消息处理.这些服务允许您的应用程序插入您的应用程序框架.但是,有一些特定于应用程序的功能,只有您的应用程序才能执行.此类功能可能包括启动和关闭任务,这些任务通常取决于应用程序.因此,抽象基类可以声明抽象关闭和启动方法,而不是尝试自己定义该行为.基类知道它需要那些方法,但是抽象类允许你的类承认它不知道如何执行这些操作; 它只知道它必须启动行动.什么时候开始,抽象类可以调用启动方法.当基类调用此方法时,Java会调用子类定义的方法.
许多开发人员忘记了定义抽象方法的类也可以调用该方法.抽象类是创建计划继承层次结构的绝佳方法.对于类层次结构中的非叶类,它们也是一个很好的选择.
类与接口
有人说你应该用接口来定义所有类,但我认为推荐看起来有点极端.当我看到设计中的某些内容经常发生变化时,我会使用界面.
例如,策略模式允许您将新算法和流程交换到程序中,而无需更改使用它们的对象.媒体播放器可能知道如何播放CD,MP3和wav文件.当然,您不希望将这些播放算法硬编码到播放器中; 这将使添加像AVI这样的新格式变得困难.此外,您的代码将充斥着无用的case语句.并且为了增加侮辱伤害,每次添加新算法时都需要更新这些案例语句.总而言之,这不是一种非常面向对象的编程方式.
使用策略模式,您可以简单地将算法封装在对象后面.如果这样做,您可以随时提供新的媒体插件.我们来调用插件类MediaStrategy.该对象将有一个方法:playStream(Stream s).因此,为了添加新算法,我们只需扩展我们的算法类.现在,当程序遇到新媒体类型时,它只是将流的播放委托给我们的媒体策略.当然,您需要一些管道才能正确实例化您需要的算法策略.
这是使用界面的绝佳场所.我们使用了策略模式,它清楚地表明了设计中将会发生变化的地方.因此,您应该将策略定义为接口.当您希望对象具有某种类型时,通常应该优先选择接口而不是继承.在这种情况下,MediaStrategy.依赖继承进行类型识别是危险的; 它将您锁定到特定的继承层次结构中.Java不允许多重继承,因此您无法扩展为您提供有用实现或更多类型标识的内容.
另外请记住,不要在OO中被扫除(参见博客)并始终根据所需的行为建模对象,如果您正在设计一个应用程序,其中您需要的唯一行为是动物的通用名称和物种,那么您只需要一类具有名称属性的动物,而不是世界上每种可能动物的数百万个类.
我有一个粗略的经验法则
功能:可能在所有部分都有所不同:界面.
数据和功能,部分将大致相同,部分不同:抽象类.
数据和功能,实际工作,如果只是稍微改变扩展:普通(具体)类
数据和功能,没有计划的更改:具有最终修饰符的普通(具体)类.
数据和功能:只读:枚举成员.
这是非常粗略和准备好的,并没有严格定义,但是有一个来自接口的频谱,其中所有内容都要更改为枚举,其中所有内容都修复有点像只读文件.
接口应该很小.真的很小.如果你真的打破了你的对象,那么你的接口可能只包含一些非常具体的方法和属性.
抽象类是快捷方式.PetBase的所有衍生品是否共享,您可以编码一次并完成?如果是,那么是抽象课的时间.
抽象类也是有限的.虽然它们为您提供了生成子对象的快捷方式,但任何给定对象都只能实现一个抽象类.很多时候,我发现这是Abstract类的限制,这就是我使用大量接口的原因.
抽象类可能包含多个接口.你的PetBase抽象类可以实现IPet(宠物拥有者)和IDigestion(宠物吃,或至少他们应该).然而,PetBase可能不会实施IMammal,因为并非所有宠物都是哺乳动物,并非所有哺乳动物都是宠物.您可以添加一个扩展PetBase并添加IMammal的MammalPetBase.FishBase可以拥有PetBase并添加IFish.IFish将ISwim和IUnderwaterBreather作为接口.
是的,我的例子对于简单的例子来说过于复杂,但这是关于接口和抽象类如何协同工作的一部分.
资料来源: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(); 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这样的类都可以免费继承"默认"(低音类)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(); 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方法的行为.
摘要:
因此,我的主要经验法则是,当您想要为类层次结构实现"默认"功能时使用抽象类或/和您正在使用的类或类型共享"是一种"关系(例如,poodle"是一个"狗的类型.
另一方面,当你没有"是一种"关系时,使用一个界面,但是有一些类型分享"能力"来做某事或有某些东西(例如鸭子"不是"人类.但是,鸭子和人类的分享"游泳的能力".
抽象类和接口之间需要注意的另一个区别是,类可以实现一个到多个接口,但是类只能从一个抽象类(或任何类)继承.是的,您可以嵌套类并具有继承层次结构(许多程序都应该这样做)但您不能在一个派生类定义中继承两个类(此规则适用于C#.在其他一些语言中,您可以执行此操作,通常只是因为这些语言缺乏接口).
还要记住使用接口遵守接口隔离原则(ISP).ISP声明不应该强迫任何客户端依赖它不使用的方法.因此,接口应该专注于特定任务,并且通常非常小(例如IDisposable,IComparable).
另一个提示是,如果您正在开发小巧,简洁的功能,请使用接口.如果要设计大型功能单元,请使用抽象类.
希望这能为某些人解决问题!
此外,如果你能想到更好的例子或想要指出一些事情,请在下面的评论中这样做!
Submain .NET编码指南中详细解释了基于接口的基类的情况:
基类与接口 接口类型是值的部分描述,可能由许多对象类型支持.尽可能使用基类而不是接口.从版本控制的角度来看,类比接口更灵活.使用类,您可以发布版本1.0,然后在版本2.0中向类添加新方法.只要该方法不是抽象的,任何现有的派生类继续保持不变.
由于接口不支持实现继承,因此适用于类的模式不适用于接口.向接口添加方法等同于向基类添加抽象方法; 任何实现接口的类都会中断,因为该类没有实现新方法.接口适用于以下情况:
几个不相关的类想要支持该协议.
这些类已经建立了基类(例如,一些是用户界面(UI)控件,一些是XML Web服务).
汇总不合适或不切实际.在所有其他情况下,类继承是更好的模型.
一个重要的区别是您只能继承一个基类,但您可以实现许多接口.因此,如果您绝对确定不需要继承其他基类,则只需要使用基类.此外,如果您发现您的界面变得越来越大,那么您应该开始将其分解为一些定义独立功能的逻辑部分,因为没有规则您的类无法全部实现(或者您可以定义不同的只是继承它们以将它们分组的接口).