Devo usar métodos abstratos ou virtuais?

11

Se assumirmos que não é desejável que a classe base seja uma classe de interface pura, e usando os 2 exemplos abaixo, qual é a melhor abordagem, usando a definição de classe de método abstrato ou virtual?

  • A vantagem da versão "abstrata" é que ela provavelmente parece mais limpa e força a classe derivada a fornecer uma implementação esperançosamente significativa.

  • A vantagem da versão "virtual" é que ela pode ser facilmente acessada por outros módulos e usada para teste sem adicionar um monte de estrutura subjacente, como a versão abstrata exige.

Versão abstrata:

public abstract class AbstractVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Versão virtual:

public class VirtualVersion
{
    public virtual ReturnType Method1()
    {
        return ReturnType.NotImplemented;
    }

    public virtual ReturnType Method2()
    {
        return ReturnType.NotImplemented;
    }
             .
             .
    public virtual ReturnType MethodN()
    {
        return ReturnType.NotImplemented;
    }

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}
Dunk
fonte
Por que estamos assumindo que uma interface não é desejável?
Anthony Pegram
Sem um problema parcialmente, é difícil dizer que um é melhor que o outro.
Codism
@ Anthony: Uma interface não é desejável porque existe uma funcionalidade útil que também entra nessa classe.
Dunk
4
return ReturnType.NotImplemented? Seriamente? Se você não pode rejeitar o tipo não implementado em tempo de compilação (você pode; usar métodos abstratos) pelo menos lance uma exceção.
Jan Hudec 27/02
3
@ Dunk: Aqui eles são necessários. Os valores retornados serão desmarcados.
Jan Hudec

Respostas:

15

Meu voto, se eu estivesse consumindo suas coisas, seria pelos métodos abstratos. Isso combina com "falhar cedo". No momento da declaração, pode ser difícil adicionar todos os métodos (embora qualquer ferramenta de refatoração decente faça isso rapidamente), mas pelo menos eu sei qual é o problema imediatamente e o corrijo. Prefiro depurar 6 meses e 12 pessoas em alterações depois para ver por que de repente estamos recebendo uma exceção não implementada.

Erik Dietrich
fonte
Bom ponto em relação à obtenção inesperada do erro NotImplemented. O que é uma vantagem no lado abstrato, porque você receberá um erro em tempo de compilação em vez de em tempo de execução.
Dunk
3
+1 - Além de falharem cedo, os herdeiros podem ver imediatamente quais métodos eles precisam implementar por meio de "Implementar Classe Abstrata", em vez de fazer o que achavam suficiente e falhar em tempo de execução.
Telastyn
Aceitei esta resposta porque falhar no tempo de compilação foi uma vantagem que não consegui listar e por causa da referência ao uso do IDE para implementar automaticamente os métodos rapidamente.
Dunk
25

A versão virtual é propensa a erros e semanticamente incorreta.

O resumo está dizendo "este método não é implementado aqui. Você deve implementá-lo para fazer essa classe funcionar"

Virtual está dizendo "Eu tenho uma implementação padrão, mas você pode me mudar se precisar"

Se seu objetivo final é a testabilidade, as interfaces normalmente são a melhor opção. (esta classe faz x em vez de esta classe é ax). Talvez você precise dividir suas classes em componentes menores para que isso funcione bem.

Tom Squires
fonte
3

Isso depende do uso da sua turma.

Se os métodos tiverem uma implementação "vazia" razoável, você terá vários métodos e geralmente substituirá apenas alguns deles; o uso de virtualmétodos faz sentido. Por exemplo, ExpressionVisitoré implementado dessa maneira.

Caso contrário, acho que você deve usar abstractmétodos.

Idealmente, você não deve ter métodos que não são implementados, mas, em alguns casos, essa é a melhor abordagem. Mas se você decidir fazer isso, esses métodos devem ser lançados NotImplementedException, não retornando algum valor especial.

svick
fonte
Eu observaria que "NotImplementedException" geralmente indica um erro de omissão, enquanto "NotSupportedException" indica uma opção aberta. Fora isso, eu concordo.
Anthony Pegram
Vale a pena notar que muitos métodos são definidos em termos de "Faça o que for necessário para satisfazer quaisquer obrigações relacionadas ao X". Invocar esse método em um objeto que não possui itens relacionados ao X pode ser inútil, mas ainda assim está bem definido. Além disso, se um objeto pode ou não ter obrigações relacionadas a X, geralmente é mais limpo e mais eficiente dizer incondicionalmente "Satisfazer todas as suas obrigações relacionadas a X", do que perguntar primeiro se ele tem alguma obrigação e pedir condicionalmente para satisfazê-las. .
Supercat 21/02
1

Eu sugiro que você reconsidere ter uma interface separada definida que sua classe base implementa e siga a abordagem abstrata.

Código de imagem como este:

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public abstract class AbstractVersion : IVersion
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Fazer isso resolve estes problemas:

  1. Por ter todo o código que usa objetos derivados de AbstractVersion agora pode ser implementado para receber a interface IVersion. Isso significa que eles podem ser mais facilmente testados em unidade.

  2. A versão 2 do seu produto pode implementar uma interface IVersion2 para fornecer funcionalidade adicional sem quebrar o código de clientes existente.

por exemplo.

public interface IVersion
{
    ReturnType Method1();        
    ReturnType Method2();
             .
             .
    ReturnType MethodN();
}

public interface IVersion2
{
    ReturnType Method2_1();
}

public abstract class AbstractVersion : IVersion, IVersion2
{
    public abstract ReturnType Method1();        
    public abstract ReturnType Method2();
             .
             .
    public abstract ReturnType MethodN();
    public abstract ReturnType Method2_1();

    //////////////////////////////////////////////
    // Other class implementation stuff is here
    //////////////////////////////////////////////
}

Também vale a pena ler sobre inversão de dependência, para impedir que essa classe contenha dependências codificadas que impedem testes de unidade eficazes.

Michael Shaw
fonte
Votei por fornecer a capacidade de lidar com versões diferentes. No entanto, tentei e tentei novamente fazer uso efetivo das classes de interface, como parte normal do design, e sempre acabo percebendo que as classes de interface fornecem pouco ou nenhum valor e, na verdade, ofuscam o código, em vez de facilitar a vida. É muito raro que eu tenha várias classes herdadas de outra, como uma classe de interface, e não há uma quantidade razoável de pontos em comum que não seja compartilhável. As aulas abstratas tendem a funcionar melhor, já que eu entendo o aspecto compartilhável que as interfaces não fornecem.
Dunk
E usar herança com bons nomes de classe fornece uma maneira muito mais intuitiva de entender facilmente as classes (e, portanto, o sistema) do que um monte de nomes de classes de interface funcional que são difíceis de unir mentalmente como um todo. Além disso, o uso de classes de interface tende a criar uma tonelada de classes extras, dificultando a compreensão do sistema de outra maneira.
Dunk
-2

A injeção de dependência depende de interfaces. Heres um pequeno exemplo. O aluno da turma tem uma função chamada CreateStudent que requer um parâmetro que implemente a interface "IReporting" (com um método ReportAction). Depois de criar um aluno, ele chama ReportAction no parâmetro concreto da classe. Se o sistema estiver configurado para enviar um email após a criação de um aluno, enviaremos uma classe concreta que envia um email em sua implementação ReportAction, ou podemos enviar outra classe concreta que envia saída para uma impressora em sua implementação ReportAction. Ótimo para reutilização de código.

Roger
fonte
1
este não parece oferecer nada substancial sobre pontos feitos e explicado em anteriores 5 respostas
mosquito