Soll ich abstrakte oder virtuelle Methoden verwenden?

11

Wenn wir davon ausgehen, dass es nicht wünschenswert ist, dass die Basisklasse eine reine Schnittstellenklasse ist, und die beiden folgenden Beispiele verwenden, was ist ein besserer Ansatz unter Verwendung der abstrakten oder virtuellen Methodenklassendefinition?

  • Der Vorteil der "abstrakten" Version besteht darin, dass sie wahrscheinlich sauberer aussieht und die abgeleitete Klasse zu einer hoffentlich sinnvollen Implementierung zwingt.

  • Der Vorteil der "virtuellen" Version besteht darin, dass sie leicht von anderen Modulen abgerufen und zum Testen verwendet werden kann, ohne dass eine Reihe von zugrunde liegenden Frameworks hinzugefügt werden müssen, wie es die abstrakte Version erfordert.

Abstrakte Version:

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

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

Virtuelle Version:

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
quelle
Warum gehen wir davon aus, dass eine Schnittstelle nicht wünschenswert ist?
Anthony Pegram
Ohne ein teilweises Problem ist es schwer zu sagen, dass eines besser ist als das andere.
Codism
@Anthony: Eine Schnittstelle ist nicht wünschenswert, da es nützliche Funktionen gibt, die auch in diese Klasse eingehen.
Dunk
4
return ReturnType.NotImplemented? Ernsthaft? Wenn Sie nicht unimplemented Typ zum Zeitpunkt der Kompilierung ablehnen können (Sie können, verwenden abstrakte Methoden) zumindest werfen eine Ausnahme.
Jan Hudec
3
@Dunk: Hier sie sind notwendig. Rückgabewerte werden unkontrolliert gehen.
Jan Hudec

Antworten:

15

Meine Stimme, wenn ich Ihre Sachen konsumieren würde, wäre für die abstrakten Methoden. Das geht einher mit "früh scheitern". Es mag zur Deklarationszeit schwierig sein, alle Methoden hinzuzufügen (obwohl jedes anständige Refactoring-Tool dies schnell erledigt), aber zumindest weiß ich sofort, wo das Problem liegt, und behebe es. Ich würde das lieber tun, als 6 Monate und Änderungen von 12 Personen später zu debuggen, um zu sehen, warum wir plötzlich eine nicht implementierte Ausnahme bekommen.

Erik Dietrich
quelle
Guter Punkt, um den NotImplemented-Fehler unerwartet zu erhalten. Was auf der abstrakten Seite ein Plus ist, da Sie anstelle eines Laufzeitfehlers eine Kompilierungszeit erhalten.
Dunk
3
+1 - Zusätzlich zum frühzeitigen Fehlschlagen können Erben sofort sehen, welche Methoden sie über "Implement Abstract Class" implementieren müssen, anstatt das zu tun, was sie für ausreichend hielten, und dann zur Laufzeit fehlschlagen.
Telastyn
Ich habe diese Antwort akzeptiert, weil ein Fehler beim Kompilieren ein Plus war, den ich nicht auflisten konnte, und weil auf die Verwendung der IDE zur schnellen automatischen Implementierung der Methoden verwiesen wurde.
Dunk
25

Die virtuelle Version ist sowohl fehleranfällig als auch semantisch falsch.

In der Zusammenfassung heißt es: "Diese Methode ist hier nicht implementiert. Sie müssen sie implementieren, damit diese Klasse funktioniert."

Virtual sagt: "Ich habe eine Standardimplementierung, aber Sie können mich bei Bedarf ändern."

Wenn Ihr oberstes Ziel die Testbarkeit ist, sind Schnittstellen normalerweise die beste Option. (Diese Klasse macht x anstatt diese Klasse ist ax). Möglicherweise müssen Sie Ihre Klassen in kleinere Komponenten aufteilen, damit dies gut funktioniert.

Tom Squires
quelle
3

Dies hängt von der Verwendung Ihrer Klasse ab.

Wenn die Methoden eine vernünftige „leere“ Implementierung haben, Sie viele Methoden haben und oft nur einige davon überschreiben, ist die Verwendung von virtualMethoden sinnvoll. Zum Beispiel ExpressionVisitorwird auf diese Weise implementiert.

Ansonsten denke ich, sollten Sie abstractMethoden verwenden.

Idealerweise sollten Sie keine Methoden haben, die nicht implementiert sind, aber in einigen Fällen ist dies der beste Ansatz. Wenn Sie sich jedoch dazu entschließen, sollten solche Methoden NotImplementedExceptioneinen speziellen Wert zurückgeben und nicht zurückgeben.

svick
quelle
Ich möchte darauf hinweisen, dass "NotImplementedException" häufig auf einen Auslassungsfehler hinweist, während "NotSupportedException" auf eine offene Auswahl hinweist. Davon abgesehen stimme ich zu.
Anthony Pegram
Es ist anzumerken, dass viele Methoden in Bezug auf "Tun Sie alles Notwendige, um alle mit X verbundenen Verpflichtungen zu erfüllen" definiert sind. Das Aufrufen einer solchen Methode für ein Objekt, das keine mit X verbundenen Elemente enthält, ist möglicherweise sinnlos, aber dennoch genau definiert. Wenn ein Objekt Verpflichtungen in Bezug auf X hat oder nicht, ist es im Allgemeinen sauberer und effizienter, bedingungslos "Erfüllen Sie alle Ihre Verpflichtungen in Bezug auf X" zu sagen, als zuerst zu fragen, ob es Verpflichtungen hat, und es unter bestimmten Bedingungen zu bitten, diese zu erfüllen .
Supercat
1

Ich würde vorschlagen, dass Sie überlegen, eine separate Schnittstelle zu definieren, die Ihre Basisklasse implementiert, und dann dem abstrakten Ansatz folgen.

Bildcode wie folgt:

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

Dies löst diese Probleme:

  1. Wenn der gesamte Code, der von AbstractVersion abgeleitete Objekte verwendet, jetzt implementiert werden kann, um stattdessen die IVersion-Schnittstelle zu empfangen, bedeutet dies, dass sie einfacher in Einheiten getestet werden können.

  2. Release 2 Ihres Produkts kann dann eine Schnittstelle IVersion2 implementieren, um zusätzliche Funktionen bereitzustellen, ohne den Code Ihres bestehenden Kunden zu beschädigen.

z.B.

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

Es lohnt sich auch, sich über die Abhängigkeitsinversion zu informieren, um zu verhindern, dass diese Klasse fest codierte Abhängigkeiten enthält, die effektive Komponententests verhindern.

Michael Shaw
quelle
Ich habe mich dafür entschieden, die Möglichkeit zu bieten, mit verschiedenen Versionen umzugehen. Ich habe jedoch immer wieder versucht, Schnittstellenklassen als normalen Bestandteil des Designs effektiv zu nutzen, und am Ende immer wieder festgestellt, dass die Schnittstellenklassen entweder keinen oder nur einen geringen Wert bieten und den Code tatsächlich verschleiern, anstatt das Leben zu erleichtern. Es ist sehr selten, dass ich mehrere Klassen von einer anderen erben lassen würde, wie eine Schnittstellenklasse, und es gibt keine angemessene Menge an Gemeinsamkeiten, die nicht gemeinsam genutzt werden können. Abstrakte Klassen funktionieren in der Regel besser, da ich den integrierten Aspekt bekomme, den Schnittstellen nicht bieten.
Dunk
Die Verwendung der Vererbung mit guten Klassennamen bietet eine viel intuitivere Möglichkeit, die Klassen (und damit das System) leichter zu verstehen, als eine Reihe von Klassennamen für funktionale Schnittstellen, die mental nur schwer miteinander zu verknüpfen sind. Durch die Verwendung von Schnittstellenklassen werden in der Regel eine Menge zusätzlicher Klassen erstellt, wodurch das System auf eine andere Weise schwerer zu verstehen ist.
Dunk
-2

Die Abhängigkeitsinjektion basiert auf Schnittstellen. Hier ist ein kurzes Beispiel. Class Student verfügt über eine Funktion namens CreateStudent, für die ein Parameter erforderlich ist, der die Schnittstelle "IReporting" (mit einer ReportAction-Methode) implementiert. Nachdem ein Schüler erstellt wurde, ruft er ReportAction für den konkreten Klassenparameter auf. Wenn das System so eingerichtet ist, dass nach dem Erstellen eines Schülers eine E-Mail gesendet wird, senden wir eine konkrete Klasse, die in der ReportAction-Implementierung eine E-Mail sendet, oder eine andere konkrete Klasse, die in der ReportAction-Implementierung eine Ausgabe an einen Drucker sendet. Ideal für die Wiederverwendung von Code.

Roger
quelle
1
Dies scheint nichts Wesentliches über Punkte zu bieten, die in den vorherigen 5 Antworten gemacht und erklärt wurden
Mücke