在这种情况下,我应该选择组合还是继承?


11

考虑一个接口:

interface IWaveGenerator
{
    SoundWave GenerateWave(double frequency, double lengthInSeconds);
}

此接口由许多类实现,这些类生成不同形状的波(例如SineWaveGeneratorSquareWaveGenerator)。

我想实现一个SoundWave基于音乐数据而不是原始声音数据生成类的类。它会收到音符的名称和以拍子(不是秒)为单位的长度,并在内部使用该IWaveGenerator功能来创建一个音符SoundWave

问题是,应该NoteGenerator包含IWaveGenerator还是应该从IWaveGenerator实现中继承?

由于两个原因,我倾向于合成:

1 -它让我注入任何IWaveGeneratorNoteGenerator动态。另外,我只需要一个NoteGenerator类,而不是SineNoteGeneratorSquareNoteGenerator等等。

2-无需NoteGenerator公开由定义的较低级接口IWaveGenerator

但是,我发布此问题是为了听取对此的其他意见,也许是我没有想到的要点。

顺便说一句:我会说NoteGenerator 概念上讲是an,IWaveGenerator因为它生成SoundWaves。

Answers:


14

它允许我动态地将任何IWaveGenerator注入NoteGenerator。另外,我只需要一个NoteGenerator类,而不是SineNoteGeneratorSquareNoteGenerator等。

这是一个明显的信号,最好在这里使用组合,而不要继承SineGeneratorSquareGenerator或(更差)两者。但是,IWaveGenerator如果您稍稍更改了后者,那么直接从NoteGenerator继承便会很有意义。

真正的问题在这里,它可能是有意义的有NoteGenerator与类似的方法

SoundWave GenerateWave(string noteName, double noOfBeats, IWaveGenerator waveGenerator);

但是没有方法

SoundWave GenerateWave(double frequency, double lengthInSeconds);

因为此接口过于具体。您希望IWaveGenerators是生成SoundWaves的对象,但是当前您的接口表示IWaveGenerators是仅根据频率和length生成SoundWaves的对象。这样更好地设计这样的界面

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

并通过,a 或您想到的任何其他生成器的构造函数传递诸如frequency或的参数lengthInSeconds,或一组完全不同的参数。这将允许您创建具有完全不同的构造参数的其他类型的。也许您想要添加一个需要一个频率和两个长度参数的矩形波发生器,或者类似的东西,也许您想接下来添加一个至少包含三个参数的三角波发生器。或者,与构造函数的参数,和。SineWaveGeneratorSquareGeneratorIWaveGeneratorNoteGeneratornoteNamenoOfBeatswaveGenerator

因此,这里的一般解决方案是将输入参数与输出函数解耦,并且仅使输出函数成为接口的一部分。


有趣的是,还没有想到这一点。但是我想知道:这种做法(在构造函数中将“将参数设置为多态函数”设置)通常在现实中可行吗?因为那样的话,代码确实必须知道它要处理的类型,从而破坏了多态性。您能举个可行的例子吗?
阿维夫·科恩

2
@AvivCohn:“代码确实必须知道它要处理的是哪种类型”-不,这是一个误解。只有代码的那部分构造了特定类型的生成器(我是工厂),并且必须始终知道它处理的哪种类型。
布朗

...如果你需要使你的对象的施工过程中多态的,你可以使用“抽象工厂”模式(en.wikipedia.org/wiki/Abstract_factory_pattern
布朗博士

这是我会选择的解决方案。小型且不可变的类是正确的选择。
斯蒂芬

9

NoteGenerator是否在概念上是IWaveGenerator无关紧要。

仅当您计划根据Liskov替换原理(即具有正确的语义和正确的语法)实现该确切接口时,才应从该接口继承。

听起来您的NoteGenerator可能在语法上具有相同的接口,但是其语义(在这种情况下,它所采用的参数的含义)将非常不同,因此在这种情况下使用继承会产生极大的误导性,并且容易出错。您在这里更喜欢构图是正确的。


实际上,我并不是说NoteGenerator会实现,GenerateWave而是以不同的方式解释参数,是的,我同意这将是一个糟糕的主意。我的意思是NoteGenerator是波形发生器的一种专业化:它能够接收“更高级别”的输入数据,而不仅仅是原始声音数据(例如,音符名称而不是频率)。即sineWaveGenerator.generate(440) == noteGenerator.generate("a4")。因此出现了问题,组成或继承。
阿维夫·科恩

如果您可以提出一个同时适合高电平和低电平波形生成类的接口,那么继承是可以接受的。但这似乎非常困难,而且几乎没有任何实际好处。构图绝对似乎是更自然的选择。
Ixrec 2015年

@Ixrec:实际上,对于所有类型的生成器都只有一个接口不是很困难,OP应该同时做这两种,使用组合注入低级生成器从简化的接口继承(但不能从继承的接口继承NoteGenerator)低级生成器实现)请参阅我的答案。
布朗

5

2-无需NoteGenerator公开由IWaveGenerator定义的较低级接口。

听起来NoteGenerator不是WaveGenerator,所以因此不应实现该接口。

构图是正确的选择。


我会说NoteGenerator 概念上讲是an,IWaveGenerator因为它会生成SoundWaves。
阿维夫·科恩

1
好吧,如果它不需要暴露GenerateWave,那么它就不是IWaveGenerator。但是听起来它使用的是IWaveGenerator(也许更多?),因此使用了合成。
埃里克·金

@EricKing:这是一个正确的答案,只要您必须坚持GenerateWave问题中所写的功能即可。但是从上面的评论中,我想这并不是OP真正想到的。
布朗

3

您有坚实的构图理由。您可能还需要增加继承。判断方法是查看调用代码。如果您希望能够NoteGenerator在期望的现有调用代码中使用IWaveGenerator,则需要实现该接口。您正在寻找可替代性的需求。无论从概念上讲,它是否是“是”波发生器,都不在话下。


在那种情况下,即选择组成,但仍需要继承才能实现可替换性,“继承”将被命名为例如IHasWaveGenerator,并且该接口上的相关方法将是GetWaveGenerator返回的实例IWaveGenerator。当然可以更改名称。(我只是想
补充

2

NoteGenerator可以实现该接口,也NoteGenerator可以具有一个内部实现(按组成方式)引用another的内部实现IWaveGenerator

通常,合成会导致代码更具可维护性(即可读性),因为您没有复杂的替代来推理。您对使用继承时具有的类矩阵的观察也很重要,并且可能被认为是指向合成的代码味道。

当您有一个要专门化或自定义的实现时,最好使用继承,这里似乎不是这种情况:您只需要使用该接口即可。


1
这是不正常的NoteGenerator实现IWaveGenerator,因为,因为笔记需要节拍。不是秒。
图兰斯·科尔多瓦

是的,当然,如果没有合理的接口实现,则该类不应实现该接口。但是,OP确实声明“我会说它NoteGenerator在概念上是一个a,IWaveGenerator因为它会生成SoundWaves”,并且他正在考虑继承,因此我很乐意考虑可能会有某种接口实现,即使还有另一个实现也是如此。更好的接口或类的签名。
埃里克·艾德
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.