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