Composicao ao invés de heranca mesmo quando temos uma relacao IS-A

Recentemente peguei uma relacao IS-A e transformei numa HAS-A na forca bruta, ou seja, peguei uma arquitetura baseada em heranca e classes abstratas e transformei numa baseada em pura composicao mais wrappers.

Client interface -> AbstractClient -> AbstractTcpClient -> AsciiLineTcpClient

foi transformado em:

Client -> BaseClient

TcpClient contem BaseClient e é também um Client

AsciiLineTcpClient contem TcpClient e é também um Client.

Um client se comunica com outro via ClientListener, e delega a maioria dos métodos para o client que ele contém via delegation.

A bola que quero levantar para o debate é:

Tirando os famigerados testes unitários, que entendo são beneficiados por composicão e dificultados por heranca, quais seriam as outras vantagens de composicao sobre heranca?

Eu trabalhei num lugar que dava preferencia por heranca e agora estou trabalhando num lugar que dá preferencia por composicão. As minhas impressões:

:arrow: Heranca é mais dificil de ser feito direito, mas uma vez feito o código fica mais bonito, limpo e de melhor compreecão.

:arrow: Protected foi disponibilizado para facilitar de o cara fazer milhões de cagadas com heranca. Dá muito bem para programar com heranca sem variáveis protected.

:arrow: Métodos protected até fazem um certo sentido, pelo menos essa é minha impressão.

:arrow: Composicão funciona também mas a arquitetura é menos natural, parecendo um Matrioshka. Fica mais difícil do cara olhar e entender o que acontece.

:arrow: Heranca é mais difícil de fazer direito, mas se o cara faz direito fica bonito. Composicao é mais seguro e menos propicio do cara fazer cagada.

Nota: Estou falando em claramente favorecer composicao (mesmo quando é possível haver uma relacao is-a) através de wrappers via interfaces, ou seja, ao invés do meu AsciiLineTcpClient extender um TcpClient, ele CONTEM o TcpClient e implementa o Client para delegar.

O pessoal fala de polimorfismo e heranca, mas com interfaces também é possível obter polimorfismo.

Fica colocando um objeto dentro do outro não me parece muito natural e fica mais difícil de entender porque olhando uma classe vc nunca vai saber quantas camadas tem ali. Quando vc tem heranca isso fica claro.

Seria isso uma questão de gosto? Opiniões?

Cara, sou iniciante não tenho vergonha nenhuma disso! hehehe.
Mas se não me engano, sem a herança não há polimorfismo!

E ai quebra tudo! depende da aplicação no caso né?
E a estrutura java também é feito de herança! por exemplo a classe String é subclasse de Object!

Dai pra frente amigo, não intende muita coisa não! hehehe. gostei do tópico e espero que ele de desenvolva!

cara sei la… eu to no segundo ano do ensino medio… tava lendo uns livros de padroes de projeto e eu conclui que composição é otimo porque fica propicio a alterações e etc… e outra que com composição voce pode controlar varias coisas em tempo de execução… coisa q, algumas vzs, é mais facil doq por herança. Eu concordo que o codigo fica mais bonito, mas por questao de “segurança” e encapsulamento é melhor fzr por composição… e outra… se vc adota os padroes de sintaxe JAVA fica 1531651351613516513516415 de vzs mais facil de entender o codigo… ruim msm é qdo vc pega uns codigo com atributo x, y, z, a, b, variavel temporaria pra dar e vender, contadora, etc. chega a doer o olho…

Mas enfim… prefiro composição… entretanto as vzs a gente precisa programar com herança…

se eu falei merda me corrijam por favor, sou novo em java.

Abraços

Posso estar falando bobeira , mas na minha opinião Composição é um modo que os desenvolvedores do Java acharam de evitar
heranças multiplas por exemplo se voce tem a classe Pai e a Classe Padrasto os dois possuem atributos similares e um método:

darMesada();

// Só que o Pai gosta de calcular 100 reais ao dia para o filhão

public class Pai {

public double darMesada() {

		double valorMesada = 100 * 30;
		return valorMesada;
	}
}

public class Padrasto   {
// ja o padrasto não vai desembolsar 300,00 para o entiado Mal-Educado e mimado pelo pai
public double darMesada() {

		double valorMesada = 1 * 30;
		return valorMesada;
	}
}

Agora vamos imaginar o PaiDeFamilia que se casou mas a moça ja tinha um filho e ele esta feliz porque vai ser Papai … que Lindo ela esta de 7 meses :lol:
Mas ai ele entra no dilema eu vou extends Pai e Padrasto … qual metódo ele ira implementar ? Sua vida se foi no diamante da Morte :twisted:

Ai alguem lá na dead Sun :cry: Teve a ideia de criar a interface - ChefeDeFamilia - que ja tras todas suas obrigações detalhadas para voce implementar da maneira que achar melhor :smiley:

Eu vejo desta forma , as duas tem suas respectivas vantagens .! Para cada caso um caso… Do mesmo modo que uma classe CheDeFamilia poderia ter sido criada tendo ali alguns metodos abstratos , porem eu penso assim se é para ser abstrato entao já existe interface para isso.

outra coisa … composição também pode ser feita entre classes ou não?
classe Carro - id,porta,banco,tapete,roda,Motor
classe Motor - id,pistao,cabecote,correia,carburador

???

Não seria isso polimorfismo tb?

interface Client

Client aClient

aClient.send(“asfsdafa”)

Interface e composicao resolve a ausência de herança múltipla, mas vc pode favorecer composicao ao invés de heranca mesmo quando não há heranca multipla.

Sim isso não deixa de ser polimorfismo , mas na minha opinião o polimorfismo acontece mesmo quando voce consegue invocar um método da classe pai com a mesma assinatura , que dependendo do classe que invocar o método ele terá um comportamento especifico para cada instancia das classes filhas mas ainda assim sera o mesmo método, deve-se ter cuidado para não confundir sobrecarga de metodos com polimorfismo.

Exemplo :

public abstract class OperacaoMatematica {
   public abstract double calcular(double x, double y);
}

public class Soma extends OperacaoMatematica {
   public double calcular(double x, double y) {
      return x+y;
   }
}
 
public class Subtracao extends OperacaoMatematica {
   public double calcular(double x, double y) {
      return x-y;
   }

public class Contas {
   public static void mostrarCalculo(OperacaoMatematica operacao, double x, double y) {
       System.out.println("O resultado é: " + operacao.calcular(x, y));
   }
 
   public static void main(String args[]) {
        //Primeiro calculamos uma soma
        Contas.mostrarCalculo(new Soma(), 5, 5); //Imprime o resultado é: 10
        //Depois uma subtração
        Contas.mostrarCalculo(new Subtracao(), 5, 5); //Imprime o resultado é: 0
   }
}
}

Sobrecarga de métodos pode acontecer por exemplo em um construtor quando voce cria um construtor que devera receber obrigatoriamente todos os atributos , e outro que diz :
Me passe pelo menos o ID e o nome que ja consigo realizar métodos e operações com esta instancia , isto é sobrecarga.

Sim, o que eu falei é que polimorfismo não é o trunfo de heranca, porque vc pode consegui-lo sem heranca via interfaces. Inclusive acho que no seu exemplo é muito melhor usar interfaces e evitar a heranca:

public interface OperacaoMatematica {
   public double calcular(double x, double y);
}

public class Soma implements OperacaoMatematica {
   public double calcular(double x, double y) {
      return x+y;
   }
}
 
public class Subtracao implements OperacaoMatematica {
   public double calcular(double x, double y) {
      return x-y;
   }

public class Contas {
   public static void mostrarCalculo(OperacaoMatematica operacao, double x, double y) {
       System.out.println("O resultado é: " + operacao.calcular(x, y));
   }
 
   public static void main(String args[]) {
        //Primeiro calculamos uma soma
        Contas.mostrarCalculo(new Soma(), 5, 5); //Imprime o resultado é: 10
        //Depois uma subtração
        Contas.mostrarCalculo(new Subtracao(), 5, 5); //Imprime o resultado é: 0
   }
}
}

A bola que eu levantei não é em relaćão a isso (polimorfismo), mas sim em relacao há reutilizaćão de funcionalidade. No seu exemplo se operacao matemática (classe abstrata) tivesse um monte de métodos concretos, vc teria duas opcoes:

  • Fazer Soma herdar de OperacaoMatemática.

  • Fazer OperacaoMatemática NÃO ser abstrata e passá-la para Soma via composicao.

São dois caminhos bem diferentes para chegar ao mesmo lugar.

E só completando, o maior problema da herança, ao meu ver, é o acoplamento que é criado na classe que herda.

Verdade, mas se vc herda APENAS de classes abstratas, teoricamente vc meio que acoplou a uma “interface” pois vc vai ter que implementar métodos abstratos.

O que vc está falando é fazer um:

public class MyHashMap extends HashMap { }

Isso é péssimo e composicão é com certeza muito melhor, mesmo o Java não facilitando a delegacao (use o Eclipse gerar automaticamente).

public class MyHashMap implements Map {

   private final Map delegate;

   public MyHashMap(Map delegate) {
      this.delegate = delegate;
   }

  // delega em todos os métodos de Map

Agora se vc tem um AbstractMap, acho que não teria problema usar heranca, mas cada uma acha uma coisa em relacao a isso.

Não. Imagina que tipo de coisas ruins podem acontecer se o mantenedor da classe abstrata resolver mexer nela. E lembrando que uma classe pode ser abstrata e ter métodos concretos (pode ter inclusive APENAS métodos concretos). O acoplamento por herança sempre é maior do que quando se implementa uma interface.

Acho que o maior problema da herança é que ela cria um relacionamento muito forte entre a superclasse e a subclasse. Ela não só gera um compromisso com comportamento, como faz a interface, mas também com uma determinada implementação.

Isso não seria um grande problema se conhecessemos todos os requisitos do negócio antes do projeto começar (aliás, muita coisa não seria um problema se tivessemos esse conhecimento mágico), mas como isso na prática não ocorre, pode ficar muito difícil adicionar um método numa árvore de herança sem que haja estresse (herança negada nos filhos, ou duplicação em vários filhos para não ter que inserir um método no pai).

Sem falar que manutenções no pai devem ser meticulosamente planejadas, pois tem um impacto muitas vezes imprevisível sobre as classes filhas.

Finalmente, a medida que a árvore da herança cresce para baixo, a classe final fica menos coesa. Essa falta de coesão muitas vezes gera uma classe difícil de entender ou gerenciar.

Claro, muitas das coisas entram no quesito “não usar herança direito”. Mas estou considerando um mal uso devido a evolução natural do projeto, quando você deve mudar código com o programa já em produção, quando fica difícil vc simplesmente remodelar a hierarquia sem causar um grande impacto no sistema todo.

E, claro, também não significa que herança não deve existir. Não vejo problema nenhum em haver uma superclasse simplificando o strategy, ou hierarquia com um ou dois níveis de altura.

Complementando o que o rmonico disse, também é bom lembrar que uma interface pode ser implementada em qualquer altura, de qualquer hierarquia de classes. Uma classe abstrata não tem essa flexibilidade, já que uma classe só pode derivar de uma única super classe.

A próprio HashMap usa heranca. Ela extende AbstractMap. Por que optaram por usar heranca ao invés de composicao pura aí?

Acho que composicao teoricamente é mais correto que heranca. Tem mais vantagens.

Mas Java sem delegacao e sem named-parameters torna composicao heavy-weight.

Eu já vi os dois mundos: muita heranca e muita composicao. O meu feeling é que muita composicao fica como aquelas bonecas russas, fácil de testar mais dificil de entender. Muita heranca deixa a coisa mais limpa e encapsulada. Vc apenas herda e pronto, já recebe de maneira abstrata super poderes sem ter que se preocupar muito com a implementacao do pai.

E há diversas maneiras de alterar um pai sem quebrar os filhos. Basta criar um novo método que chama o que precisa ser alterado e faz alguma outra coisa. Eu sempre fui contra testes unitários, e sem essa flexibilidade que heranca te dá, realmente seria impossível fazer quaqluer alteracao segura.

Ex:

public class Pai {

   protected String doSomethingVeryCrazy(String blah) {

        // um código bem complexo está aqui, e quando vc dá um trace callback vc vê que ele é chamado em 100 pontos diferentes
       // e agora, vc vai confiar nos testes unitários ?????
      // nem pensar... quem fez o teste unitário foi um cara que provavelmente estava de saco-cheio escrevendo essa chatice...
      // eu prefiro não deixar margem para qualquer erro...
   }
}

Então como o cara faz um alteracão 100% segura, sem testes unitários???

public class Pai {

   protected String doSomethingVeryCrazy(String blah) {

        // um código bem complexo está aqui, e quando vc dá um trace callback vc vê que ele é chamado em 100 pontos diferentes
       // e agora, vc vai confiar nos testes unitários ?????
      // nem pensar... quem fez o teste unitário foi um cara que provavelmente estava de saco-cheio escrevendo essa chatice...
      // eu prefiro não deixar margem para qualquer erro...
   }

  protected String doSomethingVeryCrazyInADifferentWay(String blah) {

      String s = doSomethingVeryCrazy(blah);

     // faz qualquer coisa louca aqui...

     return result;
  }
}

Daí o carinha que precisa da nova versão do método doSomethingVeryCrazy vai usar doSomethingVeryCrazyInADifferentWay e vc possui 100% de certeza absoluta que nada vai quebrar.

Uma desvantagem que eu vejo: vc pode acabar com muitos métodos protected diferentes… mas a clareza, a dinâmica, a flexibilidade e a seguranca que isso te dá, sem falar em esquecer os testes unitários acho que compensa e muito.

Primeiro, gostaria de dizer que acho este tipo de discussão muito interessante.

Acredito que a herança costuma virar problema em classes "de negócio" e não em classes de "infra", se é que existe a diferença de conceito (acabei de pensar nisso).

Classes de negócio vão evoluindo com o passar do tempo em mais e mais níveis até que, se não tomar cuidado, vira um monstro descontrolado.
(Uma hierarquia Pessoa -> PessoaFisica -> Funcionario seria um exemplo disso)

Classes de infra são mais genéricas, fornecem um serviço independente de contexto, portanto não tendem a se transformar em uma hierarquia mais profunda.
( A HashMap é um exemplo dessa)

saoj, respeito (mas não concordo) com sua opinião sobre testes unitários.
Mas acredito que sua solução não é exatamente a melhor saída para isso.

No seu exemplo, você está considerando que apenas um componente novo precisa "doSomethingVeryCrazyInADifferentWay", mas se esta mudança se aplicar a todo mundo que já usava o método anterior? Quem garante a segurança?

Acho que se você programar seus métodos com responsabilidades bem específicas, o teste unitário pode garantir que para qualquer entrada possível, mesmo com alterações, as saídas ainda são válidas.
Sei que nesse caso envolve muitos conceitos puritanos que muito código na prática passa longe (métodos coesos e simples, etc), mas não dá pra desistir da melhor solução por exigir mais.

O exemplo que passou é realmente a forma mais segura de adicionar funcionalidade mas também é um grande atestado que não se tem controle sobre o código.
E só falo isso porque eu mesmo já utilizei esta técnica: não mexe no que funciona, cria uma variação e toca o barco.
Isso via de regra causa o problema que citou: N métodos com nomes estranhos fazendo quase a mesma coisa de forma diferente.

Pra finalizar…gosto muito da estratégia dos Maps (Interface -> Classe Abstrata - Implementações) pois permitem abordar os dois mundos.
Se puder herdar a classe de abstract já ganha várias implementações de brinde.
Se não puder, implementa a interface e refaz tudo que precisar (ou usa composição pra pegar uma pronta).

Eu não sou contra as classes “AbstractXXX” desde que exista uma interface que a acompanhe (no caso, a Map). Acho que nesse caso cria-se um bom compromisso entre uma classe abstrata com uma implementação relativamente genérica e comum, e a flexibilidade das interfaces. E, realmente, para muitos casos a herança efetivamente vai gerar um código mais limpo, não é à toa que template methods estão entre os design patterns.

O código do usuário da classe depende (ou deveria depender) apenas da interface (no caso do nosso exemplo, a interface Map), portanto, sempre pode-se mudar a hierarquia sem grande impacto no código.
Essa é uma prática que costumo a adotar em meus sistemas também.

Não se pode é radicalizar. Herança é muito justificável quando a relação é de “é um”. Também, muitas vezes, simplificamos classes quebrando um código complexo em classes menores, o que geralmente começa com a adoção do padrão Strategy. Agora, com a evolução do sistema, as classes que antes só representavam algorítmos podem se tornar hierarquias de classes interligadas, porém geralmente com classes mais coesas. Como regra, eu procuro evitar hierarquias muito longas, mas não significa que meu código seja apenas um apanhado de “bonecas russas” sem qualquer herança, e isso tem dado bastante certo nos códigos que tenho feito até hoje.

Também não concordo com a opinião do saoj sobre testes unitários.

Já devem fazer uns 6 anos que vejo o SAOJ reclamar de testes unitários com herança…(Com o meu antigo login do fórum).

Faz 4 anos que larguei mão de ser desenvolvedor Java, agora desenvolvo somente utilitários em Java ultimamente, pois estou mais focado em na minha especialidade.

Estou montando um utilitário para rodar rotinas ICommand segui a seguinte arquitetura.

JFD - JFormDev -> View (Plugin do ecplise proprietário)
BackingBean -> Suportar a minha view com propriedades de tela e setting/getting
Controller <-> Runner -> Nem preciso Falar
DAO -> Idem a acima
Entity -> Estou extraindo dados só para apresentação via JPA.

A minhas entities são compátiveis com a visualização, então para simplificar criei uma Interface para crossing entre as camadas, são 15 entidades originadas de tabelas diferentes, mas os dados que preciso são comuns, existem algumas regras no comportamento que são diferentes. Por este motivo adotei a interface.

[code]public interface BAMObjectInterface
{

public static final String PATH = "c:/temp";

public void setName(String p_name);
public String getName();

public void setBAMUrl(String bam_url);
public String getBAMUrl();

public String getExportLineCommmand();
public String getImportLineCommmand();

[/code]

Há diferenças nas linhas de comando como nos casos abaixo:

Exemplo 1:

public String getExportLineCommmand() { String lineCOmmand = "-name " + getBAMUrl() + " -type dataobject " + " -file " + BAMObjectInterface.PATH + "\\" + getName() + ".xml -logfile export.log"; return lineCOmmand; }
Exemplo 2:

[code]public String getExportLineCommmand() {
String lineCOmmand = "-type rule -name " + getName() + "-file " + BAMObjectInterface.PATH + getName() + “.xml”;

	return lineCOmmand;
}[/code]

Ficou bem clean para o meu Runner uma classe que criei para executar meus comandos.

[code]
String lineCommand = IRunner.LINE_EXPORT;
lineCommand = lineCommand + object.getExportLineCommmand();
try {
writeLineCommand(lineCommand);
Runtime.getRuntime().exec(IRunner.LINE_EXEC);

} catch (IOException e)
{

e.printStackTrace();
}[/code]

Eu fiz uma primeira tentativa de usar o polimorfismo com Herança anteriormente, como naquela altura não havia imaginado que existiriam peculiariedades em cada linha de comando criei um metódo e fui herdando. Quando vi já estava feita a bagunça fiquei sobreescrevendo o original. Resolvi mudar para abstract e depois vi que era melhor ter usado uma interface mesmo.

No meu caso composição não ficaria legal, ficaria um pouco ruim lidar com as N fontes de dados. Esse seria um caso onde é mais interessante a abordagem por interfaces.

Antes de criar um metódo para ser herdado vc precisa saber oq está fazendo, senão vc acaba obrigando todo mundo a dar overwrite e no fim das contas ninguem mais sabe oq está valendo. A abordagem por interfaces é superior, acho que existem alguns casos que é interessante criar classes como as Support que implementam algumas interfaces, mas na maioria dos casos acho que o beneficio é baixo.

Sobre interfaces, existe essa entrevista bastante interessante com o Erich Gamma.
http://www.artima.com/lejava/articles/designprinciples.html

O mais importante, na minha opinião, é uma boa FUNDACÃO para o seu sistema. Explico o que seja isso mais adiante…

Fico bastante chateado quando pego um sistema feito na base do PAU, ou seja, o sistema não tinha uma boa fundacão e código foi escrito na base do pau.

Resultado: a coisa funciona muito bem, pois o cara que fez era competente e got the job done. Mas dói os zoios olhar para a coisa.

Com uma boa fundacão, vc até terá um sistema ou outro baguncado, porque o cara criou muitos métodos protected, ou porque o código do cara é ruim mesmo.

Mas o CORE vai estar bonito. A fundacão vai garantir que a coisa fique no controle.

Um código sem essa fundacao é mais ou menos como um homem sem integridade.

Por que eu contei essa história?

Porque pelo menos pra Java, eu acho que uma fundacão baseado em heranca com classes abstratas é melhor do que uma sem heranca baseada em composicao.

Uma fundacao não é só isso. É tb prnicipalmente interfaces e componentes do seu sistema. Aí que entra a ARTE e a EXPERIENCIA. O cara tem que saber quais interfaces criar para melhor descrever o problema e as interacoes entre as classes.

Ex:

Parser, ParserListener, Client, ClientListener, AbstractUDPClient, AbstractTCPClient, e por aí vai.

Se o cara erra no comeco, na fundacao, pode chamar o melhor programador do mundo que vai ser tudo uma bagunca de código.

E para concluir:

Repare que todo o FRAMEWORK nada mais é do que uma fundacao, ou seja, uma API para abstrair algo complexo por baixo.

A arte não está em programar as linhas de código. Isso qualquer cara com QI reazoável pode fazer.

A arte está em fazer uma API simples que permita com que as pessoas que forem usar o seu framework possam ter uma boa fundacao para que os seus código não fiquem zoneados.

O que eu mais gosto de fazer não é escrever código, mas desenvolver APIs. É o caso do Space4J x Prevayler, ou a do Struts2 x Mentawai.

Só que para fazer a sua API funcionar, ou seja, para que vc prove que sua fundacão funciona, que é simples e coesa, vc tem que escrever bastante código por baixo.

E minha frase favorita: “A perfeição é alcançada, não quando não existe nada mais para se acrescentar, mas quando não existe mais nada para se retirar.” (Antoine de Saint-Exupéry)

Considerando duas situações extremas:

Eu prefiro um código
a) bem refatorado, bem modular e com classes coesas, mas completamente sem testes automáticos,
do que:
b) um código mal escrito e com 100% de cobertura de testes automáticos.

Motivos:

  1. Será fácil escrever testes e manter o código coeso (com “boa fundação”);
  2. Provavelmente o código macarrônico terá também testes igualmente macarrônicos. Ambos terão que ser reescritos.

Já notei que o número de erros em código bem escrito realmente é imensamente menor do que o de código mal escrito, tenha você práticas de testes ou não.
Os testes auxiliam em evitar a reincidência do erro, especialmente durante uma refatoração. Auxiliam também na prevenção de erros mas, do contrário que livros sobre testes vendem, não fazem milagres.

Código mal escrito é sempre código mal escrito.

Acho que me expressei mal no início do meu post, mas já editei lá.

Não estou falando de uma pessoa sem fundacao, mas de um sistema sem uma boa fundacao.

Os extremos são:

1 - Um código bem escrito em cima de uma fundacao ruim.

2- Um código mal escrito em cima de uma boa fundacao.

Eu vou SEMPRE preferir a opcao 2).

A fundacão não são os testes, apesar de eu entender que tem muita gente que usa isso como fundacao. Só porque um sistema funciona e não possui bugs, ou seja, só porque todos os 1231231232131 testes unitários passaram, não significa que o código está bem feito, apenas que funciona. E como eu falei, qualquer um com QI analítico mediano consegue fazer um código que funciona.

E aqui no que vc falou:

Vou sempre preferir a).

Quando a FUNDACAO comeca errada, não há santo que faca milagre.

O CORE, de uma pessoa, de um sistema, de um prédio, é o mais importante.