As classes / métodos abstratos são obsoletos?

37

Eu costumava criar muitas classes / métodos abstratos. Então eu comecei a usar interfaces.

Agora não tenho certeza se as interfaces não estão tornando obsoletas as classes abstratas.

Você precisa de uma aula totalmente abstrata? Crie uma interface em seu lugar. Você precisa de uma classe abstrata com alguma implementação? Crie uma interface, crie uma classe. Herdar a classe, implemente a interface. Um benefício adicional é que algumas classes podem não precisar da classe pai, mas apenas implementam a interface.

Então, as classes / métodos abstratos são obsoletos?

Boris Yankov
fonte
E se sua linguagem de programação preferida não suportar interfaces? Eu me lembro que esse é o caso do C ++.
21711 Bernard
7
@ Bernard: em C ++, uma classe abstrata é uma interface em todos, exceto no nome. O fato de eles também poderem fazer mais do que interfaces "puras" não é uma desvantagem.
Gbjbaanb
@gbjbaanb: Eu suponho. Não me lembro de usá-los como interfaces, mas de fornecer implementações padrão.
21711 Bernard
Interfaces são a "moeda" das referências a objetos. De um modo geral, eles são o fundamento do comportamento polimórfico. As classes abstratas servem a um propósito diferente que o deadalnix explicou perfeitamente.
Jiggy
4
Não é como dizer "os modos de transporte estão obsoletos agora que temos carros?" Sim, na maioria das vezes, você usa um carro. Mas se você precisar de algo além de um carro ou não, não seria realmente correto dizer "Não preciso usar modos de transporte". Uma interface é muito o mesmo como uma classe abstrata, sem qualquer implementação, e com um nome especial, não?
Jack V.

Respostas:

111

Não.

As interfaces não podem fornecer implementação padrão, classes abstratas e métodos. Isso é especialmente útil para evitar a duplicação de código em muitos casos.

Essa também é uma maneira muito boa de reduzir o acoplamento seqüencial. Sem método / classes abstratos, você não pode implementar o padrão de método de modelo. Sugiro que você consulte este artigo da wikipedia: http://en.wikipedia.org/wiki/Template_method_pattern

deadalnix
fonte
3
@deadalnix O padrão do método Template pode ser bastante perigoso. Você pode facilmente acabar com um código altamente acoplado e começar a invadir o código para estender a classe abstrata modelada para lidar com "apenas mais um caso".
quant_dev 21/07
9
Isso parece mais um anti-padrão. Você pode obter exatamente a mesma coisa com a composição em uma interface. O mesmo se aplica às classes parciais com alguns métodos abstratos. Em vez de forçar o código do cliente a subclasse e substituí-lo, você deve injetar a implementação . Veja: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos
4
Isso não explica em nada como reduzir o acoplamento seqüencial. Concordo que existem várias maneiras de atingir esse objetivo, mas, por favor, responda ao problema que você está falando em vez de invocar cegamente algum princípio. Você terminará fazendo programação de cultos de carga se não puder explicar por que isso está relacionado ao problema real.
deadalnix
3
@deadalnix: Dizer que o acoplamento seqüencial é melhor reduzido com o padrão de modelo é a programação do culto à carga (usar o padrão de estado / estratégia / fábrica também pode funcionar), assim como a suposição de que ele sempre deve ser reduzido. O acoplamento seqüencial geralmente é uma mera conseqüência da exposição de um controle granular sobre algo, o que significa apenas uma troca. Isso ainda não significa que não posso escrever um invólucro que não tenha acoplamento sequencial usando composição. De fato, torna muito mais fácil.
back2dos
4
Java agora tem implementações padrão para interfaces. Isso muda a resposta?
raptortech97
15

Existe um método abstrato para que você possa chamá-lo de dentro da sua classe base, mas implementá-lo em uma classe derivada. Portanto, sua classe base sabe:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

Essa é uma maneira razoavelmente boa de implementar um furo no padrão intermediário sem que a classe derivada conheça as atividades de configuração e limpeza. Sem métodos abstratos, você teria que confiar na classe derivada implementando DoTaske lembrando-se de chamar base.DoSetup()e base.DoCleanup()o tempo todo.

Editar

Além disso, obrigado ao deadalnix por postar um link para o Template Method Pattern , que é o que eu descrevi acima sem realmente saber o nome. :)

Scott Whitlock
fonte
2
+1, prefiro sua resposta a deadalnix '. Note que você pode implementar um método de modelo de uma forma mais simples e direta usando delegados (em C #):public void DoTask(Action doWork)
Joh
11
@ Oh - verdade, mas isso não é necessariamente tão bom, dependendo da sua API. Se sua classe base é Fruite sua classe derivada é Apple, você deseja chamar myApple.Eat(), não myApple.Eat((a) => howToEatApple(a)). Além disso, você não quer Appleter que ligar base.Eat(() => this.howToEatMe()). Eu acho que é mais limpo substituir um método abstrato.
10118 Scott-Locklock
11
@ Scott Whitlock: Seu exemplo usando maçãs e frutas é um pouco desapegado da realidade para julgar se a herança é preferível aos delegados em geral. Obviamente, a resposta é "depende", portanto deixa muito espaço para debates ... De qualquer forma, acho que os métodos de modelo ocorrem muito durante a refatoração, por exemplo, ao remover código duplicado. Nesses casos, geralmente não quero mexer na hierarquia de tipos e prefiro ficar longe da herança. Esse tipo de cirurgia é mais fácil de fazer com lambdas.
Joh
Esta pergunta ilustra mais do que uma simples aplicação do padrão Template Method - o aspecto público / não público é conhecido na comunidade C ++ como o padrão Non Virtual Interface . Ao ter uma função pública não virtual, envolva a virtual - mesmo que inicialmente o primeiro não faça nada além de chamá-lo - você deixa um ponto de personalização para instalação / limpeza, registro, criação de perfil, verificações de segurança etc. que podem ser necessárias posteriormente.
Tony
10

Não, eles não são obsoletos.

De fato, existe uma diferença obscura, porém fundamental, entre Classes / Métodos Abstratos e Interfaces.

se o conjunto de classes em que uma delas deve ser usada tiver um comportamento comum que eles compartilham (classes relacionadas, quero dizer), então vá para Classes / métodos abstratos.

Exemplo: funcionário, oficial, diretor - todas essas classes têm CalculateSalary () em comum, usam classes base abstratas.

Se suas classes não tiverem nada em comum (classes não relacionadas, no contexto escolhido) entre elas, mas tiverem uma ação muito diferente na implementação, vá para Interface.

Exemplo: vaca, banco, carro, classes não relacionadas ao telescópio, mas o Isortable pode estar lá para classificá-las em uma matriz.

Essa diferença é geralmente ignorada quando abordada de uma perspectiva polimórfica. Mas, pessoalmente, sinto que há situações em que uma é mais adequada que a outra pela razão explicada acima.

WinW
fonte
6

Além das outras boas respostas, há uma diferença fundamental entre interfaces e classes abstratas que ninguém mencionou especificamente, a saber, que as interfaces são muito menos confiáveis e, portanto, impõem uma carga de teste muito maior do que as classes abstratas. Por exemplo, considere este código C #:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Estou garantido que existem apenas dois caminhos de código que preciso testar. O autor de Frobbit pode confiar no fato de que o frobber é vermelho ou verde.

Se, em vez disso, dizemos:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Agora eu não sei absolutamente nada sobre os efeitos dessa ligação para Frob lá. Eu preciso ter certeza de que todo o código do Frobbit é robusto contra qualquer possível implementação do IFrobber , mesmo implementações de pessoas que são incompetentes (ruins) ou ativamente hostis a mim ou aos meus usuários (muito piores).

As classes abstratas permitem evitar todos esses problemas; usa-os!

Eric Lippert
fonte
11
O problema de que você fala deve ser resolvido usando tipos de dados algébricos, em vez de dobrar as classes para um ponto em que elas violem o princípio de aberto / fechado.
precisa saber é o seguinte
11
Em primeiro lugar, nunca disse que é bom ou moral . Você colocou isso na minha boca. Em segundo lugar (sendo o meu ponto atual): não passa de uma desculpa ruim para um recurso de idioma ausente. Por fim, existe uma solução sem classes abstratas: pastebin.com/DxEh8Qfz . Em contraste com isso, sua abordagem usa classes aninhadas e abstratas, jogando tudo, menos o código que requer a segurança juntos, em uma grande bola de barro. Não há um bom motivo para o RedFrobber ou o GreenFrobber estarem vinculados às restrições que você deseja impor. Aumenta o acoplamento e bloqueia em muitas decisões sem nenhum benefício.
back2dos
11
Dizer que os métodos abstratos são obsoletos é errado, sem dúvida, mas alegar, por outro lado, que eles devem ser preferidos às interfaces é um equívoco. Eles são simplesmente uma ferramenta para resolver problemas diferentes das interfaces.
Groo
2
"existe uma diferença fundamental [...] nas interfaces que são muito menos confiáveis ​​[...] do que nas classes abstratas". Eu discordo, a diferença que você ilustrou no seu código depende de restrições de acesso, que eu acho que não tem nenhum motivo para diferir significativamente entre interfaces e classes abstratas. Pode ser assim em C #, mas a questão é independente de idioma.
Joh
11
@ back2dos: Gostaria apenas de salientar que a solução de Eric, de fato, usa um tipo de dados algébrico: uma classe abstrata é um tipo de soma. Chamar um método abstrato é o mesmo que correspondência de padrões em um conjunto de variantes.
Rodrick Chapman
4

Você mesmo diz:

Você precisa de uma classe abstrata com alguma implementação? Crie uma interface, crie uma classe. Herdar a classe, implementar a interface

isso soa muito trabalho em comparação com 'herdar a classe abstrata'. Você pode trabalhar por si mesmo abordando o código de uma visão 'purista', mas acho que já tenho o suficiente para fazer isso sem tentar adicionar à minha carga de trabalho sem nenhum benefício prático.

gbjbaanb
fonte
Sem mencionar que, se a classe não herdar a interface (que não foi mencionada, deveria), você deverá escrever funções de encaminhamento em alguns idiomas.
Sjoerd 21/07
Hum, o "Muito trabalho" adicional que você mencionou é que você criou uma interface e teve as classes implementá-la - isso realmente parece muito trabalho? Além disso, é claro, a interface oferece a capacidade de implementar a interface a partir de uma nova hierarquia que não funcionaria com uma classe base abstrata.
Bill K
4

Como comentei no post do @deadnix: As implementações parciais são um anti-padrão, apesar do fato de que o padrão do modelo as formaliza.

Uma solução limpa para este exemplo da wikipedia do padrão de modelo :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

Esta solução é melhor, porque usa composição em vez de herança . O padrão de modelo introduz uma dependência entre a implementação das regras de monopólio e a implementação de como os jogos devem ser executados. No entanto, essas são duas responsabilidades completamente diferentes e não há boas razões para associá-las.

back2dos
fonte
+1. Como nota lateral: "Implementações parciais são um antipadrão, apesar do fato de que o padrão de modelo as formaliza". A descrição na wikipedia define o padrão de forma limpa, apenas o exemplo de código está "errado" (no sentido de que usa herança quando não é necessário e existe uma alternativa mais simples, como ilustrado acima). Em outras palavras, não acho que o padrão em si seja o culpado, apenas a maneira como as pessoas tendem a implementá-lo.
Joh
2

Não. Mesmo sua alternativa proposta inclui o uso de classes abstratas. Além disso, como você não especificou o idioma, vou seguir em frente e dizer que o código genérico é a melhor opção do que a herança frágil de qualquer maneira. Classes abstratas têm vantagens significativas sobre interfaces.

DeadMG
fonte
-1. Não entendo o que "código genérico versus herança" tem a ver com a pergunta. Você deve ilustrar ou justificar por que "as classes abstratas têm vantagens significativas sobre as interfaces".
Joh
1

Classes abstratas não são interfaces. São classes que não podem ser instanciadas.

Você precisa de uma aula totalmente abstrata? Crie uma interface em seu lugar. Você precisa de uma classe abstrata com alguma implementação? Crie uma interface, crie uma classe. Herdar a classe, implemente a interface. Um benefício adicional é que algumas classes podem não precisar da classe pai, mas apenas implementam a interface.

Mas então você teria uma classe inútil não abstrata. Métodos abstratos são necessários para preencher o furo de funcionalidade na classe base.

Por exemplo, dada esta classe

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Como você implementaria essa funcionalidade básica em uma classe que não tem nem Frob()nem IsFrobbingNeeded?

configurador
fonte
11
Uma interface também não pode ser instanciada.
Sjoerd 21/07
@ Sjoerd: Mas você precisa de uma classe base com a implementação compartilhada; isso não pode ser uma interface.
Configurator
1

Eu sou um criador da estrutura de servlet, onde as classes abstratas desempenham um papel essencial. Eu diria mais: preciso de métodos semi-abstratos, quando um método precisa ser substituído em 50% dos casos e eu gostaria de ver um aviso do compilador sobre esse método não ter sido substituído. Eu resolvo o problema adicionando anotações. Voltando à sua pergunta, existem dois casos de uso diferentes de classes abstratas e interfaces, e ninguém está obsoleto até agora.

Dmitriy R
fonte
0

Eu não acho que eles sejam obsoletos por interfaces, mas podem ser obsoletos pelo padrão de estratégia.

O uso primário de uma classe abstrata é adiar uma parte da implementação; para dizer, "esta parte da implementação da classe pode ser diferente".

Infelizmente, uma classe abstrata força o cliente a fazer isso por herança . Considerando que o padrão de estratégia permitirá alcançar o mesmo resultado sem nenhuma herança. O cliente pode criar instâncias da sua classe em vez de sempre definir a sua própria, e a "implementação" (comportamento) da classe pode variar dinamicamente. O padrão de estratégia tem a vantagem adicional de que o comportamento pode ser alterado no tempo de execução, não apenas no tempo de design, e também possui um acoplamento significativamente mais fraco entre os tipos envolvidos.

Dave Cousineau
fonte
0

Geralmente, tenho enfrentado muito, muito, mais problemas de manutenção associados a interfaces puras do que os ABCs, mesmo os ABCs usados ​​com herança múltipla. YMMV - não sei, talvez nossa equipe apenas os tenha usado inadequadamente.

Dito isto, se usarmos uma analogia do mundo real, quanto uso há para interfaces puras completamente desprovidas de funcionalidade e estado? Se eu usar o USB como exemplo, é uma interface razoavelmente estável (acho que estamos no USB 3.2 agora, mas também manteve a compatibilidade com versões anteriores).

No entanto, não é uma interface sem estado. Não é desprovido de funcionalidade. É mais como uma classe base abstrata do que como uma interface pura. Na verdade, é mais perto de uma classe concreta com requisitos funcionais e de estado muito específicos, sendo a única abstração o que se conecta à porta e a única parte substituível.

Caso contrário, seria apenas um "buraco" no seu computador com um fator de forma padronizado e requisitos funcionais muito mais frouxos que não fariam nada por si só até que cada fabricante tivesse seu próprio hardware para fazer esse buraco fazer algo, nesse ponto torna-se um padrão muito mais fraco e nada mais que um "buraco" e uma especificação do que deve fazer, mas nenhuma provisão central de como fazê-lo. Enquanto isso, podemos terminar com 200 maneiras diferentes de fazer isso, depois que todos os fabricantes de hardware tentarem criar suas próprias maneiras de anexar funcionalidade e estado a esse "buraco".

E nesse ponto, podemos ter certos fabricantes que apresentam problemas diferentes em relação a outros. Se precisarmos atualizar a especificação, poderemos ter 200 implementações de portas USB concretas diferentes, com maneiras totalmente diferentes de lidar com a especificação que precisam ser atualizadas e testadas. Alguns fabricantes podem desenvolver implementações padrão de fato que compartilham entre si (sua classe base analógica implementando essa interface), mas não todas. Algumas versões podem ser mais lentas que outras. Alguns podem ter melhor rendimento, mas pior latência ou vice-versa. Alguns podem usar mais energia da bateria que outros. Alguns podem desbotar e não funcionar com todo o hardware que deveria funcionar com portas USB. Alguns podem exigir que um reator nuclear seja conectado para operar, o que tem tendência a envenenar os usuários.

E foi o que eu encontrei, pessoalmente, com interfaces puras. Pode haver alguns casos em que eles fazem sentido, como apenas para modelar o fator de forma de uma placa-mãe contra um gabinete de CPU. As analogias do fator de forma são, de fato, praticamente sem estado e sem funcionalidade, como no "buraco" analógico. Mas, muitas vezes, considero um grande erro as equipes considerarem que, de alguma forma, é superior em todos os casos, nem mesmo perto.

Pelo contrário, acho que muito mais casos seriam resolvidos melhor pelos ABCs do que pelas interfaces, se essas forem as duas opções, a menos que sua equipe seja tão gigantesca que seja realmente desejável ter o equivalente analógico acima de 200 implementações USB concorrentes em vez de um padrão central para manter. Em uma antiga equipe em que participei, tive que lutar muito para afrouxar o padrão de codificação para permitir ABCs e herança múltipla, e principalmente em resposta a esses problemas de manutenção descritos acima.


fonte