Substituir métodos concretos é um cheiro de código?

31

É verdade que substituir métodos concretos é um cheiro de código? Porque eu acho que se você precisar substituir métodos concretos:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

pode ser reescrito como

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

e se B quiser reutilizar um () em A, ele pode ser reescrito como:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

que não requer herança para substituir o método, isso é verdade?

ggrr
fonte
4
Votado porque (nos comentários abaixo), Telastyn e Doc Brown concordam em substituir, mas dão respostas opostas sim / não à pergunta principal porque discordam do significado de "cheiro de código". Portanto, uma pergunta que se destina a ser sobre design de código foi altamente acoplada a uma gíria. E acho desnecessariamente desnecessário, já que a pergunta é especificamente sobre uma técnica contra outra.
21716 Steve Joplin
5
A pergunta não está marcada como Java, mas o código parece ser Java. Em C # (ou C ++), você precisaria usar a virtualpalavra-chave para dar suporte a substituições. Portanto, a questão é tornada mais (ou menos) ambígua pelos detalhes da linguagem.
precisa saber é o seguinte
1
Parece que quase todos os recursos da linguagem de programação inevitavelmente acabam sendo classificados como um "cheiro de código" depois de anos suficientes.
11116 Brandon
2
Eu sinto que o finalmodificador existe exatamente para situações como estas. Se o comportamento do método não for projetado para ser substituído, marque-o como final. Caso contrário, espere que os desenvolvedores optem por substituí-lo.
Martin Tuskevicius

Respostas:

34

Não, não é um cheiro de código.

  • Se uma aula não for final, ela poderá ser subclassificada.
  • Se um método não for final, ele poderá ser substituído.

Cabe às responsabilidades de cada classe considerar cuidadosamente se a subclasse é apropriada e quais métodos podem ser substituídos .

A classe pode definir a si mesma ou qualquer método como final ou pode colocar restrições (modificadores de visibilidade, construtores disponíveis) sobre como e onde está subclassificada.

O caso usual para substituir métodos é uma implementação padrão na classe base que pode ser personalizada ou otimizada na subclasse (especialmente no Java 8 com o advento dos métodos padrão nas interfaces).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

Uma alternativa para substituir o comportamento é o Padrão de Estratégia , onde o comportamento é abstraído como uma interface e a implementação pode ser definida na classe.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}
Peter Walser
fonte
Eu entendo que, no mundo real, substituir um método concreto pode ser um cheiro de código para alguns. Mas, eu gosto desta resposta porque parece mais especificações baseadas em mim.
usar o seguinte código
No seu último exemplo, os Aimplementos não deveriam DescriptionProvider?
avistado
@ Spotted: A poderia implementar DescriptionProvidere, portanto, ser o provedor de descrição padrão. Mas a idéia aqui é a separação de preocupações: Aprecisa da descrição (algumas outras classes também podem), enquanto outras implementações as fornecem.
Peter Walser
2
Ok, soa mais como o padrão de estratégia .
avistado
@ Spotted: você está certo, o exemplo ilustra o padrão de estratégia, não o padrão de decorador (um decorador adicionaria comportamento a um componente). Corrigirei minha resposta, obrigado.
Peter Walser
25

É verdade que substituir métodos concretos é um cheiro de código?

Sim, em geral, substituir métodos concretos é um cheiro de código.

Como o método base tem um comportamento associado a ele que os desenvolvedores geralmente respeitam, alterações que levarão a erros quando sua implementação fizer algo diferente. Pior, se eles mudarem o comportamento, sua implementação correta anteriormente pode exacerbar o problema. Em geral, é bastante difícil respeitar o Princípio da Substituição de Liskov por métodos concretos não triviais.

Agora, há casos em que isso pode ser feito e é bom. Métodos semi-triviais podem ser substituídos com segurança. Métodos de base que existem apenas como um "default sane" tipo de comportamento que é significou ser substituído, têm bons usos.

Portanto, isso me parece um cheiro - às vezes bom, geralmente ruim, dê uma olhada para ter certeza.

Telastyn
fonte
29
Sua explicação é boa, mas chego exatamente à conclusão oposta: "não, substituir métodos concretos não é um cheiro de código em geral" - é apenas um cheiro de código quando alguém substitui métodos que não deveriam ser substituídos. ;-)
Doc Brown
2
@DocBrown Exatamente! Também vejo que a explicação (e particularmente a conclusão) se opõe à afirmação inicial.
Tersosauros
1
@ Spotted: O contra-exemplo mais simples é uma Numberclasse com um increment()método que define o valor como seu próximo valor válido. Se você a subclassificar em EvenNumberwhere increment()adiciona duas em vez de uma (e o setter reforça a uniformidade), a primeira proposta do OP exige implementações duplicadas de todos os métodos da classe e a segunda exige a escrita de métodos de invólucro para chamar os métodos no membro. Parte do propósito da herança é que as classes filhas deixem claro como elas diferem dos pais, fornecendo apenas os métodos que precisam ser diferentes.
Blrfl
5
@ DocBrown: ser um cheiro de código não implica que algo seja necessariamente ruim, apenas que é provável .
Mikołak 11/04
3
@docbrown - para mim, isso se encaixa ao lado de "favorecer a composição sobre a herança". Se você está exagerando nos métodos concretos, já está em uma terra de cheiros pequenos por ter herança. Quais são as chances de você estar ultrapassando algo que não deveria? Com base na minha experiência, acho provável. YMMV.
Telastyn 11/04
7

se você precisar substituir métodos concretos, [...] ele pode ser reescrito como

Se puder ser reescrito dessa maneira, significa que você tem controle sobre as duas classes. Mas então você sabe se você projetou a classe A para ser derivada dessa maneira e, se criou, não é um cheiro de código.

Se, por outro lado, você não tiver controle sobre a classe base, não poderá reescrever dessa maneira. Portanto, ou a classe foi projetada para ser derivada dessa maneira; nesse caso, vá em frente, não é um cheiro de código, ou não era, nesse caso, você tem duas opções: procure outra solução completamente ou vá em frente de qualquer maneira , porque o trabalho supera a pureza do design.

Jan Hudec
fonte
Se a classe A tivesse sido projetada para ser derivada, não faria mais sentido ter um método abstrato para expressar claramente a intenção? Como quem substitui o método pode saber que o método foi projetado dessa maneira?
avistado
@Spotted, há muitos casos em que as implementações padrão podem ser fornecidas e cada especialização precisa substituir apenas algumas delas, para ampliar a funcionalidade ou aumentar a eficiência. Exemplo extremo são os visitantes. O usuário sabe como os métodos foram projetados a partir da documentação; tem que estar lá para explicar o que é esperado da substituição de qualquer maneira.
Jan Hudec
Entendo o seu ponto de vista, ainda acho perigoso que a única maneira de um desenvolvedor, para saber se um método pode ser substituído, é lendo o documento, porque: 1) se for necessário, não tenho garantia de que será feito. 2) se não deve ser anulado, alguém fará isso por conveniência 3) nem todo mundo lê o documento. Além disso, nunca enfrentei um caso em que precisava substituir um método inteiro, mas apenas partes (e acabei usando um padrão de modelo).
avistado
@Spotted, 1) se deve ser substituído, deve ser abstract; mas há muitos casos em que não precisa ser. 2) se deve não ser substituído, deve ser final(não virtual). Mas ainda existem muitos casos em que os métodos podem ser substituídos. 3) se o desenvolvedor a jusante não ler os documentos, eles dispararão no próprio pé, mas farão isso independentemente das medidas que você colocar em prática.
Jan Hudec
Ok, então a divergência do nosso ponto de vista está apenas em 1 palavra. Você diz que todo método deve ser abstrato ou final, enquanto eu digo que todo método deve ser abstrato ou final. :) Quais são esses casos em que a substituição de um método é necessária (e segura)?
Avistado
3

Primeiro, deixe-me salientar que esta questão é apenas aplicável em certos sistemas de digitação. Por exemplo, em tipagem estrutural e tipagem pato sistemas - uma classe simplesmente ter um método com o mesmo nome e assinatura que significa que foi classe tipo compatível (pelo menos para que o método em particular, no caso de tipagem pato).

Isso é (obviamente) muito diferente do domínio das linguagens estaticamente tipadas , como Java, C ++, etc. Assim como a natureza da digitação Strong , usada em Java e C ++, além de C # e outras.


Suponho que você seja proveniente de Java, onde os métodos devem ser explicitamente marcados como finais se o designer do contrato pretender que eles não sejam substituídos. No entanto, em idiomas como C #, o oposto é o padrão. Os métodos são (sem nenhuma palavra-chave especial de decoração) " selados " (versão do C # final) e devem ser explicitamente declarados como virtualse eles pudessem ser substituídos.

Se uma API ou biblioteca foi projetada nessas linguagens com um método concreto que pode ser substituído, então (pelo menos em alguns casos) faz sentido que ela seja substituída. Portanto, não é um cheiro de código substituir um finalmétodo não selado / virtual / não concreto.     (Isso pressupõe, é claro, que os designers da API estejam seguindo as convenções e marcando como virtual(C #) ou como final(Java) os métodos apropriados para o que estão projetando.)


Veja também

Tersosauros
fonte
Na tipagem de pato, duas classes com a mesma assinatura de método são suficientes para serem compatíveis com o tipo ou também é necessário que os métodos tenham a mesma semântica, de modo que sejam liskov substituíveis por alguma classe base implícita?
Wayne Conrad
2

Sim, é porque pode levar a chamar super anti-padrão. Também pode desnaturar o objetivo inicial do método, introduzir efeitos colaterais (=> bugs) e fazer com que os testes falhem. Você usaria uma classe em que os testes de unidade são vermelhos (falsos)? Não vou.

Vou ainda mais longe, dizendo que o cheiro inicial do código é quando um método não é abstractnem final. Porque permite alterar (substituindo) o comportamento de uma entidade construída (esse comportamento pode ser perdido ou alterado). Com abstracte finala intenção é inequívoca:

  • Resumo: você deve fornecer um comportamento
  • Final: você não tem permissão para modificar o comportamento

A maneira mais segura de adicionar comportamento a uma classe existente é o que seu último exemplo mostra: usando o padrão decorador.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}
Visto
fonte
9
Essa abordagem em preto e branco não é realmente tão útil. Pense Object.toString()em Java ... Se fosse assim abstract, muitas coisas padrão não funcionariam e o código nem seria compilado quando alguém não substituísse o toString()método! Se assim fosse final, as coisas seriam ainda piores - sem capacidade de permitir que objetos sejam convertidos em string, exceto pelo modo Java. Como a resposta de @Peter Walser fala, implementações padrão (como Object.toString()) são importantes. Especialmente nos métodos padrão do Java 8.
Tersosauros
@Tersosauros Você está certo, se toString()fosse abstractou finalseria uma dor. Minha opinião pessoal é que esse método está quebrado e seria melhor não tê-lo (como equals e hashCode ). O objetivo inicial dos padrões Java é permitir a evolução da API com retrocompatibilidade.
avistado
A palavra "retrocompatibilidade" possui muitas sílabas.
Craig
@Spotted Estou muito desapontado com a aceitação geral de herança concreta neste site, eu esperava melhor: / Sua resposta deve ter muitos mais upvotes
TheCatWhisperer
1
@TheCatWhisperer Eu concordo, mas por outro lado, demorei tanto para descobrir as vantagens sutis do uso da decoração sobre a herança concreta (na verdade, ficou claro quando comecei a escrever os testes primeiro) que você não pode culpar ninguém por isso . A melhor coisa a fazer é tentar educar os outros até que eles descubram a Verdade (piada). :)
Viu