Ultimamente, estou preocupado com o uso de classes abstratas.
Às vezes, uma classe abstrata é criada com antecedência e funciona como um modelo de como as classes derivadas funcionariam. Isso significa, mais ou menos, que eles fornecem algumas funcionalidades de alto nível, mas deixam de fora certos detalhes a serem implementados pelas classes derivadas. A classe abstrata define a necessidade desses detalhes, colocando em prática alguns métodos abstratos. Nesses casos, uma classe abstrata funciona como um blueprint, uma descrição de alto nível da funcionalidade ou o que você quiser chamar. Ele não pode ser usado por si só, mas deve ser especializado para definir os detalhes que foram deixados de fora da implementação de alto nível.
Outras vezes, acontece que a classe abstrata é criada após a criação de algumas classes "derivadas" (uma vez que a classe pai / abstrato não está lá, elas ainda não foram derivadas, mas você sabe o que quero dizer). Nesses casos, a classe abstrata geralmente é usada como um local onde você pode colocar qualquer tipo de código comum que as classes derivadas atuais contenham.
Tendo feito as observações acima, estou me perguntando qual desses dois casos deve ser a regra. Deveria algum tipo de detalhe ser adicionado à classe abstrata apenas porque atualmente é comum em todas as classes derivadas? O código comum que não faz parte de uma funcionalidade de alto nível deve estar lá?
O código que pode não ter significado para a própria classe abstrata deve estar lá apenas porque é comum para as classes derivadas?
Deixe-me dar um exemplo: A classe abstrata A tem um método a () e um método abstrato aq (). O método aq (), nas duas classes derivadas AB e AC, usa o método b (). B () deve ser movido para A? Se sim, caso alguém olhe apenas para A (vamos fingir que AB e AC não estão lá), a existência de b () não faria muito sentido! Isso é ruim? Alguém deveria dar uma olhada em uma classe abstrata e entender o que está acontecendo sem visitar as classes derivadas?
Para ser sincero, no momento de perguntar isso, costumo acreditar que escrever uma classe abstrata que faça sentido sem precisar procurar nas classes derivadas é uma questão de código e arquitetura limpos. Ι realmente não gosto da idéia de uma classe abstrata que age como um despejo para qualquer tipo de código que seja comum em todas as classes derivadas.
O que você pensa / pratica?
fonte
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.
-- Por quê? Não é possível que, ao projetar e desenvolver um aplicativo, descubra que várias classes tenham uma funcionalidade comum que pode ser naturalmente refatorada em uma classe abstrata? Devo ser clarividente o suficiente para sempre antecipar isso antes de escrever qualquer código para classes derivadas? Se eu não for tão astuto, estou proibido de realizar tal refatoração? Devo jogar fora meu código e começar de novo?Respostas:
O que você está perguntando é onde colocar
b()
e, em algum outro sentido, uma pergunta é seA
é a melhor escolha como super classe imediata paraAB
eAC
.Parece que existem três opções:
b()
em ambosAB
eAC
ABAC-Parent
que herdaA
e introduzb()
, e é usada como a superclasse imediata paraAB
eAC
b()
emA
(não saber se outra classe futuroAD
vai quererb()
ou não)Até que outra classe
AD
que não queira seb()
apresente, (3) parece ser a escolha certa.Em um momento como um
AD
presente, podemos refatorar a abordagem em (2) - afinal, é software!fonte
b()
em nenhuma classe. Torne-a uma função livre que pegue todos os seus dados como argumentos e tenha ambosAB
eAC
chame-o. Isso significa que você não precisa movê-lo ou criar mais classes ao adicionarAD
.b()
não precisar de um estado de longa duração da instância.ac
que chama emb
vez deac
ser abstrata?Uma classe abstrata não deve ser a base de despejo para várias funções ou dados que são lançados na classe abstrata porque é conveniente.
A única regra prática que parece fornecer a abordagem orientada a objetos mais confiável e extensível é " Preferir composição sobre herança ". Uma classe abstrata é provavelmente melhor pensada como uma especificação de interface que não contém nenhum código.
Se você tem algum método que é um tipo de método de biblioteca que acompanha a classe abstrata, um método que é o meio mais provável de expressar alguma funcionalidade ou comportamento que as classes derivadas de uma classe abstrata normalmente precisam, faz sentido criar um classe entre a classe abstrata e outras classes em que esse método está disponível. Essa nova classe fornece uma instanciação específica da classe abstrata que define uma alternativa ou caminho de implementação específico, fornecendo esse método.
A idéia de uma classe abstrata é ter um modelo abstrato do que as classes derivadas que realmente implementam a classe abstrata devem fornecer, tanto quanto serviço ou comportamento. A herança é fácil de usar em excesso e muitas vezes as classes mais úteis são compostas de várias classes usando um padrão mixin .
No entanto, sempre há as questões de mudança, o que vai mudar e como isso vai mudar e por que isso vai mudar.
A herança pode levar a corpos de origem frágeis e frágeis (consulte também Herança: basta parar de usá-la! ).
fonte
b()
é uma função pura. Pode ser um functoide ou um modelo ou outra coisa. Estou usando a frase "método de biblioteca" como significando algum componente, seja função pura, objeto COM ou qualquer outra coisa usada para a funcionalidade que ele fornece à solução.Sua pergunta sugere uma abordagem ou ou para classes abstratas. Mas acho que você deve considerá-los apenas mais uma ferramenta na sua caixa de ferramentas. E então a pergunta se torna: Para quais trabalhos / problemas as classes abstratas são a ferramenta certa?
Um excelente caso de uso é implementar o padrão do método de modelo . Você coloca toda a lógica invariável na classe abstrata e a lógica variante em suas subclasses. Observe que a lógica compartilhada em si é incompleta e não funcional. Na maioria das vezes, trata-se de implementar um algoritmo, em que várias etapas são sempre as mesmas, mas pelo menos uma etapa varia. Coloque esta etapa como um método abstrato chamado de uma das funções dentro da classe abstrata.
Penso que o seu primeiro exemplo é essencialmente uma descrição do padrão do método de modelo (corrija-me se estiver errado); portanto, consideraria isso como um caso de uso perfeitamente válido de classes abstratas.
Para o seu segundo exemplo, acho que usar classes abstratas não é a melhor opção, porque existem métodos superiores para lidar com lógica compartilhada e código duplicado. Vamos dizer que você tem classe abstrata
A
, classes derivadasB
eC
e ambas as classes derivadas compartilhar alguma lógica na forma de métodos()
. Para decidir sobre a abordagem correta para se livrar da duplicação, é importante saber se o métodos()
faz parte da interface pública ou não.Se não for (como no seu exemplo concreto com o método
b()
), o caso é bastante simples. Basta criar uma classe separada a partir dela, extraindo também o contexto necessário para executar a operação. Este é um exemplo clássico de composição sobre herança . Se houver pouco ou nenhum contexto necessário, uma função auxiliar simples já poderá ser suficiente, conforme sugerido em alguns comentários.Se
s()
faz parte da interface pública, torna-se um pouco mais complicado. Sob a suposição de ques()
não tem nada a ver comB
eC
estar relacionadoA
, você não deve colocá-los()
lá dentroA
. Então, onde colocá-lo, então? Eu argumentaria por declarar uma interface separadaI
, que defines()
. Então, novamente, você deve criar uma classe separada que contenha a lógica para implementars()
e ambos,B
e queC
depende dela.Por último, aqui está um link para uma excelente resposta a uma pergunta interessante sobre SO que pode ajudá-lo a decidir quando escolher uma interface e quando uma aula abstrata:
fonte
Parece-me que você está perdendo o ponto de orientação a objetos, tanto lógica como tecnicamente. Você descreve dois cenários: agrupando o comportamento comum de tipos em uma classe base e polimorfismo. Ambos são aplicativos legítimos de uma classe abstrata. Mas se você deve tornar a classe abstrata ou não, depende do seu modelo analítico, não das possibilidades técnicas.
Você deve reconhecer um tipo que não tem encarnações no mundo real, mas estabelece as bases para tipos específicos que existem. Exemplo: um animal. Não existe um animal. É sempre um cachorro, um gato ou qualquer outra coisa, mas não há animal de verdade. Ainda Animal enquadra todos eles.
Então você fala de níveis. Os níveis não fazem parte do acordo quando se trata de herança. Tampouco é reconhecer dados comuns ou comportamento comum, que é uma abordagem técnica que provavelmente não ajudará. Você deve reconhecer um estereótipo e depois inserir a classe base. Se não houver esse estereótipo, talvez seja melhor usar algumas interfaces implementadas por várias classes.
O nome da sua classe abstrata, juntamente com seus membros, deve fazer sentido. Ele deve ser independente de qualquer classe derivada, técnica e logicamente. Like Animal poderia ter um método abstrato Eat (que seria polimórfico) e as propriedades abstratas booleanas IsPet e IsLifestock, que fazem sentido sem saber sobre gatos, cães ou porcos. Qualquer dependência (técnica ou lógica) deve seguir apenas um caminho: do descendente para a base. As próprias classes base também não devem ter conhecimento de classes descendentes.
fonte
Primeiro caso , da sua pergunta:
Segundo caso:
Então, sua pergunta :
Na IMO, você tem dois cenários diferentes; portanto, não é possível desenhar uma regra única para decidir qual design deve ser aplicado.
Para o primeiro caso, temos coisas como o padrão Template Method já mencionadas em outras respostas.
Para o segundo caso, você deu o exemplo da classe abstrata A que recebe o método ab (); Na IMO, o método b () só deve ser movido se fizer sentido para TODAS as outras classes derivadas. Em outras palavras, se você estiver movendo porque é usado em dois lugares, provavelmente esta não é uma boa escolha, porque amanhã poderá haver uma nova classe Derived concreta na qual b () não faz sentido algum. Além disso, como você disse, se você observar a classe A separadamente eb () também não faz sentido nesse contexto, provavelmente é uma dica de que essa também não é uma boa opção de design.
fonte
Eu tive exatamente as mesmas perguntas há algum tempo!
O que cheguei é que sempre que me deparo com esse problema, percebo que há algum conceito oculto que não encontrei. E esse conceito provavelmente deve ser expresso com algum objeto de valor . Você tem um exemplo muito abstrato, então, receio que não consiga demonstrar o que quero dizer usando seu código. Mas aqui está um caso da minha própria prática. Eu tinha duas classes que representavam uma maneira de enviar solicitação para recurso externo e analisar a resposta. Havia semelhanças na forma como os pedidos foram formados e como as respostas foram analisadas. Então era assim que minha hierarquia era:
Esse código indica que eu simplesmente uso indevidamente a herança. É assim que poderia ser refatorado:
Desde então, trato a herança com muito cuidado, porque fui machucada várias vezes.
Desde então, cheguei a outra conclusão sobre herança. Sou fortemente contra a herança baseada na estrutura interna . Ele quebra o encapsulamento, é frágil, é processual, afinal. E quando decompo meu domínio da maneira certa , a herança simplesmente não acontece com muita frequência.
fonte