Ao usar a herança para reutilizar o código, você acha muito complicado que ele engula os benefícios da reutilização?

8

Eu tenho codificado por cerca de 8 anos, mas ainda acho que a herança é muito flexível e, às vezes, deixa você totalmente confuso com o código que escreveu. Um exemplo mais simples seria:

abstract class AClass {
    protected void method1() {
        if(check()) {
            do1();
        } else {
            do2();
        }
    }
    protected abstract void do1();
    protected abstract void do2();
}

A intenção da classe é que as pessoas possam implementar do1 () e do2 () para que alguma lógica adicional possa ser feita; no entanto, às vezes, as pessoas decidem sobrecarregar o método1 (), e as coisas se complicam imediatamente.

Acho apenas no padrão de estratégia que o código é reutilizado bem por meio da herança; na maioria dos casos, o designer da classe base conhece muito bem suas subclasses, e essa herança é totalmente opcional.

Eu tenho uma classe que é herdada por 4 classes - um IoHandler, e é subclasse para servidor, cliente, servidor de borda, servidor de origem e isso começa a me deixar louco. Eu estava sempre na refatoração de código, sempre tive idéias que acho que funcionariam e depois não foram comprovadas. Dizem que o cérebro humano pode armazenar apenas sete informações uma vez, estou carregando muitas?

tactoth
fonte
1
Você sabe que pode impedir a sobrecarga de métodos específicos? Marque-o como final[se você estiver usando java]. Isso efetivamente o torna não virtual e não pode ser sobrecarregado. Perfeito para uso nesta circunstância [configuração padrão]
Farrell
@Farrell Na verdade, eu o marquei 'protegido intensamente para que possa ser sobrecarregado'. Acho que, quando faço isso, estou usando algumas idéias do MFC, que foram mal projetadas.
tactoth 27/05
2
A sobrecarga de método não tem nada a ver com herança. É realmente estranho que vários comentários e até a resposta aceita a repitam literalmente. Certamente você está falando sobre substituir ?
Aaronaught
@Aaronaught Bem, sua correção é realmente útil :(
tactoth

Respostas:

3

Depois de repetir esses padrões, eles não parecem mais tão complicados.

Além disso, você precisa conhecer as classes base antes de herdá-las. E a herança não será opcional enquanto o código comum for reutilizado. Adote qualquer outro padrão, como fábrica / composto abstrato - que a herança realmente serve a um propósito.

Regra prática: não use herança apenas para reunir várias classes. Use-o sempre que fizer sentido. No exemplo artificial, o usuário da classe filho que está sobrecarregando o método1 deve ter algum motivo sério para fazer isso. (OTOH, eu também queria uma maneira de evitar sobrecarregar determinadas funções sozinhas - para evitar isso). E antes de usar essa classe filho, você também deve saber sobre essa peculiaridade. Mas um padrão de estratégia puro não o fará - se o fizer, documente muito bem.

Se você der o seu exemplo de código real, talvez as pessoas aqui possam analisá-lo.

Subu Sankara Subramanian
fonte
Obrigado pela resposta, ainda acho que a herança é usada principalmente para compartilhar lógica, sempre apliquei o princípio de que, quando códigos podem ser compartilhados, eles devem ser compartilhados e, quando você decide não compartilhá-los, fica mais claro .
tactoth 26/05
O exemplo de código é muito grande, tenho mais de 30 classes que precisam ser resolvidas.
tactoth 26/05
18

Sim, pode ser muito complicado ... e você está em boa companhia.

Joshua Bloch em Java efetivo captura bem alguns dos problemas de herança e recomenda favorecer a composição em vez da herança.

A herança é uma maneira poderosa de obter a reutilização de código, mas nem sempre é a melhor ferramenta para o trabalho. Usado de forma inadequada, leva a software frágil. É seguro usar herança dentro de um pacote, onde a subclasse e a implementação da superclasse estão sob o controle dos mesmos programadores. Também é seguro usar a herança ao estender classes especificamente projetadas e documentadas para extensão (Item 15). Herdar de classes concretas comuns através dos limites de pacotes, no entanto, é perigoso. Como lembrete, este livro usa a palavra "herança" para significar herança de implementação (quando uma classe estende a outra). Os problemas discutidos neste item não se aplicam à herança da interface (quando uma classe implementa uma interface ou onde uma interface se estende a outra).

Diferentemente da invocação de método, a herança quebra o encapsulamento. Em outras palavras, uma subclasse depende dos detalhes de implementação de sua superclasse para o funcionamento adequado. A implementação da superclasse pode mudar de release para release e, se o fizer, a subclasse pode quebrar, mesmo que seu código não tenha sido tocado. Como conseqüência, uma subclasse deve evoluir em conjunto com sua superclasse, a menos que os autores da superclasse a tenham projetado e documentado especificamente com a finalidade de ser estendido.

Dakotah North
fonte
+1. E observe que a ideia já existe há muito mais tempo e é muito mais difundida do que apenas Java Eficiente e em Bloco ; veja aqui .
21713 Josh Kelley
1

Eu acho que sua pergunta é muito vaga e geral. O que você descreve parece um problema complexo, com ou sem herança. Então, IMHO, é totalmente normal que você esteja lutando contra isso.

Obviamente, em alguns casos, a herança não é a melhor ferramenta para um trabalho (referência obrigatória a "favorecer a composição sobre a herança" ;-). Mas, quando usado com cuidado, acho útil. Pela sua descrição, é difícil decidir se a herança é ou não a ferramenta certa para o seu caso.

Péter Török
fonte
É uma pergunta vaga e a intenção é coletar algumas idéias para facilitar a tomada de decisões de design. Eu tenho alternado entre composição e herança.
Tactoth #
1

se seus métodos são excessivamente complexos, as subclasses choram

faça com que seus métodos façam uma coisa específica

Steven A. Lowe
fonte
1

A herança de composição restringe o vetor de mudança.

Imagine que sua solução foi implementada e você usa o AClass em 300 locais diferentes da base de código, e agora há um requisito para alterar 45 das chamadas method1 () para que um método do3 () seja chamado em vez de do2 ().

Você pode implementar do3 () no IAnInterface (espero que todas as implementações da interface possam manipular um método do3 () facilmente) e criar uma BClass que chame do1 () ou do3 () na chamada method1 () e altere as 45 referências para o AClass e a dependência injetada.

agora imagem implementando essa mesma alteração para do4 (), do5 (), ...

Ou você pode fazer o seguinte ...

interface IFactoryCriteria {
    protected boolean check();
}

public final class DoerFactory() {
    protected final IAnInterfaceThatDoes createDoer( final IFactoryCriteria criteria ) {
        return criteria.check() ? new Doer1() : new Doer2();
    }
}

interface IAnInterfaceThatDoes {
    abstract void do();
}

public final class AClass implements IFactoryCriteria {
    private DoerFactory factory;
    public AClass(DoerFactory factory)
    {
        this.factory = factory;
    }

    protected void method1() {
        this.factory.createDoer( this ).do();
    }

    protected boolean check() {
        return true;//or false
    }
}

Agora você só precisa implementar uma segunda fábrica que retorne o Doer1 ou o Doer2 e altere a injeção de fábrica em 45 locais. O AClass não muda, o IAnInterface não muda e você pode lidar facilmente com mais alterações como do4 (), do5 ().

Heath Lilley
fonte
1
+1 Gosto dessa implementação do problema em relação à proposta por Ed. Eu acho que isso torna o problema mais SRP, além de limitar as dependências do AClass. Eu acho que minha pergunta principal seria os prós e os contras de passar IAnInterfaceThatDoes no AClass em vez de em uma classe de fábrica. Por exemplo, o que acontece se a lógica para determinar o IAnInterfaceThatDoes correto ficar fora do escopo da fábrica?
dreza
Fiz da fábrica a dependência apenas para minimizar a exposição de IAnInterfaceThatDoes. Se o código de chamada precisasse do acesso a essa interface, você certamente poderia movê-lo. Não conheço uma situação em que gostaria de mover a criação da interface para fora da fábrica. Eu diria que, se isso tiver que mudar, a implementação de uma interface DoerFactoryI com o método createDoer exposto seria outro caminho a percorrer. Você poderia implementar uma fábrica usando uma estrutura DI ou algo assim.
precisa
0

Por que não fazer:

interface IAnInterface {
    abstract void do1();
    abstract void do2();
}

public final class AClass {
    private IAnInterface Dependent;
    public AClass(IAnInterface dependent)
    {
        this.Dependent = dependent;
    }

    protected void method1() {
        if(check()) {
            this.Dependent.do1();
        } else {
            this.Dependent.do2();
        }
    }
}

(Eu acho que a final é o equivalente a "selado" em C #, alguém me corrija se essa sintaxe estiver incorreta).

Injeção de dependência / IoC significa que você pode herdar apenas os métodos que realmente deseja que as pessoas possam alterar, o que acho que resolve o seu problema. Além disso, não parece que a classe principal realmente faz o que do1 e do2 são, mais que delega as chamadas em suas subclasses com base em uma condição: você está quebrando a separação de preocupações!

Ed James
fonte