Sind abstrakte Klassen / Methoden überholt?


37

Ich habe viele abstrakte Klassen / Methoden erstellt. Dann habe ich angefangen, Schnittstellen zu verwenden.

Jetzt bin ich mir nicht sicher, ob Interfaces abstrakte Klassen nicht überflüssig machen.

Sie benötigen eine vollständig abstrakte Klasse? Erstellen Sie stattdessen eine Schnittstelle. Sie benötigen eine abstrakte Klasse mit einer Implementierung? Erstellen Sie eine Schnittstelle, erstellen Sie eine Klasse. Erben Sie die Klasse, implementieren Sie die Schnittstelle. Ein zusätzlicher Vorteil besteht darin, dass einige Klassen die übergeordnete Klasse möglicherweise nicht benötigen, sondern lediglich die Schnittstelle implementieren.

Sind abstrakte Klassen / Methoden überholt?


Wie wäre es, wenn Ihre Programmiersprache keine Schnittstellen unterstützt? Ich scheine mich daran zu erinnern, dass dies bei C ++ der Fall ist.
Bernard

7
@Bernard: in C ++, eine abstrakte Klasse ist eine Schnittstelle in allen , aber Namen. Dass sie auch mehr als "reine" Schnittstellen können, ist kein Nachteil.
gbjbaanb

@ gbjbaanb: Ich nehme an. Ich kann mich nicht erinnern, sie als Schnittstellen verwendet zu haben, sondern vielmehr, um Standardimplementierungen bereitzustellen.
Bernard

Schnittstellen sind die "Währung" von Objektreferenzen. Im Allgemeinen sind sie das Fundament für polymorphes Verhalten. Abstrakte Klassen erfüllen einen anderen Zweck, den Deadalnix perfekt erklärt hat.
jiggy

4
Ist das nicht so, als würde man sagen: "Sind Verkehrsträger veraltet, jetzt haben wir Autos?" Ja, die meiste Zeit benutzt du ein Auto. Aber ob Sie jemals etwas anderes als ein Auto brauchen oder nicht, es wäre nicht richtig zu sagen "Ich muss keine Verkehrsmittel benutzen". Eine Schnittstelle ist viel das gleiche wie eine abstrakte Klasse ohne Implementierung und mit einem speziellen Namen, nicht wahr?
Jack V.

Antworten:


111

Nein.

Schnittstellen können keine Standardimplementierung bereitstellen, abstrakte Klassen und Methoden. Dies ist besonders nützlich, um in vielen Fällen eine Codeduplizierung zu vermeiden.

Dies ist auch eine sehr gute Möglichkeit, die sequentielle Kopplung zu reduzieren. Ohne abstrakte Methode / Klassen können Sie kein Vorlagenmethodenmuster implementieren. Ich schlage vor, Sie schauen sich diesen Wikipedia-Artikel an: http://en.wikipedia.org/wiki/Template_method_pattern


3
@deadalnix Template-Methodenmuster können sehr gefährlich sein. Sie können leicht mit stark gekoppeltem Code enden und damit beginnen, den Code zu hacken, um die mit Vorlagen versehene abstrakte Klasse auf "nur noch einen Fall" zu erweitern.
quant_dev

9
Dies scheint eher ein Anti-Pattern zu sein. Sie können genau das gleiche mit Komposition gegen eine Schnittstelle erhalten. Gleiches gilt für Teilklassen mit einigen abstrakten Methoden. Anstatt den Client-Code zu zwingen, Unterklassen zu bilden und diese zu überschreiben, sollte die Implementierung injiziert werden . Siehe: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos

4
Dies erklärt überhaupt nicht, wie die sequentielle Kopplung verringert werden kann. Ich bin damit einverstanden, dass es mehrere Möglichkeiten gibt, um dieses Ziel zu erreichen, aber bitte beantworten Sie das Problem, über das Sie sprechen, anstatt sich blind auf ein Prinzip zu berufen. Sie werden am Ende Frachtkult-Programmierung machen, wenn Sie nicht erklären können, warum dies mit dem eigentlichen Problem zusammenhängt.
Deadalnix

3
@deadalnix: Zu sagen, dass die sequentielle Kopplung mit dem Vorlagenmuster am besten reduziert wird, ist eine Frachtkultprogrammierung (die Verwendung des Zustands- / Strategie- / Fabrikmusters kann ebenso gut funktionieren), wie die Annahme, dass es immer reduziert werden muss. Sequentielle Kopplung ist oft eine bloße Folge der Offenlegung einer sehr feinkörnigen Kontrolle über etwas, was nichts anderes als ein Kompromiss bedeutet. Das bedeutet immer noch nicht, dass ich keinen Wrapper schreiben kann, der keine sequentielle Kopplung mit Komposition hat. In der Tat macht es viel einfacher.
back2dos

4
Java verfügt jetzt über Standardimplementierungen für Schnittstellen. Ändert das die Antwort?
Raptortech97

15

Es gibt eine abstrakte Methode, mit der Sie sie aus Ihrer Basisklasse aufrufen, aber in einer abgeleiteten Klasse implementieren können. Ihre Basisklasse weiß also:

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

protected abstract void DoWork();

Dies ist eine einigermaßen gute Methode, um ein Loch im mittleren Muster zu implementieren, ohne dass die abgeleitete Klasse etwas über die Setup- und Bereinigungsaktivitäten weiß. Ohne abstrakte Methoden müsste man sich darauf verlassen, dass die abgeleitete Klasse implementiert DoTaskund sich daran erinnert, immer base.DoSetup()und immer aufzurufen base.DoCleanup().

Bearbeiten

Vielen Dank auch an deadalnix für den Link zum Template Method Pattern , den ich oben beschrieben habe, ohne den Namen zu kennen. :)


2
+1, ich bevorzuge deine Antwort auf deadalnix '. Beachten Sie, dass Sie mithilfe von Delegaten (in C #) eine Vorlagenmethode einfacher implementieren können:public void DoTask(Action doWork)
Joh

1
@Joh - stimmt, aber das ist nicht unbedingt so schön, abhängig von Ihrer API. Wenn Ihre Basisklasse Fruitund Ihre abgeleitete Klasse ist Apple, möchten Sie anrufen myApple.Eat(), nicht myApple.Eat((a) => howToEatApple(a)). Außerdem möchten Sie nicht Appleanrufen müssen base.Eat(() => this.howToEatMe()). Ich denke, es ist sauberer, nur eine abstrakte Methode zu überschreiben.
Scott Whitlock

1
@Scott Whitlock: Ihr Beispiel mit Äpfeln und Früchten ist etwas zu realitätsfern, um beurteilen zu können, ob das Streben nach Vererbung Delegierten im Allgemeinen vorzuziehen ist. Offensichtlich lautet die Antwort "es kommt darauf an", sodass viel Raum für Debatten bleibt ... Trotzdem finde ich, dass Template-Methoden beim Refactoring häufig vorkommen, z. B. beim Entfernen von doppeltem Code. In solchen Fällen möchte ich mich normalerweise nicht mit der Typhierarchie herumschlagen, und ich halte mich lieber von der Vererbung fern. Diese Art der Operation ist bei Lambdas einfacher.
Joh

Diese Frage zeigt mehr als eine einfache Anwendung des Musters der Vorlagenmethode - der öffentliche / nicht öffentliche Aspekt ist in der C ++ - Community als das Muster der nicht virtuellen Schnittstelle bekannt . Wenn Sie eine öffentliche, nicht virtuelle Funktion haben, die die virtuelle Funktion einschließt - auch wenn die erste zunächst nur die zweite Funktion aufruft - lassen Sie einen Anpassungspunkt für Setup / Bereinigung, Protokollierung, Profilerstellung, Sicherheitsüberprüfungen usw., die möglicherweise später benötigt werden.
Tony

10

Nein, sie sind nicht überholt.

Tatsächlich gibt es einen obskuren, aber grundlegenden Unterschied zwischen abstrakten Klassen / Methoden und Schnittstellen.

Wenn die Menge der Klassen, in denen eine dieser Klassen verwendet werden muss, ein gemeinsames Verhalten aufweist (verwandte Klassen, meine ich), dann entscheiden Sie sich für abstrakte Klassen / Methoden.

Beispiel: clerk, Officer, Director - alle diese Klassen haben CalculateSalary () gemeinsam, verwenden abstrakte Basisklassen. CalculateSalary () kann unterschiedlich implementiert werden, aber es gibt bestimmte andere Dinge wie GetAttendance (), die in der Basisklasse eine gemeinsame Definition haben.

Wenn Ihre Klassen nichts gemeinsam haben (im gewählten Kontext nicht verwandte Klassen), aber eine Aktion haben, die sich in der Implementierung stark unterscheidet, dann entscheiden Sie sich für Interface.

Beispiel: Kuh-, Bank-, Auto-, Telesope-unabhängige Klassen, aber Isortable kann vorhanden sein, um sie in einem Array zu sortieren.

Dieser Unterschied wird normalerweise ignoriert, wenn man sich aus einer polymorphen Perspektive nähert. Ich persönlich habe jedoch das Gefühl, dass es Situationen gibt, in denen einer aus dem oben erläuterten Grund besser geeignet ist als der andere.


6

Neben den anderen guten Antworten gibt es einen fundamentalen Unterschied zwischen Interfaces und abstrakten Klassen, den niemand speziell erwähnt hat, nämlich dass Interfaces weitaus weniger zuverlässig sind und daher eine viel größere Testlast auferlegen als abstrakte Klassen. Betrachten Sie beispielsweise diesen C # -Code:

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);
         ...
    }
}

Mir wird garantiert, dass es nur zwei Codepfade gibt, die ich testen muss. Der Autor von Frobbit kann sich darauf verlassen, dass der Frobber entweder rot oder grün ist.

Wenn wir stattdessen sagen:

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);
         ...
    }
}

Ich weiß jetzt absolut nichts über die Auswirkungen dieses Anrufs bei Frob. Ich muss sicher sein, dass der gesamte Code in Frobbit gegenüber jeder möglichen Implementierung von IFrobber robust ist , auch gegenüber Implementierungen von Personen, die inkompetent (schlecht) oder aktiv gegen mich oder meine Benutzer (weitaus schlimmer) sind.

Mit abstrakten Klassen können Sie all diese Probleme vermeiden. benutze sie!


1
Das Problem, von dem Sie sprechen, sollte gelöst werden, indem Sie algebraische Datentypen verwenden, anstatt Klassen so weit zu biegen, dass sie das Open / Closed-Prinzip verletzen.
back2dos

1
Erstens habe ich nie gesagt, dass es gut oder moralisch ist . Das steckst du mir in den Mund. Zweitens (das ist mein eigentlicher Punkt): Es ist nichts anderes als eine schlechte Entschuldigung für eine fehlende Sprachfunktion. Schließlich gibt es eine Lösung ohne abstrakte Klassen: pastebin.com/DxEh8Qfz . Im Gegensatz dazu werden bei Ihrem Ansatz verschachtelte und abstrakte Klassen verwendet, wobei alle, außer dem Code, der die gemeinsame Sicherheit erfordert, in einen großen Schlammballen geworfen werden. Es gibt keinen guten Grund, warum RedFrobber oder GreenFrobber an die Einschränkungen gebunden sein sollten, die Sie erzwingen möchten. Es erhöht die Kopplung und sichert viele Entscheidungen ohne Nutzen.
back2dos

1
Zu behaupten, abstrakte Methoden seien veraltet, ist zweifelsohne falsch, aber zu behaupten, sie sollten Schnittstellen vorgezogen werden, ist ein Irrtum. Sie sind lediglich ein Werkzeug zur Lösung anderer Probleme als Schnittstellen.
Groo

2
"Es gibt einen grundlegenden Unterschied, [...] Schnittstellen sind weit weniger zuverlässig als abstrakte Klassen". Ich bin anderer Meinung, der Unterschied, den Sie in Ihrem Code dargestellt haben, beruht auf Zugriffsbeschränkungen, von denen ich glaube, dass sie keinen Grund haben, zwischen Interfaces und abstrakten Klassen signifikant zu unterscheiden. Es mag in C # so sein, aber die Frage ist sprachunabhängig.
Joh

1
@ back2dos: Ich möchte nur darauf hinweisen, dass Erics Lösung tatsächlich einen algebraischen Datentyp verwendet: Eine abstrakte Klasse ist ein Summentyp. Das Aufrufen einer abstrakten Methode entspricht dem Mustervergleich für eine Reihe von Varianten.
Rodrick Chapman

4

Du sagst es selbst:

Sie benötigen eine abstrakte Klasse mit einer Implementierung? Erstellen Sie eine Schnittstelle, erstellen Sie eine Klasse. Erben Sie die Klasse, implementieren Sie die Schnittstelle

das klingt nach viel arbeit im vergleich zu 'die abstrakte klasse erben'. Sie können sich die Arbeit selbst machen, indem Sie sich aus puristischer Sicht dem Code nähern, aber ich finde, dass ich bereits genug zu tun habe, ohne zu versuchen, meinen Arbeitsaufwand ohne praktischen Nutzen zu erhöhen.


Ganz zu schweigen davon, dass Sie Weiterleitungsfunktionen in einigen Sprachen schreiben müssen, wenn die Klasse die Schnittstelle nicht erbt (was nicht erwähnt wurde).
Sjoerd

Ähm, die zusätzliche "Menge Arbeit", die Sie erwähnt haben, ist, dass Sie ein Interface erstellt haben und die Klassen es implementieren ließen - scheint das wirklich viel Arbeit zu sein? Darüber hinaus können Sie die Schnittstelle natürlich über eine neue Hierarchie implementieren, die mit einer abstrakten Basisklasse nicht funktionieren würde.
Bill K

4

Wie ich in @deadnix-Post kommentiert habe: Teilweise Implementierungen sind ein Anti-Pattern, obwohl das Template-Pattern sie formalisiert.

Eine saubere Lösung für dieses Wikipedia-Beispiel des Vorlagenmusters :

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
}

Diese Lösung ist besser, da anstelle der Vererbung die Komposition verwendet wird . Das Vorlagenmuster führt eine Abhängigkeit zwischen der Implementierung der Monopoly-Regeln und der Implementierung der Art und Weise ein, wie Spiele ausgeführt werden sollen. Dies sind jedoch zwei völlig unterschiedliche Zuständigkeiten, und es gibt keinen guten Grund, sie zu koppeln.


+1. Als Randnotiz: "Partielle Implementierungen sind ein Anti-Pattern, obwohl das Template-Pattern sie formalisiert." Die Beschreibung auf Wikipedia definiert das Muster sauber, nur das Codebeispiel ist "falsch" (in dem Sinne, dass es Vererbung verwendet, wenn es nicht benötigt wird und eine einfachere Alternative besteht, wie oben dargestellt). Mit anderen Worten, ich glaube nicht, dass das Muster selbst schuld ist, sondern nur die Art und Weise, wie die Leute dazu neigen, es umzusetzen.
Joh

2

Nein. Auch Ihre vorgeschlagene Alternative beinhaltet die Verwendung abstrakter Klassen. Da Sie die Sprache nicht angegeben haben, gehe ich gleich vor und sage, dass generischer Code ohnehin die bessere Option als spröde Vererbung ist. Abstrakte Klassen haben erhebliche Vorteile gegenüber Schnittstellen.


-1. Ich verstehe nicht, was "generischer Code vs. Vererbung" mit der Frage zu tun hat. Sie sollten veranschaulichen oder begründen, warum "abstrakte Klassen signifikante Vorteile gegenüber Schnittstellen haben".
Joh

1

Abstrakte Klassen sind keine Schnittstellen. Das sind Klassen, die nicht instanziiert werden können.

Sie benötigen eine vollständig abstrakte Klasse? Erstellen Sie stattdessen eine Schnittstelle. Sie benötigen eine abstrakte Klasse mit einer Implementierung? Erstellen Sie eine Schnittstelle, erstellen Sie eine Klasse. Erben Sie die Klasse, implementieren Sie die Schnittstelle. Ein zusätzlicher Vorteil besteht darin, dass einige Klassen die übergeordnete Klasse möglicherweise nicht benötigen, sondern lediglich die Schnittstelle implementieren.

Aber dann hätten Sie eine nicht abstrakte nutzlose Klasse. Abstrakte Methoden sind erforderlich, um die Funktionslücke in der Basisklasse auszufüllen.

Zum Beispiel mit dieser Klasse

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

    public abstract boolean IsFrobbingNeeded { get; }

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

Wie würden Sie diese Basisfunktionalität in einer Klasse implementieren, die weder Frob()noch hat IsFrobbingNeeded?


1
Eine Schnittstelle kann auch nicht instanziiert werden.
Sjoerd

@Sjoerd: Aber du brauchst eine Basisklasse mit der gemeinsamen Implementierung; Das kann keine Schnittstelle sein.
Konfigurator

1

Ich bin ein Entwickler von Servlet-Frameworks, bei denen abstrakte Klassen eine wesentliche Rolle spielen. Ich würde mehr sagen, ich brauche semi-abstrakte Methoden, wenn eine Methode in 50% der Fälle überschrieben werden muss und ich möchte, dass die Warnung des Compilers darüber nicht überschrieben wird. Ich behebe das Problem beim Hinzufügen von Anmerkungen. Zurück zu Ihrer Frage: Es gibt zwei verschiedene Anwendungsfälle für abstrakte Klassen und Interfaces, und bisher ist noch niemand überholt.


0

Ich glaube nicht, dass sie durch Schnittstellen veraltet sind, aber sie könnten durch das Strategiemuster veraltet sein.

Die primäre Verwendung einer abstrakten Klasse besteht darin, einen Teil der Implementierung zu verschieben. zu sagen, "dieser Teil der Implementierung der Klasse kann anders gemacht werden".

Leider zwingt eine abstrakte Klasse den Client dazu, dies über die Vererbung zu tun . Während das Strategiemuster es Ihnen ermöglicht, dasselbe Ergebnis ohne Vererbung zu erzielen. Der Client kann Instanzen Ihrer Klasse erstellen, anstatt immer ihre eigenen zu definieren, und die "Implementierung" (das Verhalten) der Klasse kann dynamisch variieren. Das Strategiemuster hat den zusätzlichen Vorteil, dass das Verhalten nicht nur zur Entwurfszeit zur Laufzeit geändert werden kann, sondern auch eine erheblich schwächere Kopplung zwischen den beteiligten Typen aufweist.


0

Ich hatte im Allgemeinen weitaus mehr Wartungsprobleme mit reinen Schnittstellen als mit ABCs, selbst mit ABCs, die mehrfach vererbt wurden. YMMV - keine Ahnung, vielleicht hat unser Team sie nur unzureichend eingesetzt.

Wenn wir jedoch eine reale Analogie verwenden, wie viel Nutzen haben reine Schnittstellen, denen Funktionalität und Status völlig fehlen? Wenn ich als Beispiel USB verwende, ist das eine ziemlich stabile Schnittstelle (ich denke, wir haben jetzt USB 3.2, aber die Abwärtskompatibilität wurde beibehalten).

Es ist jedoch keine zustandslose Schnittstelle. Es ist nicht ohne Funktionalität. Es ist eher eine abstrakte Basisklasse als eine reine Schnittstelle. Es ist tatsächlich näher an einer konkreten Klasse mit sehr spezifischen Funktions- und Statusanforderungen, wobei die einzige Abstraktion das ist, was als einziger austauschbarer Teil in den Port eingesteckt wird.

Andernfalls wäre es nur eine "Lücke" in Ihrem Computer mit einem standardisierten Formfaktor und viel geringeren funktionalen Anforderungen, die nichts für sich tun würden, bis sich jeder Hersteller eine eigene Hardware ausgedacht hätte, um dieses Loch zu veranlassen, zu welchem ​​Zeitpunkt etwas zu tun es wird ein viel schwächerer Standard und nichts weiter als ein "Loch" und eine Spezifikation dessen, was es tun soll, aber keine zentrale Bestimmung, wie es getan werden soll. In der Zwischenzeit stehen uns möglicherweise 200 verschiedene Möglichkeiten zur Verfügung, nachdem alle Hardwarehersteller versucht haben, ihre eigenen Möglichkeiten zu finden, um die Funktionalität und den Status dieser "Lücke" festzulegen.

Und zu diesem Zeitpunkt haben wir möglicherweise bestimmte Hersteller, die andere Probleme vorziehen. Wenn wir die Spezifikation aktualisieren müssen, stehen möglicherweise 200 verschiedene konkrete USB-Port-Implementierungen mit völlig unterschiedlichen Methoden zur Verfügung, um die Spezifikation zu lösen, die aktualisiert und getestet werden müssen. Einige Hersteller entwickeln möglicherweise De-facto-Standardimplementierungen, die sie untereinander teilen (Ihre analoge Basisklasse, die diese Schnittstelle implementiert), aber nicht alle. Einige Versionen sind möglicherweise langsamer als andere. Einige haben möglicherweise einen besseren Durchsatz, aber eine schlechtere Latenz oder umgekehrt. Einige verbrauchen möglicherweise mehr Batteriestrom als andere. Einige funktionieren möglicherweise nicht mit der gesamten Hardware, die mit USB-Anschlüssen kompatibel sein soll. Einige könnten erfordern, dass ein Kernreaktor angeschlossen wird, um zu arbeiten, der dazu neigt, seinen Benutzern eine Strahlenvergiftung zuzufügen.

Und das habe ich persönlich mit reinen Schnittstellen gefunden. In einigen Fällen können sie sinnvoll sein, z. B. um den Formfaktor eines Motherboards anhand eines CPU-Gehäuses zu modellieren. Formfaktor-Analogien sind in der Tat so gut wie zustandslos und funktionslos wie das analoge "Loch". Aber ich halte es oft für einen großen Fehler für Teams, das in jedem Fall als überlegen zu betrachten, nicht einmal als nahe.

Im Gegenteil, ich denke, weitaus mehr Fälle lassen sich mit ABCs besser lösen als mit Schnittstellen, wenn dies die beiden Möglichkeiten sind, es sei denn, Ihr Team ist so gigantisch, dass es eigentlich wünschenswert ist, über das analoge Äquivalent von 200 konkurrierenden USB-Implementierungen zu verfügen, anstatt über einen zentralen Standard pflegen. In einem früheren Team, in dem ich war, musste ich tatsächlich hart kämpfen, nur um den Codierungsstandard zu lockern, um ABCs und Mehrfachvererbung zu ermöglichen, und zwar hauptsächlich als Reaktion auf diese oben beschriebenen Wartungsprobleme.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.