De: http://www.artima.com/lejava/articles/designprinciples4.html
Erich Gamma: Eu ainda acho que é verdade mesmo depois de dez anos. A herança é uma maneira legal de mudar o comportamento. Mas sabemos que é frágil, porque a subclasse pode facilmente fazer suposições sobre o contexto em que um método que substitui está sendo chamado. Existe um forte acoplamento entre a classe base e a subclasse, devido ao contexto implícito no qual o código da subclasse que eu inserir será chamado. A composição tem uma propriedade melhor. O acoplamento é reduzido apenas com algumas coisas menores que você conecta em algo maior, e o objeto maior apenas chama o objeto menor de volta. Do ponto de vista da API, definir que um método possa ser substituído é um compromisso mais forte do que definir que um método pode ser chamado.
Eu não entendo o que ele quis dizer. Alguém poderia explicar isso?
Se você publica uma função normal, fornece um contrato unilateral:
O que a função faz se chamada?
Se você publicar um retorno de chamada, também concederá um contrato unilateral:
quando e como será chamado?
E se você publica uma função substituível, é ao mesmo tempo, portanto, você concede um contrato bilateral:
quando será chamado e o que deve fazer se for chamado?
Mesmo que seus usuários não estejam abusando da sua API (quebrando a parte do contrato, que pode ser proibitivamente cara de detectar), você pode ver facilmente que o último precisa de muito mais documentação, e tudo o que você documenta é um compromisso, o que limita suas outras escolhas.
Um exemplo de renúncia a um contrato frente e verso é a mudança de
show
ehide
parasetVisible(boolean)
em java.awt.Component .fonte
show
ehide
ainda existem, são apenas@Deprecated
. Portanto, a mudança não quebra nenhum código que apenas os invoque. Mas se você os tiver substituído, suas substituições não serão chamadas pelos clientes que migrarem para o novo 'setVisible'. (Eu nunca usei o Swing, por isso não sei como é comum substituí-los; mas, como aconteceu há muito tempo, imagino o motivo pelo qual o Deduplicator se lembra é que ele o mordeu dolorosamente.)A resposta de Kilian Foth é excelente. Gostaria apenas de adicionar o exemplo canônico * de por que isso é um problema. Imagine uma classe Point inteira:
Agora vamos subclassificar que seja um ponto 3D.
Super simples! Vamos usar nossos pontos:
Você provavelmente está se perguntando por que estou postando um exemplo tão fácil. Aqui está o problema:
Quando comparamos o ponto 2D com o ponto 3D equivalente, nos tornamos verdadeiros, mas quando invertemos a comparação, nos tornamos falsos (porque p2a falha
instanceof Point3D
).Conclusão
Geralmente, é possível implementar um método em uma subclasse de forma que não seja mais compatível com o modo como a superclasse espera que ele funcione.
Geralmente é impossível implementar equals () em uma subclasse significativamente diferente de uma maneira que seja compatível com a classe pai.
Quando você escreve uma classe que pretende permitir que as pessoas subclasses, é uma boa idéia escrever uma contrato sobre como cada método deve se comportar. Melhor ainda seria um conjunto de testes de unidade que as pessoas poderiam executar contra suas implementações de métodos substituídos para provar que não violam o contrato. Quase ninguém faz isso porque é muito trabalho. Mas se você se importa, é o que deve ser feito.
Um ótimo exemplo de um contrato bem escrito é o Comparator . Apenas ignore o que diz
.equals()
pelas razões descritas acima. Aqui está um exemplo de como o Comparator pode fazer as coisas.equals()
não .Notas
O item 8 "Java efetivo" de Josh Bloch foi a fonte desse exemplo, mas Bloch usa um ColorPoint que adiciona uma cor ao invés de um terceiro eixo e usa duplas ao invés de ints. O exemplo de Java de Bloch é basicamente duplicado por Odersky / Spoon / Venners, que disponibilizou seu exemplo online.
Várias pessoas se opuseram a este exemplo porque, se você informar a classe pai sobre a subclasse, poderá corrigir esse problema. Isso é verdade se houver um número suficientemente pequeno de subclasses e se os pais souberem de todas elas. Mas a pergunta original era sobre como criar uma API para a qual alguém escreverá subclasses. Nesse caso, você geralmente não pode atualizar a implementação pai para ser compatível com as subclasses.
Bônus
O comparador também é interessante, pois funciona em torno da questão da implementação correta de equals (). Melhor ainda, segue um padrão para corrigir esse tipo de problema de herança: o padrão de design da Estratégia. As Typeclasses com as quais Haskell e Scala se empolgam também são o padrão da Estratégia. A herança não é ruim ou errada, é apenas complicada. Para uma leitura mais aprofundada, consulte o artigo de Philip Wadler Como tornar o polimorfismo ad-hoc menos ad hoc
fonte
equals
como Map e Set o definem. A igualdade ignora completamente a ordem, com o efeito de que, por exemplo, dois SortedSets com os mesmos elementos, mas diferentes ordens de classificação ainda comparam iguais.B
é filha de uma classeA
e você deve instanciar um objeto de uma classeB
, deve poder converter oB
objeto de classe para seu pai e usar a API da variável castada sem perder nenhum detalhe de implementação de a criança Você violou a regra ao fornecer a terceira propriedade. Como você planeja acessar az
coordenada depois de converter aPoint3D
variávelPoint2D
, quando a classe base não faz ideia de que essa propriedade existe? Se, ao converter uma classe filho em sua base, você quebrar a API pública, sua abstração estará errada.Herança enfraquece o encapsulamento
Ao publicar uma interface com herança permitida, você aumenta substancialmente o tamanho da sua interface. Cada método substituível pode ser substituído e, portanto, deve ser considerado como retorno de chamada fornecido ao construtor. A implementação fornecida por sua classe é apenas o valor padrão do retorno de chamada. Assim, algum tipo de contrato deve ser fornecido, indicando quais são as expectativas sobre o método. Isso raramente acontece e é uma das principais razões pelas quais o código orientado a objetos é chamado de quebradiço.
Abaixo está um exemplo real (simplificado) da estrutura de coleções java, cortesia de Peter Norvig ( http://norvig.com/java-iaq.html ).
Então, o que acontece se subclassificarmos isso?
Temos um bug: Ocasionalmente, adicionamos "dog" e a hashtable recebe uma entrada para "dogss". A causa foi alguém fornecendo uma implementação do put que a pessoa que projetava a classe Hashtable não esperava.
A herança quebra a extensibilidade
Se você permitir que sua classe seja subclassificada, estará comprometendo-se a não adicionar métodos à sua classe. Caso contrário, isso poderia ser feito sem quebrar nada.
Quando você adiciona novos métodos a uma interface, qualquer pessoa que tenha herdado da sua classe precisará implementar esses métodos.
fonte
Se um método deve ser chamado, você só precisa garantir que ele funcione corretamente. É isso aí. Feito.
Se um método foi projetado para ser substituído, você também deve pensar cuidadosamente sobre o escopo do método: se o escopo for muito grande, a classe filho frequentemente precisará incluir código copiado e colado do método pai; se for muito pequeno, muitos métodos precisarão ser substituídos para ter a nova funcionalidade desejada - isso adiciona complexidade e contagem de linhas desnecessárias.
Portanto, o criador do método pai precisa fazer suposições sobre como a classe e seus métodos poderão ser substituídos no futuro.
No entanto, o autor está falando sobre uma questão diferente no texto citado:
Considere o método
a
que normalmente está sendo chamado do métodob
, mas em alguns casos raros e não óbvios do métodoc
. Se o autor do método de substituição ignorar oc
método e suas expectativasa
, é óbvio como as coisas podem dar errado.Portanto, é mais importante que
a
seja definido de forma clara e inequívoca, bem documentado, "faça uma coisa e faça bem" - mais do que se fosse um método projetado apenas para ser chamado.fonte