¿Debo preferir la composición o la herencia en este escenario?

11

Considere una interfaz:

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

Esta interfaz se implementa mediante una serie de clases que generan ondas de diferentes formas (por ejemplo, SineWaveGeneratory SquareWaveGenerator).

Quiero implementar una clase que genere SoundWavedatos basados ​​en datos musicales, no en sonido sin formato. Recibiría el nombre de una nota y una duración en términos de latidos (no segundos), y utilizaría internamente la IWaveGeneratorfuncionalidad para crear un SoundWaveacorde.

La pregunta es, ¿debería NoteGeneratorcontener IWaveGeneratoro debería heredar de una IWaveGeneratorimplementación?

Me inclino por la composición por dos razones:

1- Me permite inyectar cualquiera IWaveGeneratora la NoteGeneratordinámica. Además, sólo necesito una NoteGeneratorclase, en lugar de SineNoteGenerator, SquareNoteGenerator, etc.

2- No es necesario NoteGeneratorexponer la interfaz de nivel inferior definida por IWaveGenerator.

Sin embargo, estoy publicando esta pregunta para escuchar otras opiniones al respecto, tal vez puntos en los que no he pensado.

Por cierto: diría que NoteGenerator es conceptualmente IWaveGeneratorporque genera SoundWaves.

Aviv Cohn
fuente

Respuestas:

14

Me permite inyectar cualquier IWaveGenerator al NoteGenerator dinámicamente. Además, solo necesito una clase NoteGenerator, en lugar de SineNoteGenerator , SquareNoteGenerator , etc.

Esto es una clara señal de que sería mejor composición de uso aquí, y no hereda de SineGeneratoro SquareGeneratoro (peor) a la vez. Sin embargo, tendrá sentido heredar un NoteGenerator directamente de un IWaveGeneratorsi cambia un poco este último.

El verdadero problema aquí es que probablemente sea significativo tener NoteGeneratorun método como

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

pero no con un método

SoundWave GenerateWave(double frequency, double lengthInSeconds);

porque esta interfaz es muy específica Desea que IWaveGenerators sean objetos que generan SoundWaves, pero actualmente su interfaz expresa IWaveGenerators son objetos que generan SoundWaves exclusivamente de frecuencia y longitud . Así que mejor diseño de esta interfaz de esta manera

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

y pasar parámetros como frequencyo lengthInSeconds, o un conjunto de parámetros completamente diferente a través de los constructores de a SineWaveGenerator, a SquareGenerator, o cualquier otro generador que tenga en mente. Esto le permitirá crear otro tipo de IWaveGenerators con parámetros de construcción completamente diferentes. Tal vez desee agregar un generador de onda rectangular que necesite una frecuencia y dos parámetros de longitud, o algo así, tal vez desee agregar un generador de onda triangular a continuación, también con al menos tres parámetros. O bien, una NoteGenerator, con parámetros del constructor noteName, noOfBeats, y waveGenerator.

Entonces, la solución general aquí es desacoplar los parámetros de entrada de la función de salida y hacer que solo la función de salida forme parte de la interfaz.

Doc Brown
fuente
Interesante, no he pensado en esto. Pero me pregunto: ¿esto (establecer los 'parámetros a una función polimórfica' en el constructor) a menudo funciona en realidad? Porque entonces el código tendría que saber con qué tipo se trata, arruinando así el polimorfismo. ¿Puedes dar un ejemplo donde esto funcionaría?
Aviv Cohn
2
@AvivCohn: "el código tendría que saber de qué tipo se trata" - no, eso es un error. Solo la parte del código que construye el tipo específico de generador (puede ser una fábrica), y que tiene que saber siempre con qué tipo se trata.
Doc Brown
... y si necesita que el proceso de construcción de sus objetos sea polimórfico, puede usar el patrón de "fábrica abstracta" ( en.wikipedia.org/wiki/Abstract_factory_pattern )
Doc Brown
Esta es la solución que elegiría. Clases pequeñas e inmutables es la forma correcta de ir aquí.
Stephen
9

Si NoteGenerator es o no "conceptualmente" un IWaveGenerator no importa.

Solo debe heredar de una interfaz si planea implementar esa interfaz exacta de acuerdo con el Principio de sustitución de Liskov, es decir, con la semántica correcta y la sintaxis correcta.

Parece que su NoteGenerator podría tener sintácticamente la misma interfaz, pero su semántica (en este caso, el significado de los parámetros que toma) será muy diferente, por lo que el uso de la herencia en este caso sería muy engañoso y potencialmente propenso a errores. Tienes razón al preferir la composición aquí.

Ixrec
fuente
En realidad, no quise decir NoteGeneratorque implementaría, GenerateWavepero interpretaría los parámetros de manera diferente, sí, estoy de acuerdo en que sería una idea terrible. Quise decir que NoteGenerator es una especie de especialización de un generador de ondas: es capaz de recibir datos de entrada de 'nivel superior' en lugar de solo datos de sonido sin procesar (por ejemplo, un nombre de nota en lugar de una frecuencia). Es decir sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Entonces viene la pregunta, composición o herencia.
Aviv Cohn
Si puede encontrar una interfaz única que se ajuste a las clases de generación de ondas de alto y bajo nivel, entonces la herencia puede ser aceptable. Pero eso parece muy difícil y es poco probable que tenga beneficios reales. La composición definitivamente parece la opción más natural.
Ixrec
@ Ixrec: en realidad, no es muy difícil tener una sola interfaz para todos los tipos de generadores, el OP probablemente debería hacer ambas cosas, usar composición para inyectar un generador de bajo nivel y heredar de una interfaz simplificada (pero no heredar el NoteGenerator de un implementación de generador de bajo nivel) Ver mi respuesta.
Doc Brown
5

2- No es necesario que NoteGenerator exponga la interfaz de nivel inferior definida por IWaveGenerator.

Parece que NoteGeneratorno es un WaveGenerator, por lo tanto, no debe implementar la interfaz.

La composición es la elección correcta.

Eric King
fuente
Yo diría que NoteGenerator es conceptualmente un IWaveGeneratorporque genera SoundWaves.
Aviv Cohn
1
Bueno, si no necesita exponer GenerateWave, entonces no es un IWaveGenerator. Pero parece que usa un IWaveGenerator (¿quizás más?), Por lo tanto, composición.
Eric King
@EricKing: esta es una respuesta correcta siempre y cuando uno tenga que apegarse a la GenerateWavefunción tal como está escrita en la pregunta. Pero por el comentario anterior, supongo que eso no es lo que el OP realmente tenía en mente.
Doc Brown
3

Tienes un caso sólido para la composición. Es posible que tenga un caso para agregar también herencia. La forma de saberlo es mirando el código de llamada. Si desea poder utilizar un NoteGeneratorcódigo de llamada existente que espera un IWaveGenerator, entonces necesita implementar la interfaz. Estás buscando una necesidad de sustituibilidad. Si conceptualmente es "un generador de onda" no viene al caso.

Karl Bielefeldt
fuente
En ese caso, es decir, elegir la composición, pero aún necesita esa herencia para que la sustituibilidad suceda, la "herencia" se nombraría IHasWaveGenerator, por ejemplo , y el método relevante en esa interfaz sería el GetWaveGeneratorque devuelve una instancia de IWaveGenerator. Por supuesto, los nombres se pueden cambiar. (Solo estoy tratando de dar más detalles, avíseme si mis detalles están mal)
Rwong
2

Está bien NoteGeneratorimplementar la interfaz y también NoteGeneratortener una implementación interna que haga referencia (por composición) a otra IWaveGenerator.

En general, la composición da como resultado un código más fácil de mantener (es decir, legible), porque no tiene complejidades de anulaciones para razonar. Tu observación sobre la matriz de clases que tendrías al usar la herencia también es correcta, y probablemente se pueda considerar como un olor a código que apunta hacia la composición.

La herencia se usa mejor cuando tienes una implementación que deseas especializar o personalizar, lo cual no parece ser el caso aquí: solo necesitas usar la interfaz.

Erik Eidt
fuente
1
No está bien NoteGeneratorimplementarlo IWaveGeneratorporque las notas requieren ritmos. no segundos
Tulains Córdova
Sí, ciertamente si no hay una implementación sensata de la interfaz, entonces la clase no debería implementarla. Sin embargo, el OP sí dijo que "yo diría que NoteGeneratores conceptualmente IWaveGeneratorporque genera SoundWaves", y, al considerar la herencia, tomé la libertad mental por la posibilidad de que pudiera haber alguna implementación de la interfaz, aunque haya otra Mejor interfaz o firma para la clase.
Erik Eidt