我应该在什么时候使用接口,什么时候使用基类?
如果我不想定义方法的基本实现,是否总是应该使用接口?
如果我有一个Dog和Cat类,为什么我要实现IPet而不是PetBase?我可以理解为ISheds或IBarks(IMakesNoise?)等功能提供接口,因为这些功能可以针对每个宠物进行设置,但我不明白为什么要为通用Pet使用哪种方式。
我应该在什么时候使用接口,什么时候使用基类?
如果我不想定义方法的基本实现,是否总是应该使用接口?
如果我有一个Dog和Cat类,为什么我要实现IPet而不是PetBase?我可以理解为ISheds或IBarks(IMakesNoise?)等功能提供接口,因为这些功能可以针对每个宠物进行设置,但我不明白为什么要为通用Pet使用哪种方式。
让我们以一个狗和猫类的例子来说明,使用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 ++),但能够实现多个接口,因此它允许您在严格的根据需要的基础上构造对象。
嗯,Josh Bloch在Effective Java第二版中亲自说过:
一些主要观点:
另一方面,接口很难进化。如果你在接口中添加一个方法,它会破坏所有实现它的内容。
现有类可以轻松地改装以实现新的接口。您所需要做的就是添加所需的方法(如果它们尚不存在)并将一个implements子句添加到类声明中。
接口非常适合定义混合类型。粗略地说,混合类型是指类可以实现除其“主要类型”之外的类型,以声明它提供某些可选行为。例如,Comparable是一种混合接口,允许类声明其实例与其他可相互比较的对象相对有序。
接口允许构建非分层类型框架。类型层次结构对于组织某些内容非常有用,但其他内容并不总是适合严格的层次结构。
通过包装器类惯用语,接口使功能增强变得安全而强大。如果使用抽象类来定义类型,则希望添加功能的程序员别无选择,只能使用继承。
此外,您可以通过为导出的每个非平凡接口提供抽象骨架实现类来结合接口和抽象类的优点。
接口和基类代表两种不同的关系形式。
继承(基类)代表一种“is-a”关系。例如,狗或猫“是”宠物。这种关系始终表示类的(单一)目的(与“单一职责原则”结合使用)。
接口则代表一个类的额外特征。我将其称为“is”关系,例如在C#中的"Foo
可被处理",因此有IDisposable
接口。
现代风格是定义 IPet 和 PetBase。
接口的优点在于其他代码可以完全“干净”地使用它,而不必与其他可执行代码有任何联系。同时,接口还可以混合使用。
但是基类对于简单的实现和常见的实用程序非常有用。因此,也提供抽象基类以节省时间和代码。
一般来说,应该优先使用接口而不是抽象类。使用抽象类的一个原因是当有多个具体类存在公共实现时。当然,你仍然应该声明一个接口(IPet),并让一个抽象类(PetBase)实现该接口。通过使用小巧、独立的接口,您可以使用多种接口来进一步提高灵活性。接口允许在不同边界之间最大限度地提供类型的灵活性和可移植性。在跨边界传递引用时,始终传递接口而不是具体类型。这样接收端就可以确定具体实现,并提供最大的灵活性。在TDD / BDD编程中,这是绝对正确的。
Gang of Four在他们的书中指出:"由于继承暴露了子类的细节实现,所以经常说'继承破坏了封装'。我相信这是真的。
这与.NET有关,但《框架设计准则》一书认为,在不断演变的框架中,通常使用类会更具灵活性。一旦一个接口被发布,您就没有机会在不破坏使用该接口的代码的情况下进行更改。但是,使用类可以修改它而不会破坏链接到它的代码。只要您进行正确的修改,包括添加新功能,即可扩展和发展代码。
Krzysztof Cwalina在第81页上说:
在.NET Framework的三个版本过程中,我已经与我们团队中的很多开发人员讨论了这个准则。其中许多人,包括最初不同意这些准则的人,都表示后悔将某些API作为接口发布。我甚至没有听说过有哪个人后悔发布了一个类。
话虽如此,接口确实有其应用场景。作为一般指南,请始终提供接口的抽象基类实现,以便作为实现接口的一种方式的示例。在最好的情况下,该基类将节省大量工作。
Juan,
我认为接口是描述类的一种方式。例如,一个约克夏犬品种类可能是父级犬类的后代,但它还实现了IFurry、IStubby和IYippieDog接口。因此,类定义了类本身,而接口告诉我们关于它的信息。
这样做的好处在于,例如,我可以收集所有IYippieDog并将它们放入我的Ocean集合中。因此,现在我可以跨越一组特定的对象,找到满足我要求的对象,而无需过多地检查类。
我发现接口真正应该定义类的公共行为子集。如果它为实现所有公共行为的所有类定义,则通常不需要存在。它们对我没有任何有用的信息。
尽管如此,这个想法与每个类都应该有一个接口,并且应该编写接口的想法相反。那很好,但你最终会得到很多一对一的接口和类,这会使事情变得混乱。我理解的想法是这样做实际上不会花费太多成本,现在您可以轻松地交换东西。然而,我发现我很少这样做。大多数时候我只是直接修改现有类,如果该类的公共接口需要更改,我仍然会遇到相同的问题,只是现在必须在两个地方进行更改。
因此,如果您和我一样思考,您肯定会说猫和狗都是可宠物化的(IPettable)。这是一种匹配它们两个的特征。
但另一个问题是它们是否应该有相同的基类?问题是它们是否需要被广泛视为同一件事。当然,它们都是动物,但这是否符合我们将如何一起使用它们的方式。
比如我想收集所有的动物类并将它们放入我的Ark容器中。
或者它们需要成为哺乳动物吗?也许我们需要某种跨动物哺乳工厂?
它们甚至需要在一起链接吗?仅知道它们都是可宠物化的是否足够?
我经常会有一种想要衍生整个类层次结构的冲动,即使我只需要一个类。我这样做是因为我预计将来可能会用到它,但实际上通常从未用过。即使我确实需要用到它,通常我也会发现需要做很多修复工作。那是因为我正在创建的第一个类不是Dog,我没有那么幸运,而是鸭嘴兽。现在我的整个类层次结构都基于奇怪的情况,我有很多浪费的代码。
你可能也会发现并非所有的猫都能被抚摸(就像那个无毛的),这时你可以将接口移动到适合的派生类中。你会发现这是一种比突然将所有猫都从PettableBase派生出来更少破坏性的改变。
以下是“接口”和“基类”的基本简单定义:
干杯
这篇Java World文章中很好地解释了这个问题。
个人而言,我倾向于使用接口来定义接口 - 即系统设计的部分,指定如何访问某些内容。
通常情况下,我会有一个类实现一个或多个接口。
抽象类我用作其他东西的基础。
以下是上述文章JavaWorld.com文章,作者Tony Sintes,04/20/01中的摘录:
许多开发人员忘记了定义抽象方法的类也可以调用该方法。抽象类是创建计划继承层次结构的一种极好方式。它们也是类层次结构中非叶类的良好选择。接口与抽象类
选择接口和抽象类不是一种二选一的方案。如果需要更改设计,请将其设为接口。但是,您可以有提供某些默认行为的抽象类。抽象类非常适合应用程序框架内部。
抽象类允许您定义某些行为; 它们强制子类提供其他行为。例如,如果您有一个应用程序框架,则抽象类可以提供默认服务,例如事件和消息处理。这些服务允许您的应用程序插入到应用程序框架中。但是,仅您的应用程序才能执行某些特定于应用程序的功能。这样的功能可能包括启动和关闭任务,这通常取决于应用程序。因此,抽象基类可以声明抽象的关闭和启动方法,而不是尝试定义该行为本身。基类知道它需要这些方法,但抽象类允许您的类承认它不知道如何执行这些操作; 它只知道必须启动这些操作。当启动时,抽象类可以调用启动方法。当基类调用此方法时,Java将调用由子类定义的方法。