Am 13. Oktober 2008 haben Peter Bucher und ich unter dem Titel Noch Fragen, Bucher? Ja, Roden! angekündigt, jeweils zum ersten eines jeden Monats einen Kommentar zu einem vorab gemeinsam gewählten Thema verfassen zu wollen. Bisher sind in dieser Reihe folgende Kommentare erschienen:
Heute, am 1. April 2009, ist es nun wieder so weit, und unser Thema für diesen Monat lautet:
Interfaces vs abstrakte Basisklassen
So wohl Peter wie auch ich haben uns unabhängig voneinander im Vorfeld unsere Gedanken gemacht, wie wir diesem Thema gegenüberstehen. Peters Kommentar findet sich zeitgleich in seinem Blog, folgend nun mein Kommentar zu diesem Thema:
Interfaces haben in den vergangenen fünf Jahren einen ungeahnten Aufschwung erlebt. Ein – wenn nicht der – Grund hierfür liegt sicherlich in der zunehmenden Verbreitung von Microkernel-Architekturen, denen unter .NET mit Projekten wie beispielsweise Unity und autofac Rechnung getragen wird.
Allerdings ist auch die Entwicklung eines Microkernel im Eigenbau inzwischen keine hochwissenschaftliche Angelegenheit mehr – Microkernel sind also ein weiteres Thema, das nach und nach seinen elitären Status verliert und zu einer Commodity wird.
Bei all der Beschäftigung mit Interfaces verlässt ein anderes Sprachkonstrukt allerdings häufig all zu leicht das eigene Blickfeld: Abstrakte Basisklassen. Diese haben – der zunehmenden Fokussierung auf Schnittstellen zum Trotz – nach wie vor ihre berechtigte Existenz.
Die Frage lautet also: Wann bietet sich der Einsatz von Interfaces an, wann der einer abstrakten Basisklasse?
Um diese Frage beantworten zu können, empfiehlt es sich, sich zunächst einen Überblick über die jeweiligen Vor- und Nachteile zu verschaffen. Die Vorteile abstrakter Basisklassen liegen auf der Hand:
- Polymorphie: Da abstrakte Basisklassen selbst nicht instanziiert werden können, müssen davon abgeleitete konkrete Klassen erstellt werden, die daraufhin ihrerseits instanziiert werden können. Da all diese konkreten Klassen aber über die gleiche abstrakte Basisklasse verfügen, kann die abstrakte Basisklasse zur Laufzeit an Stelle einer konkreten Klasse verwendet werden. Dieses Verhalten wird als Polymorphie bezeichnet und geht einher mit dem zweiten Vorteil.
- Schnittstelle: Eine abstrakte Basisklasse definiert einen gemeinsamen Kontrakt für sich und die von ihr abgeleiteten Klassen. In jedem Fall liegt also eine wohldefinierte Schnittstelle vor. Sofern die abstrakte Basisklasse ihrerseits Code enthält, ist diese Schnittstelle potenziell nicht nur syntaktischer, sondern sogar semantischer Natur.
- Codebasis: Da es in einer abstrakten Basisklasse möglich ist, nicht nur die Definition einer Schnittstelle vorzunehmen, sondern auch Code zu hinterlegen, bietet es sich an, Code, der in allen abgeleiteten Klassen gemeinsam genutzt werden soll, in einer abstrakten Basisklasse zu platzieren, um Redundanzen zu vermeiden. Zudem steht es dem Entwickler der abstrakten Basisklasse frei, nur die Klasse selbst als abstrakt zu markieren, nicht jedoch die darin enthaltenen Methoden. Das bedeutet, dass für abgeleitete Klassen kein Vererbungszwang besteht, was sich wiederum positiv auf die Versionierbarkeit niederschlägt.
Neben all diesen positiven Aspekten gibt es jedoch auch zwei essenzielle Nachteile, die den Einsatz einer abstrakten Basisklasse potenziell von vornherein verbieten können:
- Einfachvererbung: Da .NET (und damit auch C#) keine Mehrfachvererbung unterstützen, schließt der Einsatz einer abstrakten Basisklasse den Einsatz einer anderen Basisklasse aus. Gelegentlich ist dies jedoch aus technischen Gründen zwingend erforderlich, so dass eine abstrakte Basisklasse nicht genutzt werden kann. Dies ist beispielsweise bei der Framework-eigenen Klasse MarshalByRefObject der Fall – muss von ihr abgeleitet werden, hat man als Entwickler mit einer eigenen abstrakten Basisklasse schlechte Karten.
- is-a: Auch für abstrakte Basisklassen gilt, dass es sich bei der Ableitung von ihnen um eine is-a-Relation handelt, deren Einsatz im objektorientierten Paradigma wohlüberlegt sein will. Beispielsweise ist ein Quadrat im objektorientierten Sinn eben kein Rechteck, da sich die Semantik in einigen Punkten unterscheidet.
Beide Nachteile können durch Interfaces als leichtgewichtige Alternative ausgeräumt werden:
- Mehrfachvererbung: Da Interfaces nur eine syntaktische, in keinem Fall jedoch eine semantische Schnittstelle definieren, kann eine Klasse beliebig viele Interfaces zugleich implementieren. Falls tatsächlich zwei Interfaces eine Methode mit gleicher Signatur enthalten sollten, kann die Eindeutigkeit durch explizite Implementierung wiederhergestellt werden.
- has-a: Im Gegensatz zu einer Vererbung von einer Klasse beschreibt ein Interface lediglich eine Eigenschaft oder eine Fähigkeit, über die eine implementierende Klasse verfügt. Es handelt sich also nur um eine (semantisch schwächere) has-a-Relation, was den Aufbau komplexer Klassenhierarchien deutlich vereinfacht.
Doch auch Interfaces haben einen essenziellen Nachteil:
- Codebasis: Interfaces können keinen Code enthalten, was zum einen wiederum Redundanzen und Codeduplizierung fördert. Zum anderen können Methoden in Interfaces nur ausschließlich als abstrakt definiert werden. In einer implementierenden Klasse besteht also Vererbungszwang, was die Versionierbarkeit eines Interfaces häufig unmöglich macht, in jedem Fall aber zumindest stark einschränkt.
Um nach all diesen Vorüberlegungen auf die ursprüngliche Frage zurückzukommen: Wann empfiehlt sich nun der Einsatz von Interfaces, wann der einer abstrakten Basisklasse?
Spielen die jeweiligen Nachteile keine Rolle, fällt die Entscheidung wenig überraschend nicht sonderlich schwer. Vermeintlich schwierig wird es, wenn sie eine Rolle spielen – doch wie gesagt, dies ist nur vermeintlich so: Der entscheidende Punkt ist nämlich, dass sich der Einsatz eines Interfaces und einer abstrakten Basisklasse nicht gegenseitig ausschließen.
Statt sich also auf eine der beiden Möglichkeiten einzuschränken, werden einfach beide Varianten genutzt: Aus einem oder wird ein und.
Zunächst wird dementsprechend ein Interface entwickelt, das als grundlegende syntaktische Basis dient. Darauf aufbauend wird dann eine abstrakte Basisklasse entwickelt, die eine Standardimplementierung dieses Interfaces enthält.
Konkrete Klassen haben nun die Wahl, ob sie die Freiheit eines Interfaces oder den Komfort einer abstrakten Basisklasse benötigen, und können je nach Bedarf so oder so ableiten. Die Polymorphie bleibt dabei über das Interface in jedem Fall erhalten, so dass zu einem späteren Zeitpunkt die Implementierung eines Interfaces sogar vollkommen problemlos gegen die Ableitung von einer Basisklasse (oder umgekehrt) ausgetauscht werden kann.