Devo preferir composição ou herança neste cenário?

11

Considere uma interface:

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

Essa interface é implementada por várias classes que geram ondas de diferentes formas (por exemplo, SineWaveGeneratore SquareWaveGenerator).

Quero implementar uma classe que gere uma SoundWavebase em dados musicais, não em dados brutos de som. Ele receberia o nome de uma nota e um comprimento em termos de batidas (não segundos) e usaria internamente a IWaveGeneratorfuncionalidade para criar uma SoundWaveconformidade.

A questão é: deve NoteGeneratorconter IWaveGeneratorou deve herdar de uma IWaveGeneratorimplementação?

Estou me inclinando para a composição por dois motivos:

1- Ele me permite injetar qualquer IWaveGeneratorà NoteGeneratordinamicamente. Além disso, eu só preciso de uma NoteGeneratorclasse, em vez de SineNoteGenerator, SquareNoteGenerator, etc.

2- Não há necessidade de NoteGeneratorexpor a interface de nível inferior definida por IWaveGenerator.

No entanto, estou postando esta pergunta para ouvir outras opiniões sobre isso, talvez pontos em que não pensei.

BTW: Eu diria que NoteGenerator é conceitualmente um IWaveGeneratorporque gera SoundWaves.

Aviv Cohn
fonte

Respostas:

14

Ele permite injetar qualquer IWaveGenerator no NoteGenerator dinamicamente. Além disso, eu só preciso de uma classe NoteGenerator, em vez de SineNoteGenerator , SquareNoteGenerator , etc.

Isso é um sinal claro que seria melhor para composição uso aqui, e não herdam SineGeneratorou SquareGeneratorou (pior) ambos. No entanto, fará sentido herdar um NoteGenerator diretamente de um IWaveGeneratorse você o alterar um pouco.

O verdadeiro problema aqui é que provavelmente é significativo ter NoteGeneratorum método como

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

mas não com um método

SoundWave GenerateWave(double frequency, double lengthInSeconds);

porque essa interface é muito específica. Você quer que IWaveGenerators sejam objetos que geram SoundWaves, mas atualmente sua interface expressa IWaveGenerators são objetos que geram SoundWaves exclusivamente de frequência e comprimento . Então, crie melhor essa interface dessa maneira

interface IWaveGenerator
{
    SoundWave GenerateWave();
}

e passar parâmetros como frequencyou lengthInSeconds, ou um conjunto de parâmetros completamente diferente, pelos construtores de a SineWaveGenerator, a SquareGeneratorou qualquer outro gerador que você tenha em mente. Isso permitirá que você crie outro tipo de IWaveGenerators com parâmetros de construção completamente diferentes. Talvez você queira adicionar um gerador de ondas retangulares que precise de uma frequência e dois parâmetros de comprimento, ou algo assim, talvez queira adicionar um gerador de ondas triangulares a seguir, também com pelo menos três parâmetros. Ou, uma NoteGenerator, com parâmetros do construtor noteName, noOfBeats, e waveGenerator.

Portanto, a solução geral aqui é separar os parâmetros de entrada da função de saída e tornar apenas a função de saída parte da interface.

Doc Brown
fonte
Interessante, não pensei nisso. Mas eu me pergunto: isso (definir os 'parâmetros para uma função polimórfica' no construtor) geralmente funciona na realidade? Porque então o código precisaria saber com que tipo ele está lidando, arruinando o polimorfismo. Você pode dar um exemplo de onde isso funcionaria?
Aviv Cohn
2
@AvivCohn: "o código realmente precisaria saber com que tipo está lidando" - não, isso é um equívoco. Somente a parte do código que constrói o tipo específico de gerador (talvez uma fábrica), e que precisa saber sempre com qual tipo ele está lidando.
Doc Brown
... e se você precisar tornar polimórfico o processo de construção de seus objetos, poderá usar o padrão "fábrica abstrata" ( pt.wikipedia.org/wiki/Abstract_factory_pattern )
Doc Brown
Esta é a solução que eu escolheria. Classes pequenas e imutáveis ​​são o caminho certo a seguir.
Stephen
9

Se o NoteGenerator é ou não "conceitualmente" um IWaveGenerator não importa.

Você só deve herdar de uma interface se planeja implementar essa interface exata de acordo com o Princípio de Substituição de Liskov, ou seja, com a semântica correta e com a sintaxe correta.

Parece que seu NoteGenerator pode ter sintaticamente a mesma interface, mas sua semântica (neste caso, o significado dos parâmetros necessários) será muito diferente, portanto, usar herança neste caso seria altamente enganoso e potencialmente propenso a erros. Você está certo em preferir composição aqui.

Ixrec
fonte
Na verdade, eu não quis dizer que NoteGeneratoriria implementar, GenerateWavemas interpretar os parâmetros de maneira diferente, sim, eu concordo que seria uma péssima idéia. Eu quis dizer que o NoteGenerator é uma espécie de especialização de um gerador de ondas: ele é capaz de captar dados de entrada de 'nível superior' em vez de apenas dados de som não processados ​​(por exemplo, um nome de nota em vez de uma frequência). Ou seja sineWaveGenerator.generate(440) == noteGenerator.generate("a4"). Então chega a pergunta, composição ou herança.
Aviv Cohn
Se você conseguir criar uma interface única que atenda às classes de geração de ondas de alto e baixo nível, a herança poderá ser aceitável. Mas isso parece muito difícil e improvável de trazer benefícios reais. Composição definitivamente parece ser a escolha mais natural.
Ixrec
@Ixrec: na verdade, não é muito difícil ter uma interface única para todos os tipos de geradores, o OP provavelmente deve fazer os dois, usar a composição para injetar um gerador de baixo nível e herdar de uma interface simplificada (mas não herdar o NoteGenerator de um implementação de gerador de baixo nível) Veja minha resposta.
Doc Brown
5

2- Não é necessário que o NoteGenerator exponha a interface de nível inferior definida pelo IWaveGenerator.

Parece que NoteGeneratornão é um WaveGenerator, portanto, não deve implementar a interface.

Composição é a escolha correta.

Eric King
fonte
Eu diria que NoteGenerator é conceitualmente um IWaveGeneratorporque gera SoundWaves.
Aviv Cohn
1
Bem, se não precisa se expor GenerateWave, não é IWaveGenerator. Mas parece que ele usa um IWaveGenerator (talvez mais?), Daí a composição.
Eric King
@ EricKing: esta é uma resposta correta, desde que seja necessário manter a GenerateWavefunção conforme está escrito na pergunta. Mas pelo comentário acima, acho que não é isso que o OP realmente tinha em mente.
Doc Brown
3

Você tem um argumento sólido para composição. Você pode ter um caso para adicionar também herança. A maneira de saber é observando o código de chamada. Se você quiser usar um NoteGeneratorno código de chamada existente que espera um IWaveGenerator, precisará implementar a interface. Você está procurando uma necessidade de substituibilidade. Se conceitualmente "é um gerador de ondas" está fora de questão.

Karl Bielefeldt
fonte
Nesse caso, ou seja, escolhendo a composição, mas ainda precisando dessa herança para que a substituibilidade aconteça, a "herança" seria nomeada IHasWaveGenerator, por exemplo , e o método relevante nessa interface seria o GetWaveGeneratorque retorna uma instância de IWaveGenerator. É claro que a nomeação pode ser alterada. (Eu só estou tentando carne para fora mais detalhes - deixe-me saber se meus dados estão errados.)
rwong
2

É bom NoteGeneratorimplementar a interface e também NoteGeneratorter uma implementação interna que faça referência (por composição) a outra IWaveGenerator.

Geralmente, a composição resulta em um código mais sustentável (ou seja, legível), porque você não tem complexidades de substituições para raciocinar. Sua observação sobre a matriz de classes que você teria ao usar herança também é relevante e provavelmente pode ser pensada como um cheiro de código apontando para a composição.

A herança é melhor usada quando você tem uma implementação que deseja especializar ou personalizar, o que não parece ser o caso aqui: você só precisa usar a interface.

Erik Eidt
fonte
1
Não é bom NoteGeneratorimplementar IWaveGeneratorporque porque as notas exigem batidas. não segundos-.
Tulains Córdova
Sim, certamente, se não houver uma implementação sensata da interface, a classe não deve implementá-la. No entanto, o OP afirmou que "eu diria que NoteGeneratoré conceitualmente um IWaveGeneratorporque gera SoundWaves", e, como ele estava considerando a herança, tomei latitude mental pela possibilidade de que houvesse alguma implementação da interface, mesmo que exista outra melhor interface ou assinatura para a classe.
precisa