Dovrei usare metodi astratti o virtuali?


11

Se assumiamo che non sia desiderabile che la classe base sia una pura classe di interfaccia, e usando i 2 esempi dal basso, qual è un approccio migliore, usando la definizione di classe di metodo astratta o virtuale?

  • Il vantaggio della versione "astratta" è che probabilmente ha un aspetto più pulito e costringe la classe derivata a dare un'implementazione speranzosa significativa.

  • Il vantaggio della versione "virtuale" è che può essere facilmente inserito da altri moduli e utilizzato per i test senza aggiungere un mucchio di framework sottostanti come richiede la versione astratta.

Versione astratta:

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

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

Versione virtuale:

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
    //////////////////////////////////////////////
}

Perché supponiamo che un'interfaccia non sia desiderabile?
Anthony Pegram,

Senza un problema parziale, è difficile dire che uno è migliore dell'altro.
Codismo,

@Anthony: un'interfaccia non è desiderabile perché ci sono funzionalità utili che andranno anche in questa classe.
Dunk,

4
return ReturnType.NotImplemented? Sul serio? Se non puoi rifiutare il tipo non implementato al momento della compilazione (puoi; usa metodi astratti) almeno getta un'eccezione.
Jan Hudec,

3
@Dunk: qui sono necessari. I valori restituiti rimarranno deselezionati.
Jan Hudec,

Risposte:


15

Il mio voto, se consumassi le tue cose, sarebbe per i metodi astratti. Ciò corrisponde a "fallimento precoce". Al momento della dichiarazione può essere doloroso aggiungere tutti i metodi (anche se qualsiasi strumento di refactoring decente lo farà rapidamente), ma almeno so qual è il problema immediatamente e risolverlo. Preferirei piuttosto che eseguire il debug di 6 mesi e 12 modifiche successive di 12 persone per vedere perché improvvisamente stiamo ottenendo un'eccezione non implementata.


Un buon punto per quanto riguarda inaspettatamente ottenere l'errore NotImplemented. Che è un vantaggio sul lato astratto, perché otterrai un errore di compilazione anziché di runtime.
Dunk,

3
+1 - Oltre a fallire presto, gli eredi possono vedere immediatamente quali metodi devono implementare tramite "Implementa la classe astratta" anziché fare ciò che pensavano fosse sufficiente e quindi fallire in fase di esecuzione.
Telastyn,

Ho accettato questa risposta perché il fallimento durante la compilazione è stato un vantaggio che non sono riuscito a elencare e a causa del riferimento all'utilizzo dell'IDE per implementare automaticamente i metodi rapidamente.
Dunk,

25

La versione virtuale è sia soggetta a bug che semanticamente errata.

Abstract sta dicendo "questo metodo non è implementato qui. È necessario implementarlo per far funzionare questa classe"

Virtual sta dicendo "Ho un'implementazione predefinita ma puoi cambiarmi se ne hai bisogno"

Se il tuo obiettivo finale è la testabilità, le interfacce sono normalmente l'opzione migliore. (questa classe fa x anziché questa classe è ax). Potrebbe essere necessario suddividere le classi in componenti più piccoli per farlo funzionare correttamente.


3

Questo dipende dall'uso della tua classe.

Se i metodi hanno un'implementazione “vuota” ragionevole, hai molti metodi e spesso ne ignori solo alcuni, quindi usare i virtualmetodi ha senso. Ad esempio ExpressionVisitorè implementato in questo modo.

Altrimenti, penso che dovresti usare abstractmetodi.

Idealmente, non dovresti avere metodi che non sono implementati, ma in alcuni casi questo è l'approccio migliore. Ma se decidi di farlo, tali metodi dovrebbero lanciare NotImplementedException, non restituire un valore speciale.


Vorrei sottolineare che "NotImplementedException" indica spesso un errore di omissione, mentre "NotSupportedException" indica una scelta palese. A parte questo, sono d'accordo.
Anthony Pegram,

Vale la pena notare che molti metodi sono definiti in termini di "Fai tutto ciò che è necessario per soddisfare tutti gli obblighi relativi a X". Invocare un metodo simile su un oggetto che non ha elementi correlati a X può essere inutile, ma sarebbe comunque ben definito. Inoltre, se un oggetto può o meno avere degli obblighi relativi a X è generalmente più pulito ed efficiente dire incondizionatamente "Soddisfa tutti i tuoi obblighi relativi a X", piuttosto che chiedere prima se ha degli obblighi e chiedergli condizionatamente di soddisfarli .
supercat,

1

Suggerirei di riconsiderare di avere un'interfaccia separata definita quale implementa la classe di base, quindi seguire l'approccio astratto.

Codice di imaging come questo:

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
    //////////////////////////////////////////////
}

In questo modo risolve questi problemi:

  1. Avendo tutto il codice che utilizza oggetti derivati ​​da AbstractVersion ora può essere implementato per ricevere invece l'interfaccia IVersion, Ciò significa che possono essere più facilmente testati dall'unità.

  2. La versione 2 del prodotto può quindi implementare un'interfaccia IVersion2 per fornire funzionalità aggiuntive senza violare il codice dei clienti esistenti.

per esempio.

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
    //////////////////////////////////////////////
}

Vale anche la pena leggere anche sull'inversione delle dipendenze, per evitare che questa classe contenga dipendenze codificate che impediscono test di unità efficaci.


Ho votato per fornire la possibilità di gestire diverse versioni. Tuttavia, ho provato e riprovato a fare un uso efficace delle classi di interfaccia, come una normale parte della progettazione e finendo sempre per rendermi conto che le classi di interfaccia forniscono un valore minimo / minimo e in realtà offuscano il codice invece di semplificare la vita. È molto raro che io abbia ereditato più classi da un'altra, come una classe di interfaccia, e non c'è una buona dose di comunanza che non sia condivisibile. Le classi astratte tendono a funzionare meglio quando ottengo l'aspetto condivisibile che le interfacce non forniscono.
Dunk

L'uso dell'ereditarietà con nomi di classi validi fornisce un modo molto più intuitivo per comprendere facilmente le classi (e quindi il sistema) rispetto a un mucchio di nomi di classi di interfacce funzionali che sono difficili da collegare mentalmente nel loro insieme. Inoltre, l'uso delle classi di interfaccia tende a creare un sacco di classi extra, rendendo il sistema più difficile da comprendere in un altro modo.
Dunk

-2

L'iniezione di dipendenza si basa su interfacce. Ecco un breve esempio. Class Student ha una funzione chiamata CreateStudent che richiede un parametro che implementa l'interfaccia "IReporting" (con un metodo ReportAction). Dopo aver creato uno studente, chiama ReportAction sul parametro di classe concreto. Se il sistema è impostato per inviare un'e-mail dopo aver creato uno studente, inviamo una classe concreta che invia un'e-mail nella sua implementazione ReportAction, oppure potremmo inviare un'altra classe concreta che invia output a una stampante nella sua implementazione ReportAction. Ottimo per il riutilizzo del codice.


1
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 5 risposte
moscerino del
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.