Qual código deve ser incluído em uma classe abstrata?

9

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?

Alexandros Gougousis
fonte
7
Por que deve haver uma "regra"?
Robert Harvey
11
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?
Robert Harvey
Desculpe, se eu fui mal interpretado! Eu estava tentando dizer o que sinto (por enquanto) como uma prática melhor e não implica que deva haver uma regra absoluta. Além disso, não estou sugerindo que qualquer código que pertença à classe abstrata seja escrito apenas com antecedência. Eu estava descrevendo como, na prática, uma classe abstrata acaba com código de alto nível (que funciona como um modelo para os derivados), bem como código de baixo nível (que você não pode entender sua usabilidade, você não procura as classes derivadas).
Alexandros Gougousis
@RobertHarvey para uma classe base pública, você seria proibido de olhar para classes derivadas. Para classes internas, não faz diferença.
precisa

Respostas:

4

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?

O que você está perguntando é onde colocar b()e, em algum outro sentido, uma pergunta é se Aé a melhor escolha como super classe imediata para ABe AC.

Parece que existem três opções:

  1. sair b()em ambos ABeAC
  2. crie uma classe intermediária ABAC-Parentque herda Ae introduz b(), e é usada como a superclasse imediata para ABeAC
  3. colocar b()em A(não saber se outra classe futuro ADvai querer b()ou não)

  1. sofre por não estar SECO .
  2. sofre de YAGNI .
  3. então, isso deixa este.

Até que outra classe ADque não queira se b()apresente, (3) parece ser a escolha certa.

Em um momento como um ADpresente, podemos refatorar a abordagem em (2) - afinal, é software!

Erik Eidt
fonte
7
Existe uma quarta opção. Não coloque b()em nenhuma classe. Torne-a uma função livre que pegue todos os seus dados como argumentos e tenha ambos ABe ACchame-o. Isso significa que você não precisa movê-lo ou criar mais classes ao adicionar AD.
user1118321
11
@ user1118321, excelente, bom pensamento fora da caixa. Faz um sentido particular se b()não precisar de um estado de longa duração da instância.
precisa
O que me preocupa em (3) é que a classe abstrata não descreve mais um pedaço de comportamento independente. São bits e partes de código e não consigo entender o código da classe abstrata sem ir e voltar entre esta e todas as classes derivadas.
Alexandros Gougousis 28/10
Você pode criar uma base / padrão acque chama em bvez de acser abstrata?
Erik Eidt 28/10
3
Para mim, a pergunta mais importante é: POR QUE AB e AC usam o mesmo método b (). Se for apenas coincidência, deixaria duas implementações semelhantes no AB e no AC. Mas provavelmente é porque existe alguma abstração comum para AB e AC. Para mim, DRY não é tanto um valor em si, mas uma dica de que você provavelmente perdeu alguma abstração útil.
Ralf Kleberhoff 28/10
2

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! ).

O problema frágil da classe base é um problema arquitetônico fundamental dos sistemas de programação orientados a objetos em que as classes base (superclasses) são consideradas "frágeis" porque modificações aparentemente seguras em uma classe base, quando herdadas pelas classes derivadas, podem causar mau funcionamento das classes derivadas. . O programador não pode determinar se uma alteração da classe base é segura simplesmente examinando isoladamente os métodos da classe base.

Richard Chambers
fonte
Estou certo de que qualquer conceito de POO pode levar a problemas se for mal utilizado, usado em excesso ou abusado! Além disso, supor que b () é um método de biblioteca (por exemplo, uma função pura sem outras dependências) restringe o escopo da minha pergunta. Vamos evitar isso.
Alexandros Gougousis
@AlexandrosGougousis Não estou assumindo que 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.
Richard Chambers
Desculpe, se eu não estava claro! Mencionei a função pura como um dos muitos exemplos (você deu mais).
Alexandros Gougousis
1

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.

À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.

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.

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.

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 derivadas Be Ce ambas as classes derivadas compartilhar alguma lógica na forma de método s(). Para decidir sobre a abordagem correta para se livrar da duplicação, é importante saber se o método s()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 que s()não tem nada a ver com Be Cestar relacionado A, você não deve colocá-lo s()lá dentro A. Então, onde colocá-lo, então? Eu argumentaria por declarar uma interface separada I, que define s(). Então, novamente, você deve criar uma classe separada que contenha a lógica para implementar s()e ambos, Be que Cdepende 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:

Uma boa classe abstrata reduzirá a quantidade de código que precisa ser reescrita porque sua funcionalidade ou estado podem ser compartilhados.

Larsbe
fonte
Concordo que s () fazer parte da interface pública torna as coisas muito mais complicadas. Eu geralmente evitaria definir métodos públicos chamando outros métodos públicos na mesma classe.
Alexandros Gougousis
1

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.

Martin Maat
fonte
O estereótipo mencionado é mais ou menos a mesma coisa que quero dizer quando falo sobre funcionalidade de nível superior ou quando alguém fala sobre conceitos generalizados sendo descritos por classes mais altas na hierarquia (versus conceitos mais especializados descritos por classes mais baixas no hierarquia).
Alexandros Gougousis 2/11
"As próprias classes base também não devem ter conhecimento de classes descendentes." Concordo completamente com isto. Uma classe pai (abstrata ou não) deve conter uma parte da lógica de negócios independente que não precise ser inspecionada na implementação da classe filha para entender isso.
Alexandros Gougousis
Há outra coisa sobre uma classe base conhecer seus subtipos. Sempre que um novo subtipo é desenvolvido, a classe base precisa ser modificada, o que é inverso e violado o princípio de aberto-fechado. Encontrei isso recentemente no código existente. Uma classe base tinha um método estático que criava instâncias de subtipos com base na configuração do aplicativo. A intenção era um padrão de fábrica. Fazer dessa maneira também viola o SRP. Com alguns pontos do SOLID, eu costumava pensar "duh, isso é óbvio" ou "a linguagem cuidará disso automaticamente", mas achei que as pessoas são mais criativas do que eu poderia imaginar.
Martin Maat
0

Primeiro caso , da sua pergunta:

Nesses casos, uma classe abstrata funciona como um blueprint, uma descrição de alto nível da funcionalidade ou o que você quiser chamar.

Segundo caso:

Outras vezes ... a classe abstrata geralmente é usada como um local onde você pode colocar qualquer tipo de código comum que as classes derivadas atuais contenham.

Então, sua pergunta :

Eu estou querendo saber qual desses dois casos deve ser a regra. ... 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?

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.

Emerson Cardoso
fonte
-1

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:

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

Esse código indica que eu simplesmente uso indevidamente a herança. É assim que poderia ser refatorado:

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

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.

Vadim Samokhin
fonte
@ Downvoter, faça-me um favor e comente o que há de errado com esta resposta.
Vadim Samokhin 1/11