设计C#类库时,何时应选择通过接口继承?[关闭]


77

我有一个数字Processor类,可以做两种非常不同的事情,但是是从通用代码中调用的(“控制反转”情况)。

我想知道在决定它们是否都应该继承自BaseProcessor或实现IProcessor为接口时,我应该了解哪些设计考虑因素(或者对您来说是对用户有用的认识)。


4
另请参见并非针对C#的更笼统的问题:接口与基类
科迪·格雷

Answers:


185

通常,规则如下所示:

  • 继承描述了is-a关系。
  • 实施接口描述了可以做的关系。

为了更具体地说明这一点,让我们看一个例子。的System.Drawing.Bitmap是-一个图像(并且同样地,其从继承Image类),但它也可以做的配置,所以它实现了IDisposable接口。它还可以进行序列化,因此可以从ISerializable接口实现。

但是实际上,接口通常用于模拟C#中的多重继承。如果您的Processor类需要继承自System.ComponentModel.Component,则您别无选择,只能实现一个IProcessor接口。

事实是,接口和抽象基类都提供了指定特定类可以做什么的协定。众所周知,接口必须声明此合同,但这是不正确的。在我看来,最大的好处是抽象基类允许您为子类提供默认功能。但是,如果没有有意义的默认功能,则没有什么可以阻止您将方法本身标记为abstract,要求派生类自己实现它,就像它们要实现接口一样。

对于此类问题的答案,我经常转向.NET Framework设计指南,其中有关于在类和接口之间进行选择的说明:

通常,类是公开抽象的首选构造。

接口的主要缺点是,在允许API演变时,它们不如类灵活。交付接口后,其成员集合将永久固定。接口的任何添加都会破坏实现接口的现有类型。

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

[。。。]

支持接口的最常见论点之一是,它们允许将协定与实现分开。但是,该参数错误地假定您无法使用类将合同与实现分开。驻留在独立于其具体实现的单独程序集中的抽象类是实现此类分离的好方法。

他们的一般建议如下:

  • 不要在接口上偏爱定义类。
  • 不要使用抽象类而不是接口来将契约与实现分离。如果正确定义了抽象类,则可以在合同和实现之间实现相同程度的解耦。
  • 不要,如果你需要提供一个值类型的多态层次定义一个接口。
  • 考虑定义接口以实现与多重继承相似的效果。

克里斯·安德森(Chris Anderson)对此最后一个宗旨表示特别同意,认为:

抽象类型的版本要好得多,并允许将来扩展,但是它们也会消耗您唯一的基本类型。当您真正定义两个对象之间随时间变化的契约时,接口是合适的。抽象基类型更适合为一系列类型定义公共基。


1
谢谢-非常好的答案。有一个警告:如果您打算将基类发布为“公共类型”,则建议该类的用户使用THERE OWN基类来继承该类...给它们一些地方以使它们通用行为(将来)。...感谢Link的支持。我要读一些好书;-)
corlettk 2011年

17
应该注意的是,界面更多是“必须要做的”。
2012年

鉴于最近在Java 8中所做的更改以在接口中引入默认方法,这一答案变得更加正确。否则,在不破坏实现接口的每段代码的情况下,没有其他方法可以引入新的合同/方法。(尽管此更改使抽象类和接口之间的界限变得更加模糊)
Harshdeep

1
这不是“ Head First设计模式”的主旨的反面。我无论如何都不是专家,但是他们的口头禅更喜欢组合而不是继承。有人可以解开这个吗?
alimack

@ali我根本看不到矛盾。当然,自从我写出这个答案已经有很多年了,但是在再次略读它时,我看不到我提倡继承而不是合成的主张。实际上,它没有任何关于合成的内容。如果您不需要通过继承建模的牢固关系,那么组合是更好的选择。不要表达比您需要的牢固的关系。
科迪·格雷

10

理查德

为什么选择它们?我将有一个IProcessor接口作为已发布的类型(供系统中的其他位置使用);如果碰巧您的IProcessor的各种CURRENT实现都有共同的行为,那么抽象BaseProcessor类将是实现该共同行为的真正好地方。

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

只是我谦虚的观点。

干杯。基思


谢谢Keith-这是一个好点。如果有任何考虑因素会折磨我,让我选择一个而不是另一个,我真的很徘徊。很好地实现这两个方面。
可能在海滩上

1
嗯,我不同意这一点。如果没有该接口的具体实现,通常不应公开该接口。暴露接口的另一个问题是版本控制(有关详细信息,请参见我的答案)。我认为您无需同时执行这两项操作。这并不总是设计决策的最佳答案。尝试同时进行这两项工作的代价是增加了复杂性,而我很难看到它在这里的真正优势。只需使用抽象基类而不是接口,您就不会错过任何事情。
科迪·格雷

5

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

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

什么时候使用接口?

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

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

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


例子:

让我们谈谈表格。

  • 我们接受可回收表=>必须使用“ IRecyclableTable”之类的接口定义此,以确保所有可回收表都将具有“ Recycle”方法

  • 我们希望桌面表=>必须使用继承定义。“桌面表”是“表”。所有表都具有相同的属性和行为,而桌面表将具有相同的属性和行为,从而增加了使桌面表与其他类型的表不同的功能。

我可以谈论关联,即对象图中这两种情况的含义,但以我谦卑的观点,如果我需要从概念的角度给出论据,我会用这种论证来回答。


2
我不太确定“可回收表”是什么,但是我可能会实现一个IRecyclable接口,因为似乎几个不相关的对象可能需要可回收(即,其他不代表表的类)。然后,我仍然可以从Table该类中实现所有表,包括DesktopTableandPicnicTableFoldingTable
科迪·格雷

而且,如果您仔细检查我的文字,是否发现我说的话与您不同?:)阅读有关“可回收表”的入门知识:“示例”。这只是一个示例,与任何对象图,设计或实际案例绝对隔离。
马蒂亚斯Fidemraizer

无论如何,谢谢您的建议,我的话不要错,嘿!:)
马蒂亚斯Fidemraizer


2

我在设计选择上不太擅长,但是如果有要求,我将更喜欢在仅扩展成员的情况下实现iProcessor接口。如果还有其他功能不需要扩展,则从baseprocessor继承是更好的选择。


@jitendra-谢谢,您能否提供更多有关为什么这是一个更好选择的详细信息?
可能在海滩上

您为什么更喜欢界面?接口比抽象基类有什么优势?我完全不同意您的直觉,除非我特别需要模拟多重继承,否则我总是会在接口上选择一个抽象基类。
科迪·格雷

如果只有几个函数,那么继承就更有意义了,因为您不必实现基类中可用的每个函数。而且,您没有足够的信息来了解基类的功能和参数的完整列表。因此,在这种情况下,继承更有意义。
jitendragarg

另外,即使应该稍后使用基类,也始终可以创建普通的基类而不是抽象基类,并且仅继承所需的功能。但是,在那种情况下,性能会受到影响,因为接口比类轻。另外,在抽象类上使用接口的情况下,其他开发人员甚至不需要实现每个函数,只需使用基类中的几个函数即可。因此,基类的可重用性也是一个因素,您在进行此类选择之前应考虑。
jitendragarg

@cody gray如果我没有记错的话,那么您应该实现整个抽象类,这对于可重用性来说是个坏主意。同样,这只是我的观点。可能是错误的。如果有误,请编辑评论。
jitendragarg

2

除了设计纯度,您只能继承一次,因此,如果您需要继承某个框架类,那么这个问题就没有意义了。

如果可以选择,可以实用地选择保存最多键入内容的选项。无论如何,它通常是纯粹的选择。

编辑:

如果基类将具有某种实现,那么这可能会很有用,如果仅是抽象知识,那么它也可能是接口。


这是个好的观点。通常,选择基类可以节省最多的键入时间。如果您的方法具有多个重载,而所有重载都调用一个核心函数,则可以将所有参数验证,边界检查和函数重载放在基类中。这意味着派生类仅需要实现核心的“肉和土豆”功能。这不是接口的选项,其中实现该接口的每个每次都必须完成所有工作。
科迪·格雷

@Cody Gray关于最后一部分,但是也许基类实现了一个接口,并且接口实现是虚拟的,所以这是一种混合情况!开玩笑,没关系。
马蒂亚斯Fidemraizer

1

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

另外,如果您需要在界面中添加“其他功能”,最好的选择是按照SOLID中的I(即接口隔离原则)创建一个新的界面。


0

如果选择的内容无关紧要,请始终选择“界面”。它具有更大的灵活性。它可以保护您免受将来的更改(如果您需要更改基类中的某些内容,则继承的类可能会受到影响)。它还可以更好地封装细节。如果您正在使用某种依赖注入控制反转,则它们倾向于使用接口。

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

与ASP.NET Mvc中的ControllerBase和IController相似。


3
有趣的是,您认为接口更灵活,可以在将来进行更改时证明这一点。如果将来您需要在界面中添加一些功能呢?这将是一个巨大的变化,因为实现该接口的每个类必须实现您刚刚添加的方法。基类不是这种情况,因为基类可以简单地提供一个默认实现。
科迪·格雷

每个“未来变化”案例都有不同的故事。在您的情况下,更好的方法是使用抽象类,但这样做时,我可能还希望使用接口抽象类(I-和-base)。同样从不同的角度来看,如果您是使用“类”的人,而不是使用设计“类”的人,则更希望看到两种或三种方法,而不是一整套方法。在这种情况下,界面在关注点分离方面更好。您可以使用一个界面执行某项操作,而另一个界面则可以执行另一项操作。所有这些接口实际上可能是一个类。
亨德利

关于向接口添加功能:通常不向接口添加新功能(合同类型或存储库不是这种情况),可以创建新的功能(c#中的接口可以从另一个接口派生)。实施得很好。
Hendry

1
我不喜欢这种说法。由于重构原因,不应进行设计。好的设计易于重构,并且与接口用法的继承无关。当然,可以进行软件更改,但是可以这样做,但要注意要解决什么样的需求,才能解决超时问题,因此,即使我没错,即使继承了,好的继承树也可以添加,修改和/或删除逻辑,但是其含义应保持“原样”。
马蒂亚斯Fidemraizer

@CodyGray和Matias说的,时间
10。– MikeSchinkel,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.