在设计 C# 类库时,我应该何时选择继承而不是接口? [关闭]

Posted

技术标签:

【中文标题】在设计 C# 类库时,我应该何时选择继承而不是接口? [关闭]【英文标题】:When should I choose inheritance over an interface when designing C# class libraries? [closed] 【发布时间】:2011-08-14 13:34:39 【问题描述】:

我有一些 Processor 类,它们将做两件非常不同的事情,但从通用代码中调用(“控制反转”情况)。

我想知道在决定它们是否都应该从BaseProcessor 继承或将IProcessor 实现为接口时,我应该认识到(或认识到,对于你们美国用户而言)哪些设计考虑因素。

【问题讨论】:

另见更一般的问题,不特定于 C#:Interface vs Base class 【参考方案1】:

一般来说,规则是这样的:

继承描述了一种is-a关系。 实现一个接口描述了一种can-do关系。

为了更具体地说明这一点,让我们看一个例子。 System.Drawing.Bitmapis-an 图像(因此,它继承自 Image 类),但它也 can-do 处理,因此它实现IDisposable 接口。它也可以做序列化,所以它从ISerializable接口实现。

但更实际地,接口通常用于在 C# 中模拟多重继承。如果你的Processor 类需要继承System.ComponentModel.Component 之类的东西,那么你别无选择,只能实现IProcessor 接口。

事实上,接口和抽象基类都提供了一个约定,指定特定类可以做什么。声明这个合约需要接口是一个常见的神话,但这是不正确的。在我看来,最大的优势是抽象基类允许您为子类提供默认功能。但是,如果没有有意义的默认功能,则没有什么可以阻止您将方法本身标记为 abstract,要求派生类自己实现它,就像它们要实现一个接口一样。

对于此类问题的答案,我经常求助于.NET Framework Design Guidelines,其中有关于在类和接口之间进行选择的说法:

一般来说,类是公开抽象的首选构造。

接口的主要缺点是,在允许 API 演进方面,它们的灵活性远低于类。一旦你发布了一个接口,它的成员集就永远固定了。对接口的任何添加都会破坏实现接口的现有类型。

一个类提供了更多的灵活性。您可以将成员添加到已发布的课程中。只要该方法不是抽象的(即,只要您提供该方法的默认实现),任何现有的派生类都将继续保持不变。

[ . . . ]

支持接口的最常见论点之一是它们允许将合同与实现分开。但是,该论点错误地假设您不能使用类将合同与实现分开。抽象类与其具体实现位于一个单独的程序集中,是实现这种分离的好方法。

他们的一般建议如下:

不要倾向于定义类而不是接口。 不要使用抽象类而不是接口来将契约与实现分离。如果定义正确,抽象类允许契约和实现之间相同程度的解耦。 如果您需要提供值类型的多态层次结构,请定义一个接口。 考虑定义接口以实现与多重继承类似的效果。

克里斯·安德森特别同意最后一条原则,他认为:

抽象类型的版本要好得多,并允许未来的可扩展性,但它们也会烧毁你唯一的基本类型。当您真正定义随时间不变的两个对象之间的契约时,接口是合适的。抽象基类型更适合为类型族定义公共基。

【讨论】:

谢谢 - 很好的答案......同上。只有一个警告:如果您要发布一个基类作为“公共类型”,那么建议该类的用户使用 THERE OWN 基类从它继承...给他们一个地方让他们有共同的行为(将来)....并感谢大量的链接。我有一些很好的阅读要做;-) 需要注意的是,界面更像是“必须做的”。 鉴于最近在 java 8 中为在接口中引入默认方法所做的更改,这个答案变得更加真实。否则,在不破坏实现接口的每一段代码的情况下,没有其他方法可以引入新的合约/方法。 (虽然抽象类和接口之间的界限随着这种变化变得更加模糊) 这个建议是不是几乎与“Head First Design patterns”的主旨相反。我无论如何都不是专家,但他们的口头禅是更喜欢组合而不是继承。谁能解开这个? @ali 我根本看不出矛盾。诚然,我写这个答案已经很多年了,但是在再次浏览它时,我没有看到任何地方我提倡继承而不是组合。实际上,它并没有说明关于作曲的任何内容。如果不需要继承建模的强关系,那么组合是更好的选择。不要表达比你需要的更牢固的关系。【参考方案2】:

理查德,

为什么要在它们之间进行选择?我有一个 IProcessor 接口作为 published 类型(用于系统的其他地方);如果碰巧你的 IProcessor 的各种 CURRENT 实现具有共同行为,那么抽象 BaseProcessor 类将是实现该共同行为的真正好地方。

这样,如果您将来需要一个不用于 BaseProcessor 服务的 IProcessor,它不必拥有它(并且可能隐藏它)......但那些确实想要它的人可以拥有它...... . 减少重复的代码/概念。

只是我的拙见。

干杯。基思。

【讨论】:

谢谢基思 - 这是一个很好的观点。如果有任何考虑可能会回来咬我选择一个而不是另一个,我真的在徘徊。很好地实现这两个方面。 嗯,我不同意这一点。如果没有该接口的具体实现,您通常不应该公开接口。暴露接口的另一个问题是版本控制(有关详细信息,请参阅我的答案)。我认为你不需要两者都做。这并不总是设计决策的最佳答案。尝试两者都以增加复杂性为代价,我很难看出这在这里有什么好处。只需使用抽象基类而不是接口,您就不会错过任何东西。【参考方案3】:

接口是一种“契约”,它们确保某些类实现所需的一组成员 - 属性、方法和事件 -。

基类(具体或抽象,无关紧要)是某些实体的原型。就是这些实体,代表了一些实际物理或概念实体中的共同点。

何时使用接口?

当某种类型需要声明时,至少有一些消费者应该关心的行为和属性,并使用它们来完成某些任务。

何时使用基类(具体和/或抽象)

当一组实体共享相同的原型时,意味着 B 继承 A,因为 B 是具有差异的 A,但 B 可以被识别为 A。


例子:

我们来谈谈表格。

我们接受可回收的表格 => 这必须使用像“IRecyclableTable”这样的接口来定义,以确保所有可回收的表格都有一个“回收”方法.

我们想要桌面表格 => 这必须用继承来定义。 “桌面表”是“表”。所有表格都具有共同的属性和行为,而桌面表格将具有相同的属性和行为,添加此类内容会使桌面表格与其他类型的表格不同。

我可以在一个对象图中谈论关联,这两种情况的含义,但在我看来,如果我需要从概念的角度给出论点,我会用这个论点来回答。

【讨论】:

我不太确定“可回收表”是什么,但我可能会实现一个 IRecyclable 接口,因为似乎有几个 unrelated 对象需要可回收(即其他不代表表的类)。然后我仍然会从Table 类中实现所有表,包括DesktopTablePicnicTableFoldingTable 而且,如果您仔细检查我的文字,您是否发现我说的与您不同? :) 阅读有关“可回收表”的内容:“示例”。这只是一个示例,与任何对象图、设计或实际案例完全隔离。 不管怎样,谢谢你的建议,不要误会我的话,嘿嘿! :) 有人说“接口不是契约”:blogs.msdn.microsoft.com/kcwalina/2004/10/24/…【参考方案4】:

我不太擅长设计选择,但如果被问到,如果只有要扩展的成员,我会更喜欢实现 iProcessor 接口。如果还有其他功能不需要扩展,继承baseprocessor是更好的选择。

【讨论】:

@jitendra - 谢谢,您能否提供更多细节说明为什么这是一个更好的选择? 为什么更喜欢接口?接口相对于抽象基类的优势是什么?我完全不同意你的直觉——我会总是选择抽象基类而不是接口,除非我特别需要模拟多重继承。 如果只有几个函数,继承更有意义,因为您不必实现基类中可用的每个函数。并且没有必要对基类的函数和参数的完整列表有足够的信息。因此,在这种情况下,继承更有意义。 另外,你总是可以创建一个普通的基类,而不是抽象的,并且只继承所需的功能,即使这个基类应该在以后使用。但是,在这种情况下,性能会受到影响,因为接口应该比类更轻。另外,如果在抽象类上使用接口,其他开发人员甚至不需要实现每个函数,只需使用基类中的几个函数。因此,基类的可重用性也是一个因素,您应该在做出此类选择之前考虑。 @cody gray 如果我没记错的话,你应该实现整个抽象类,这在可重用性的情况下是个坏主意。同样,这只是我的观点。可能是错的。如果有错误,请编辑评论。【参考方案5】:

抛开设计纯洁性,你只能继承一次,如果你需要继承一些框架类,这个问题就没有实际意义了。

如果您有选择,您可以务实地选择最节省打字的选项。无论如何,它通常是纯粹的选择。

编辑:

如果基类有一些实现,那么这可能很有用,如果它是纯粹抽象的,那么它也可能是一个接口。

【讨论】:

这是一个很好的观点。通常,选择基类可以节省最多的输入。如果你有一个方法有几个重载都调用一个核心函数,你可以把所有的参数验证、边界检查和函数重载放在基类中。这意味着派生类只需要实现核心的“肉和土豆”功能。这不是接口的选项,实现该接口的 每个 类每次都必须完成所有工作。 @Cody Gray 关于最后一部分,但也许基类实现了一个接口,而接口实现是虚拟的,所以这是一个混合情况!开个玩笑,没关系。【参考方案6】:

鉴于 SOLID 原则为您的项目提供了更多的可维护性和可扩展性,我更喜欢接口而不是继承。

另外,如果您需要在界面中添加“附加功能”,最好的选择是完全创建一个新界面,遵循 SOLID 中的 I,即界面隔离原则。

【讨论】:

【参考方案7】:

如果您选择什么都不重要,请始终选择界面。它允许更大的灵活性。它可能会保护您免受将来的更改(如果您需要更改基类中的某些内容,则继承的类可能会受到影响)。它还允许更好地封装细节。如果您使用的是依赖注入控制反转,他们倾向于使用接口。

或者如果不能避免继承,将它们一起使用可能是个好主意。创建一个实现接口的抽象基类。在您的情况下,ProcessorBase 实现了一个 IProcessor。

类似于 ASP.NET Mvc 中的 ControllerBase 和 IController。

【讨论】:

有趣的是,您认为接口更加灵活,并通过未来的变化证明了这一点。如果将来您需要在界面中添加一些功能怎么办?这将是一个重大更改,因为实现该接口的每个类必须实现您刚刚添加的方法。基类不是这种情况,因为基类可以简单地提供一个默认实现。 每一个“未来变化”的案例都有不同的故事。在您的情况下,更好的方法是使用抽象类,但在这样做时,我可能也更喜欢接口抽象类(I-和-base)。同样从不同的角度来看,如果您是使用“类”的人,而不是设计它的人,则最好看到两个或三个方法,而不是一大堆。在这种情况下,接口在关注点分离方面更好。您可以使用一个界面做一件事,而另一个界面做另一件事。所有这些接口实际上可能是一个类。 关于向接口添加功能:通常不会向接口添加新功能(合同类型或存储库并非如此),您可以新建一个(c#中的接口可以从另一个派生接口),前提是关注点分离实现得很好。 我不喜欢这种争论。不应该因为重构的原因而进行设计。一个好的设计很容易重构,而不是接口使用的继承。软件更改,当然,但这可以通过注意什么样的需求将解决超时,所以,如果我没有错,即使是继承,一个好的继承树也会添加、修改和/或删除逻辑,但是其含义应保持“原样”。 @CodyGray 和 Matias 所说的,乘以 10。

以上是关于在设计 C# 类库时,我应该何时选择继承而不是接口? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

何时在 c# 中使用 ArrayList 而不是 array[]?

我啥时候应该使用结构而不是类?

C# 转换继承的通用接口

C# 应该有多重继承吗? [关闭]

桥接模式

使用与多重继承相关的接口的任何真实示例