Freitag, 30. Mai 2014

Program to an Interface - Interface oder abstrakte Klasse?

"Program to an interface, not an implementation." Erstes Prinzip des objektorientierten Entwurfs, [GOF1995].
In den vorausgehenden Blogartikeln wurden ausführlich die Vorzüge und potentiellen Gegenanzeigen von Interfaces behandelt. Im vorliegenden Artikel soll nun eine Frage diskutiert werden, die bei jeder Einführung eines abstrakten Typs gestellt werden sollte. (Aus irgendeinem Grund hat sich diese Frage auch als eine häufige Frage in Bewerbungsgesprächen etabliert).

Interface oder abstrakte Klasse?

Die Frage wird von Erich Gamma selbst in einem Interview aufgeworfen: 
"Eine Frage ist, ob man immer Java-Interfaces verwenden sollte. Eine abstrakte Klasse ist ebenfalls eine gute Möglichkeit. Tatsächlich ermöglichen abstrakte Klassen mehr Flexibilität hinsichtlich der weiteren Evolution. Sie ermöglichen es, neues Verhalten hinzuzufügen, ohne die Klienten zu brechen." [Gamma2004]
Mit den in Java 8 eingeführten Default-Methoden in Interfaces ist diese Argumentation allerdings bereits wieder überholt - ein weiteres Beispiel dafür, dass die Diskussion von Konzepten immer wieder von den Realisierungsdetails der konkreten Sprache oder Entwicklungsumgebung beeinflusst wird.

In Effective Java, 2nd Edition lautet hingegen Item 18 ganz pauschal:
"Prefer interfaces to abstract classes" [Bloch2008]
Als zentrale Argumente werden die folgenden vorgetragen:
  • Durch Verwendung eines Interfaces vermeidet man die z. T. erheblichen Probleme, die sich aus der Einbindung der Subklassen in eine Vererbungshierarchie ergeben würden. Bloch argumentiert hier auf ähnliche Weise wie bei der Erläuterung des Composit Reuse Prinzips (aka Favour composition over inheritence). Die Verwendung von Interfaces erlaubt insbesondere nicht-hierarchische Typisierungen (z. B. Implementierung mehrerer Interfaces durch eine Klasse oder die Implementierung eines Interfaces durch diverse Klassen, die unzusammenhängend über die Vererbungshierarchie verteilt sind) und muss daher nicht die subtilen Kopplungen in Kauf nehmen, die sich innerhalb einer Vererbungshierarchie ergeben können.
  • Bloch hebt auch die Möglichkeit der Kombination von Interfaces und abstrakten Klassen hervor, die sogenannten Skelettimplementierungen (engl. skeletal implementations). Dabei wird der eigentliche Typ durch ein Interface deklariert, welches auch von allen Nutzern zur Typdeklaration verwendet wird. Zusätzlich wird durch eine abstrakte Klasse, die das Interface teilweise implementiert, einige Standardfunktionalität bereitgestellt, welche die Implementierung des Interfaces ggf. vereinfacht oder wiederverwendbare Standardmethoden bereitstellt. Dem Nutzer bleibt dann die Freiheit, sich in die Vererbungshierarchie der Skelettimplementierung einzubinden oder direkt vom Interface abzuleiten:
Als Variante der Skelettimplementierung bietet sich zudem die Bereitstellung einer Standardimplementierung (engl. simple implementation) an, die sich von der Skelettimplementierung nur darin unterscheidet, dass sie nicht abstrakt ist. Sie wird häufig als die einfachst mögliche Implementierung betrachtet.

Auch Microsoft und Oracle haben sich zu dem Thema geäußert: Microsoft im MSDN-Artikel Recommendations for Abstract Classes vs. Interfaces, Oracle im Tutorial-Beitrag Abstract Classes compared to Interfaces. Die dort vorgetragenen Argumente lassen sich wie folgt zusammenfassen.

Die Verwendung einer abstrakten Klasse ist naheliegend, wenn
  • eine oder mehrere Methoden von allen Subklassen geteilt werden sollen (Microsoft, Oracle)
  • sich die Subklassen mehrere Felder teilen oder wenn nicht-statische Felder deklariert werden sollen (Oracle)
  • einzelne geteilte Methoden nicht public sein sollen (Oracle) 
  • lediglich verschiedene Varianten oder Versionen einer Komponente erstellt werden sollen (Microsoft)
  • die verschiedenen Implementierungen eng verwandt sind (Microsoft)
  • die zu entwickelnde Funktionseinheit tendentiell groß ist (Microsoft)
Die Verwendung eines Interfaces ist naheliegend, wenn
  • die Möglichkeit zur Mehrfachvererbung genutzt werden soll (Oracle)
  • die zu erstellende Funktionalität von einer großen Zahl sehr unterschiedlicher, unzusammenhängender Implementierungen geteilt werden soll (Microsoft, Oracle)
  • die zu entwickelnde Funktionseinheit tendentiell klein ist (Microsoft)
  • das Verhalten eines speziellen Datentyps spezifiziert werden soll, ohne dass die konkrete Implementierung eine Rolle spielt (Oracle)
Für C# ist zu ergänzen, dass eine erwünschte Nutzung expliziter Interfaceimplementierungen (d. h. Methoden, die nur auf Variablen des Interface-Typs aufgerufen werden können, nicht auf Variablen des Subklassen-Typs) die Verwendung eines Interfaces erfordert.

Zusammenfassung

Es sind in der Regel sehr konkrete Argumente, welche die Verwendung einer abstrakten Klasse erforderlich machen, beispielsweise das erforderliche Teilen gemeinsamer Felder oder Methoden durch alle Implementierungen oder der Wunsch, einzelne Methoden nicht public zu deklarieren. Häufig lassen sich diese Situationen allerdings durch die Nutzung von Skelettimplementierungen lösen. Dennoch kann es trotz aller grundsätzlicher Bedenken auch einmal angebracht sein, eine echte Vererbungshierarchie aufzubauen, an deren Spitze eine abstrakte Basisklasse steht. Dies wird insbesondere dann der Fall sein, wenn alle Implementierungen eng verwandt sind und sich nicht über verschiedene Komponenten verteilen.

Das Hauptargument gegen die Verwendung von Interfaces bestand in der Vergangenheit in der erschwerten Evolution einmal veröffentlichter Interfaces. Mit den Default-Methoden in Java 8 könnte diesem Argument zumindest teilweise die Luft ausgehen - man wird beobachten müssen, wie sich Default-Methoden in der Praxis schlagen. In Sprachen, die keinen Mechanismus zur Implementierung von Default-Methoden bereitstellen, muss allerdings das Problem der Schnittstellen-Evolution im Auge behalten werden. In den meisten anderen Fällen ist die Verwendung von Interfaces oder einer Kombination von Interface mit Skelettimplementierung angebracht. Diese vermeiden die problematischen Eigenschaften einer Vererbungshierarchie und bieten daher eine maximale Entkopplung des verwendeten Typs von seiner Implementierung.

Siehe auch

Alle Artikel der Serie "Program to an Interface ...":

Einführung
Kategorisierung von Interfaces
Totale 1:n-Interfaces
Totale 1:1-Interfaces
Freiwillig partielle Interfaces
Notwendig partielle Interfaces
Fazit und Gegenbeispiele
Interface oder abstrakte Klasse?
Werkzeugunterstützung
Das Prinzip

Quellen

[Bloch2008] - Effective Java 2nd Edition, Joshua Bloch (2008)
[Gamma2004] - Design Principles from Design Patterns - A conversation with Erich Gamma, Interview von Bill Venners, siehe http://www.artima.com/lejava/articles/designprinciples.html
[GOF1995] - Design Patterns - Elements of Reusable Software, E. Gamma, R. Helm, R. Johnson, J. Vlissides, (Addison‐Wesley, 1995)