Como não aprender orientação a objetos: Herança

Postado em 14. out, 2006 por em Arquitetura, Java

Já falei sobre os problemas que os getters e setters podem trazer, caso usados de maneira indiscriminada. A bola da vez é a herança.

Lembro quando comecei a programar profissionalmente em Java, no final de 2000. Estava em um projeto juntamente com o Tiago Silveira, e algo que eu adorava fazer era que meus beans estendessem de HashMap quando tivessem propriedades dinâmicas. O Tiago vivia me criticando, dizendo que eu deveria ter um HashMap e não ser um HashMap. Eu não entendi o porquê de ter muito mais trabalho para escrever métodos que apenas delegariam, em vez de já herdar tudo! Tão simples, e herança parecia se encaixar tão bem nesse caso.

Depois de um tempo comecei a sofrer bastante: tipos errados eram passados como argumento, minha classe possuia métodos que eu não queria e também nao era para tratá-la como um mapa. Seria o eu o único a cometer essa atrocidade apenas para economizar um pouquinho de código? E a resposta vem do java 1.0:

class Stack extends Vector
class Properties extends Hashtable

Claramente um Properties não deveria ser um Hashtable, afinal ela não mapeia objetos a objetos, mas acaba nos fornecendo um método put(Object, Object), que se usarmos sem passar Strings vai causar problemas. Como resolver isso depois que já nos comprometemos com a herança? A única solução é colocar um aviso grande no javadoc para ninguém usar determinados métodos herdados! O mesmo vale para a Stack e o Vector. Uma Stack não é um Vector, definitivamente. Se você pensar direitinho, uma pilha tem comportamento oposto da implementação da Vector!

Se um gato possui raça e patas, e um cachorro possui raça, patas e tipoDoPelo, logo Cachorro extends Gato? Pode parecer engraçado, mas é o mesmo caso que os anteriores: herança por preguiça, por comodismo, por que vai dar uma ajudinha. A relação “é um” não se encaixa aqui, e vai nos gerar problemas.

Em vários itens do livro Effective Java, Joshua Bloch cita o uso de heraça e de membros package-default e protected. O item 12 diz “minimize o acesso a suas classes e membros“, o item 15 diz “desenhe suas classes pensando no uso de herança, caso contrário proíba-a” e o item 16 “prefira interfaces a classes abstratas“.

Mas sem dúvida o principal é o item 14: “prefira composição em vez de herança“, onde Joshua Bloch diz com todas as letras que herança quebra o encapsulamento. E isso não é novidade, essa conclusão é atribuída a Alan Snyder, no artigo entitulado Encapsulation and Inheritance in Object-Oriented Programming Languages, que data de 1986! Faz incríveis 20 anos que alguém percebeu que “… na maioria das linguagens a introdução da herança compromete seriamente os benefícios do encapsulamento …” (traduzido livremente do abstract do artigo citado). Se você preferir uma opinião mais atual, pode ler esse post do Martin Fowler, que tem menos de um mês.

No livro, Joshua Bloch da como exemplo a criação de uma classe filha de HashSet, e mostra que, sem conhecer profundamente o código fonte da classe mãe, fica impossível de que essa classe filha funcione corretamente ao reescrever um método que é aparentemente inofensivo. A partir do momento que você precisa conhecer o código fonte da sua mãe, você quebrou o encapsulamento. Mais ainda: quando a classe mãe precisar sofrer alguma modificação, o desenvolvedor precisa estar ciente que pode quebrar o funcionamento de várias classes filhas, que pressupunham determinado comportamento interno. Leitura recomendadíssima.

Existem outros exemplos mais sutis de mau uso de herança. Um deles é quando criamos um DAO que é mãe de todos os DAOs. Nesse meu post sobre DAO genérico o Fábio Kung fez um bom comentário sobre isso, onde usamos herança apenas com o intuito de economizar meia dúzia de linhas, sendo que a relação “é um” não existe.

O outro exemplo é HttpServlet e sua própria Servlet. Se você já estendeu de HttpServlet sentiu o problema do método service. Acontece que a implementação padrão do método service em HttpServlet chama o método doGet, doPost, doPut, etc de acordo com o método http utilizado. Esses métodos possuem uma implementação padrão que lançam uma exception. Se você sobrescreve o método service de uma serlvet , você não pode chamar o super.service, pois caso o chame, ele chamará o do correspondente, jogando uma exception. Portanto jamais chame super.service. Já no caso dos métodos init(ServletConfig) e destroy, por exemplo, você deve chamar o super pois não sabemos se a sua classe pai tem algum recurso a ser iniciado e/ou liberado. Sendo assim, em alguns casos você deve, e em outros não deve chamar o super, e você só vai saber disso quando conhecer o código fonte de sua classe pai HttpServlet (ou ler a documentação sobre esse comportamento estranho), quebrando o princípio de encapsulamento. É fácil de reconhecer classes que apresentam esse mesmo problema: o javadoc delas costuma detalhar bastante como o método funciona, e muitas vezes até mostrar o código fonte deles!! Procure-as no jdk, elas não são poucas.

Quem conhece um pouco a API de servlets sabe que eles tentaram resolver o problema adicionando um outro método init, que não recebe parâmetro, e esse sim deve ser reescrito e não precisamos chamar o super. Em outras palavras, uma gambiarra.

Aqui cabe lembrar o que o Joshua Bloch falou: “Crie suas classes pensando em herança, ou então proiba-a“. Parece que HttpServlet não cai nesse caso… Minha proposta para a HttpServlet seria bem simples: você poderia implementar alguma interface, digamos PostProcessor, GetProcessor, entre outras, e registra-la na HttpServlet (um método setPostProcessor, ou passando para o construtor, ou ainda no XML). A interface PostProcessor poderia ser parecida com:

public interface PostProcessor {
  void init(ServletConfig config);
  void doPost(HttpServletRequest request, HttpServletResponse response);
  void destroy();
}

Se sua classe quisesse aceitar tanto GET quanto POST, bastaria implementar ambas interfaces. Interfaces! O caminho é desse lado.

Repare que sempre podemos substituir herança através de um refatoramento simples: extração de uma interface com os métodos herdados, somado a criação de uma implementação que simplesmente delega esses métodos para a antiga classe mãe. Quando então usar herança? Essa é uma questão difícil. Na minha visão particular, a resposta seria um enfático "quase nunca". Creio que a resposta aqui fique um pouco a critério de cada desenvolvedor, mas sempre com muita cautela!

Paulo Silveira

Mais sobre o autor

Tags: , , , , , ,

28 Respostas para “Como não aprender orientação a objetos: Herança”

  1. Luca Bastos

    15. out, 2006

    Belo post!

    Neste momento estamos em meio a uma possível virada na arquitetura dos sistemas com o hype dos sistemas baseados em serviços e eventos. Neste ambiente o conceito de desenvolver baseado em interfaces é mais importante ainda pois é tudo que enxergamos.

    Outro modo de enfatizar a importância de se desenvolver por interfaces ao invés de herança, está na adoção cada vez mais freqüente de desenvolvimento baseado em testes. Aqui as classes herdadas costumam dar canseira.

    Há um livrinho bem recente e bem rápido de ler para quem já leu o Effective Java, que trata só de interfaces em Java e C#. O PDF custa 20 dólares. Trata-se de Interface Oriented Design do professor Ken Pugh que tem mais de 30 anos de experiência. Vejam:
    http://www.pragmaticprogrammer.com/titles/kpiod/

    Vale a pena pelo menos como diversão.

  2. Luiz Aguiar

    16. out, 2006

    Paulo vc disse tudo, o maior problema do uso indiscriminado da herança é a preguiça. Muitos desenvolvedores começam querendo poupar meia dúzia de linhas, e começam a intupir a classe mãe com tranqueiras, e logo isso vira uma grande bola de neve, com gambiarras na mãe pra atender as classes filhas.
    Exemplos disso acho que todos já vimos as dúzias por ai né. =)

  3. Fernando Boaglio

    16. out, 2006

    Legal Paulo, muitas gambis vêm do mau uso de herança mesmo, mas ela existe para ser usada no contexto correto. Toda vez que alguém digitasse “extends” deveria ter um aviso da ferramenta: “use com moderação”. =)

  4. George Gastaldi

    16. out, 2006

    Excelente Post !! É por este e outros posts que admiro este blog.

  5. ronildobraga

    17. out, 2006

    Excelente artigo, aprendi muito lendo essa explicação.
    So complementando posto aqui as 5 regras basicas que nao devem ser violada quando se for usar herança. retirado de: http://www.dsc.ufcg.edu.br/~jacques/cursos/map/html/pat/herancavscomposicao.htm

    1)O objeto “é um tipo especial de” e não “um papel assumido por”
    2)O objeto nunca tem que mudar para outra classe
    3)A subclasse estende a superclasse mas não faz override ou anulação de variáveis e/ou métodos
    4)Não é uma subclasse de uma classe “utilitária”
    5)Para classes do domínio do problema, a subclasse expressa tipos especiais de papeis, transações ou dispositivos

  6. Antonio Kantek

    18. out, 2006

    Paulo, excelente texto.
    Uma vez me ensinaram que para saber se uma heranca faz ou nao sentido, basta filosofar.

    Um carro (por exemplo) cujos atributos sao guardados em um hash nao eh um hash! Ele possui um hash, mas eh um carro !!!! Meio obvio, mas funciona.

  7. soro

    21. out, 2006

    Por que não pode fazer override d métodos q não sejam abstratos?

  8. Tiago Silveira

    22. out, 2006

    Ótimo post! Tenho dois comentários a fazer:

    1. Hoje em dia, quando eu estou modelando, eu uso muito o Princípio da Substituição de Liskov (http://c2.com/cgi/wiki?LiskovSubstitutionPrinciple), que basicamente diz que um tipo S é subtipo de um tipo T se eu jogo uma instância de S numa variável declarada T e ninguém percebe, no sentido de que o contrato (pré-condições, pós-condições e efeitos colaterais de cada método) permanece inalterado.

    2. Tem uma galera tentando construir uma especificação e uma implementação de referência pra arquitetura REST que pode substituir os servlets no futuro: http://www.restlet.org.

  9. Paulo Silveira

    09. set, 2007

    Aqui tem dois excelentes entrevistas que abordam a questão.

    Uma com Eric Gamma:
    http://www.artima.com/lejava/articles/designprinciples.html

    Outra com James Gosling:
    http://www.artima.com/intv/gosling3P.html

  10. Reinaldo de Carvalho

    30. nov, 2007

    Será que há a possibilidade de se extinguir com herança em Java? Já que traz tantos males, como a quebra de encapsulamento e dificuldade de manutenção, seria bom que a possibilidade de usar herança nem existisse!
    Viva à Interface!

  11. Paulo Silveira

    15. abr, 2008

    E aqui tem uma discussão atual no GUJ sobre o assunto:
    http://guj.com.br/posts/list/87814.java

  12. Leandro

    22. nov, 2009

    Se a classe PostProcessor já contém o “Post” , porque repetir no método um doPost? poderia ser só do()! Afinal, estamos pensando em interface que seria init(), do() e destroy(); implementado por GetProcessor, PutProcessor, etc….

  13. Paulo Silveira

    22. nov, 2009

    Ola Leandro! Boa observacao, poderia ainda ser uma interface Processor, com metodo do() ou process(), e ai nossa servlet receberia por parametro 4 Processors, ou um map de metodo->Process, etc.

  14. Tiago

    09. fev, 2011

    Olá estou acompanhando a apostila de java e lendo todos os link recomendados pela mesma.

    ótimo trabalho

  15. Maicon

    01. mar, 2011

    Belo Post, parabéns…”prefira composição em vez de herança”, essa frase precisamos lembrar sempre!! abraços

  16. Thalys de Aguiar Gomes

    22. ago, 2011

    Em uma aula na USP um professor citou que com a utilização de Herança, há uma pequena perda de desempenho…. como isso pode acontecer e em quais casos,

    por favor…

Trackbacks/Pingbacks

  1. blog.caelum.com.br » JPA com Hibernate: Herança e Mapeamentos - março 4, 2007

    [...] Aqui na Caelum, conforme discutido anteriormente, usamos herança com muito critério. Herança na JPA é mapeada com @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS) por padrão, isto é, ele vai utilizar uma única tabela para guardar todos os dados de todas as classes filhas: não há normalização e uma coluna (o discriminator, por default DTYPE no Hibernate) será utilizada para distinguir entre as possíveis subclasses. [...]

  2. blog.caelum.com.br » Como não aprender Orientação a Objetos: relacionamento bidirecional - março 28, 2007

    [...] Isso tudo pode ser muito mais complicado em relacionamentos 1:N e N:M. O conselho é tentar evitar o relacionamento bidirecional, e nunca cria-lo sem uma real necessidade, assim como já comentamos sobre evitar herança e evitar getters e setters. [...]

  3. » Effective Java: segunda edição » blog.caelum.com.br - julho 25, 2008

    [...] é um tópico que já foi discutido anteriormente nesse post. O fato é o seguinte: é muito fácil usar herança de maneira errada, como é o caso de Stack [...]

  4. Diego Plentz » Blog Archive » A dieta do programador - maio 10, 2009

    [...] Prefira um bom restaurante à um posto de conveniências: prefira comer um bom prato em um restaurante do que um hamburguer em um posto de conveniências. Usar herança pode parecer mais prático que usar interfaces, mas seu estômago prefere as interfaces. [...]

  5. Compondo seu comportamento: herança, Chain of Responsibility e Interceptors | blog.caelum.com.br - junho 28, 2010

    [...] de classes é positivo por causa do polimorfismo, mas cria uma acoplamento perigoso, já discutido por diversos autores, em especial nos livros Effective Java e Design [...]

  6. Relacionamento bidirecional entre classes | blog.caelum.com.br - janeiro 20, 2011

    [...] bidirecional, e nunca cria-lo sem uma real necessidade, assim como já comentamos sobre evitar herança e evitar getters e [...]

  7. Possibilidades de design no uso do seu Generic DAO | blog.caelum.com.br - fevereiro 9, 2011

    [...] de execução. Esse é um dos principais motivos para muitos não gostarem de usar o GenericDAO e preferirem usar composição ao invés de herança. Mas como fazer para não repetir o código trivial das operações do [...]

  8. Effective Java: segunda edição | blog.caelum.com.br - fevereiro 10, 2011

    [...] é um tópico que já foi discutido anteriormente nesse post. O fato é o seguinte: é muito fácil usar herança de maneira errada, como é o caso de Stack [...]

  9. Facilitando a manutenção dos testes ao diminuir o acoplamento com o código | blog.caelum.com.br - março 1, 2011

    [...] Programar para interfaces não só diminui o acoplamento entre as classes de produção, mas também entre seu código de teste e de produção. Devemos buscar sempre o baixo acoplamento, para diminuir os custos de manutenção, não importando onde ele esteja presente. [...]

  10. Como não aprender orientação a objetos: o excesso de ifs | blog.caelum.com.br - abril 12, 2011

    [...] o uso da herança é delicado, e o desenvolvedor deve estar ciente de que ela pode trazer um acoplamento indesejado e suas alternativas. O uso de interfaces se encaixaria aqui com [...]

  11. OO – Perigos da Herança « Thiago Turim - junho 25, 2011

    [...] Blog da Caelum, Paulo Silveira, também abordou o assunto no post Como não aprender orientação a objetos: Herança e deu um exemplo bem prático: Se um gato possui raça e patas, e um cachorro possui raça, patas e [...]

  12. Recursos dinâmicos do Objective-C | blog.caelum.com.br - março 13, 2012

    [...] Se retornarmos qualquer coisa diferente de NIL, o seletor passado como argumento será executado no objeto retornado. Com isso, não precisamos fazer nosso objeto ser filho de NSString para ganhar os comportamentos dele, poderíamos apenas delegar as chamadas dos métodos definidos em NSString para um atributo. Evitando o abuso de herança e valorizando a composição, porém de uma forma transparente. [...]

Deixar uma Resposta