Должен ли я предпочесть композицию или наследование в этом сценарии?

11

Рассмотрим интерфейс:

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

Этот интерфейс реализован рядом классов, которые генерируют волны различной формы (например, SineWaveGeneratorи SquareWaveGenerator).

Я хочу реализовать класс, который генерирует SoundWaveмузыкальные данные, а не необработанные звуковые данные. Он получит имя заметки и длину в виде ударов (не секунд) и внутренне использует IWaveGeneratorфункциональные возможности для создания SoundWaveсоответствующего.

Вопрос в том, должен ли он NoteGeneratorсодержать IWaveGeneratorили должен наследоваться от IWaveGeneratorреализации?

Я склоняюсь к композиции по двум причинам:

1- Это позволяет мне вводить любого IWaveGeneratorк NoteGeneratorдинамически. Кроме того , мне нужен только один NoteGeneratorкласс, вместо SineNoteGenerator, SquareNoteGeneratorи т.д.

2. Нет необходимости NoteGeneratorвыставлять интерфейс нижнего уровня, определенный IWaveGenerator.

Однако я публикую этот вопрос, чтобы услышать другие мнения по этому поводу, может быть, пункты, о которых я не задумывался.

Кстати, я бы сказал NoteGenerator , концептуально, IWaveGeneratorпотому что он генерирует SoundWaves.

Авив Кон
источник

Ответы:

14

Это позволяет мне динамически вводить любой IWaveGenerator в NoteGenerator. Кроме того , мне нужен только один класс NoteGenerator, вместо SineNoteGenerator , SquareNoteGenerator и т.д.

Это явный признак, что было бы лучше использовать композицию здесь, а не наследовать от SineGeneratorили SquareGeneratorили (хуже) от обоих. Тем не менее, имеет смысл наследовать NoteGenerator напрямую от, IWaveGeneratorесли вы немного измените последний.

Реальная проблема здесь в том, что, вероятно , смысл иметь NoteGeneratorс методом , как

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

но не методом

SoundWave GenerateWave(double frequency, double lengthInSeconds);

потому что этот интерфейс слишком специфичен. Вы хотите, чтобы IWaveGenerators были объектами, которые генерируют SoundWaves, но в настоящее время ваш интерфейс выражает IWaveGenerators как объекты, которые генерируют SoundWaves исключительно по частоте и длине . Так лучше спроектировать такой интерфейс таким образом

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

и передавать параметры, такие как frequencyили lengthInSeconds, или совершенно другой набор параметров через конструкторы a SineWaveGenerator, a SquareGeneratorили любого другого генератора, который вы имеете в виду. Это позволит вам создавать другие виды IWaveGeneratorс совершенно другими параметрами конструкции. Может быть, вы хотите добавить генератор прямоугольных волн, для которого нужны параметры частоты и два параметра длины, или что-то в этом роде, может быть, вы хотите добавить генератор треугольных волн, также по крайней мере с тремя параметрами. Или же NoteGenerator, с параметрами конструктора noteName, noOfBeatsи waveGenerator.

Таким образом, общее решение здесь состоит в том, чтобы отделить входные параметры от выходной функции и сделать только выходную функцию частью интерфейса.

Док Браун
источник
Интересно, не думал об этом. Но мне интересно: часто ли это (установка «параметров для полиморфной функции» в конструкторе) часто работает в реальности? Потому что тогда код действительно должен знать, с каким типом он имеет дело, тем самым разрушая полиморфизм. Можете ли вы привести пример, где это будет работать?
Авив Кон
2
@AvivCohn: «код действительно должен знать, с каким типом он имеет дело» - нет, это неправильное представление. Только часть кода , который строит тип конкретной генератора (Mybe завод), и что должен знать всегда , какой тип он имеет дело.
Док Браун
... и если вам нужно сделать процесс создания ваших объектов полиморфным, вы можете использовать шаблон "абстрактной фабрики" ( en.wikipedia.org/wiki/Abstract_factory_pattern )
Док Браун,
Это решение, которое я бы выбрал. Маленькие, неизменные классы - правильный путь сюда.
Стивен
9

Является ли NoteGenerator «концептуально» IWaveGenerator или нет, не имеет значения.

Вы должны наследовать от интерфейса, только если планируете реализовать этот точный интерфейс в соответствии с принципом подстановки Лискова, то есть с правильной семантикой и правильным синтаксисом.

Похоже, ваш NoteGenerator может иметь синтаксически тот же интерфейс, но его семантика (в данном случае значения параметров, которые он принимает) будет сильно отличаться, поэтому использование наследования в этом случае будет очень вводящим в заблуждение и потенциально подверженным ошибкам. Вы правы, предпочитая композицию здесь.

Ixrec
источник
На самом деле я не имел в виду, NoteGeneratorчто реализовывал бы, GenerateWaveа интерпретировал параметры по-другому, да, я согласен, что это была бы ужасная идея. Я имел в виду, что NoteGenerator является своего рода специализацией волнового генератора: он может принимать входные данные «более высокого уровня», а не только необработанные звуковые данные (например, имя ноты вместо частоты). То есть sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Таким образом, возникает вопрос, состав или наследство.
Авив Кон
Если вы можете придумать единый интерфейс, который подходит как для генерации волн высокого, так и для низкого уровня генерации, тогда наследование может быть приемлемым. Но это кажется очень трудным и вряд ли принесет какие-либо реальные выгоды. Композиция определенно кажется более естественным выбором.
Ixrec
@Ixrec: на самом деле, не очень сложно иметь единый интерфейс для всех типов генераторов, ФП, вероятно, должен делать и то, и другое, использовать композицию для внедрения генератора низкого уровня и наследовать от упрощенного интерфейса (но не наследовать NoteGenerator от реализация генератора низкого уровня) Смотрите мой ответ.
Док Браун
5

2 - Нет необходимости для NoteGenerator выставлять интерфейс нижнего уровня, определенный IWaveGenerator.

Похоже, NoteGeneratorэто не WaveGeneratorтак, поэтому не следует реализовывать интерфейс.

Композиция - это правильный выбор.

Эрик Кинг
источник
Я бы сказал NoteGenerator , концептуально, IWaveGeneratorпотому что он генерирует SoundWaveс.
Авив Кон
1
Ну, если это не нужно подвергать GenerateWave, то это не так IWaveGenerator. Но, похоже, он использует IWaveGenerator (может, больше?), Следовательно, композицию.
Эрик Кинг,
@EricKing: это правильный ответ, если нужно придерживаться GenerateWaveфункции, как написано в вопросе. Но из комментария выше я думаю, что это не то, что на самом деле имел в виду ФП.
Док Браун
3

У вас есть веские аргументы в пользу композиции. У вас может быть дело также добавить наследство. Способ сказать, посмотрев на код вызова. Если вы хотите иметь возможность использовать NoteGeneratorв существующем коде вызова, который ожидает IWaveGenerator, то вам нужно реализовать интерфейс. Вы ищете потребность в заменяемости. Является ли это концептуальным генератором волн "это что-то", не имеет значения.

Карл Билефельдт
источник
В этом случае, т. Е. При выборе композиции, но все еще нуждающемся в этом наследовании для обеспечения возможности замены, будет названо, например IHasWaveGenerator, «наследование» , и соответствующий метод на этом интерфейсе будет GetWaveGeneratorвозвращать экземпляр IWaveGenerator. Конечно, название можно изменить. (Я просто пытаюсь
уточнить
2

Это хорошо для NoteGeneratorреализации интерфейса, а также для того, NoteGeneratorчтобы иметь внутреннюю реализацию, которая ссылается (по составу) на другую IWaveGenerator.

Как правило, компоновка приводит к более поддерживаемому (то есть читаемому) коду, потому что у вас нет сложностей переопределений для пересмотра. Ваше наблюдение за матрицей классов, которую вы имели бы при использовании наследования, также является точным и, вероятно, может восприниматься как запах кода, указывающий на композицию.

Наследование лучше использовать, когда у вас есть реализация, которую вы хотите специализировать или настроить, что, похоже, не так: вам просто нужно использовать интерфейс.

Эрик Эйдт
источник
1
Это не для NoteGeneratorреализации, IWaveGeneratorпотому что примечания требуют ударов. не секунды.
Тулаинс Кордова
Да, конечно, если нет разумной реализации интерфейса, то класс не должен ее реализовывать. Тем не менее, OP заявил, что «я бы сказал NoteGenerator, концептуально, IWaveGeneratorпотому что он генерирует SoundWaves», и он рассматривал наследование, поэтому я взял умственную широту для возможности, что может быть некоторая реализация интерфейса, даже если есть другая лучший интерфейс или подпись для класса.
Эрик Эйдт