Der russische Schriftsteller Isaac Asimov hat neben zahlreichen Werken, die primär der Science-Fiction angehören, auch zahlreiche Kurzgeschichten geschrieben: Dazu gehört unter anderem eine kriminalistische Kurzgeschichte mit dem Titel What’s in a Name?, in der es um die Aufklärung eines Mordes geht.
Den Titel What’s in a Name? könnte man im Deutschen sinngemäß am Besten mit der Frage wiedergeben, welche Bedeutung einem Namen innewohnt – und ob einem Namen überhaupt eine Bedeutung innewohnt.
Angelehnt an diese Semantik möchte ich heute die Frage stellen, welche Bedeutung einer Schnittstelle innewohnt. Ausgangslage für diese Frage ist eine Diskussion, die ich vor einigen Tagen mit Peter Bucher geführt habe.
Thema dieser Diskussion war ursächlich, inwiefern LightCore benannte Registrierungen von Typen unterstützen soll. Nahezu allen Microkerneln liegt das Prinzip zu Grunde, einen konkreten Typ auf eine abstrakte Schnittstelle zu registrieren, über die er dann instanziiert werden kann.
Zur Instanziierung bieten die meisten Microkernel zwei Varianten an: Entweder wird die einzig vorhandene Registrierung genutzt, um eine Instanz des konkreten Typs zu erzeugen, oder – im Fall mehrerer vorliegender Registrierung – wird von jedem Typ eine Instanz erzeugt und eine entsprechende Liste zurückgegeben.
In LightCore geschieht dies mit Hilfe der Methoden Resolve<T> und ResolveAll<T>, die eine einzelne Instanz des Typs T oder ein IEnumerable<T> zurückgeben – je nachdem, wie viele Registrierungen für den angeforderten Kontrakt vorliegen.
So weit, so gut. Nun bieten die meisten Microkernel – so derzeit auch LightCore – allerdings zusätzlich noch die Möglichkeit, im Falle mehrerer Registrierungen diese an Hand von Namen zu unterscheiden. Auf diese Art können beispielsweise mehrere Addins auf die Schnittstelle IAddIn registriert werden, dennoch kann die verwendende Anwendung gezielt die Instanz eines dedizierten Addins anfragen.
Ich persönlich konnte mich mit diesem Vorgehen noch nie anfreunden – und auch heute noch halte ich es für falsch. In unserer Diskussion ging es nun darum, die Beweggründe für meine Abneigung gegenüber benannten Registrierungen herauszufinden.
Peter hat mir als konkrete Diskussionsgrundlage die Schnittstelle IController aus dem Namensraum System.Web.Mvc in ASP.NET MVC genannt, die von jedem Controller implementiert wird. Wird nun in einer ASP.NET MVC-basierten Anwendung auf einen Microkernel gesetzt, um die einzelnen Controller zu instanziieren, so ist klar, dass alle Controller auf die gleiche Schnittstelle registriert werden.
Sobald eine Anfrage an eine solche Webanwendung gestellt wird, besteht die Aufgabe nun darin, den richtigen Controller zu instanziieren: Die Auswahl allein an Hand der Schnittstelle zu treffen, funktioniert nicht – hierüber lassen sich die einzelnen Controller nicht unterscheiden. Ein Aufruf von ResolveAll<IController> nützt aber auch nichts, da auf diese Art alle Controller instanziiert werden würden.
Die vermeintliche Lösung liegt nun darin, jede Registrierung eines Controllers mit einem Namen auszustatten, und vom Microkernel sodann eine benannte Instanz anzufordern. Dass dieses Vorgehen funktioniert, steht außer Frage – doch ist es sinnvoll? Ich meine, nein. Denn es widerspricht dem Sinn einer Schnittstelle.
Die Idee einer Schnittstelle ist, verschiedene Implementierungen eines Aspekts zu ermöglichen, die potenziell gegeneinander austauschbar sind: Eine Schnittstelle dient also dazu, nach außen gleiches Verhalten anzubieten, aber mit verschiedenen Implementierungen – wobei die konkret gewählte Implementierung für den Verwender transparent ist.
In anderen Worten: Angenommen, es liegen eine Schnittstelle IDataSource sowie zwei Implementierungen, SqlServerDataSource und XmlDataSource, vor. Dann ist es für die Anwendung, die auf IDataSource aufsetzt, irrelevant, welche konkrete Implementierung konfiguriert wurde – denn das Verhalten der Implementierungen nach außen ist gleich, nur die Implementierung unterscheidet sich.
Dieses Muster dient als Basis für austauschbare Komponenten: Austauschbarkeit gelingt nur, wenn neben der Syntax auch die Semantik gleich bleibt – lediglich die Umsetzung darf schwanken.
Auf das Beispiel der Controller in ASP.NET MVC bezogen trifft dieses Muster aber nicht zu – denn die Syntax der einzelnen Controller ist zwar identisch, die Semantik aber nicht. Denn beispielsweise ein HomeController verhält sich schlicht und ergreifend anders als ein DownloadController, obwohl beide durchaus die Schnittstelle IController implementieren.
Der Sinn der beiden Controller beziehungsweise ihre Existenzberechtigung sind verschiedener Natur, sie erfüllen verschiedene Aufgaben und sind auch nicht gegen einander austauschbar. Zwei Implementierungen eines HomeControllers wären austauschbar – aber nicht ein Home- und ein DownloadController.
Bei den Controllern gilt also das genaue Gegenteil von oben genanntem Paradigma: Hier wird nicht gleiches Verhalten auf unterschiedliche Art implementiert, statt dessen wird unterschiedliches Verhalten auf die gleiche Art implementiert! Das ist ein gravierender Unterschied in Bezug auf die Semantik. Dass die Syntax bei all dem gleich bleibt, ist lediglich ein Nebeneffekt – der aber für die Komponentenorientierung an dieser Stelle nichts zur Sache tut.
Kurzum: Es ist schlicht und ergreifend falsch, zwei unterschiedliche Typen von Controllern auf die gleiche Schnittstelle zu registrieren. Wenn man diese Tatsache erst einmal akzeptiert hat, wird auch schnell deutlich, was genau sich an benannten Registrierungen falsch anfühlt: Sie sind keine Lösung, sie sind lediglich ein Workaround für ein tiefer liegendes Problem.
Die Frage lautet nun – was wäre eine korrekte Lösung, die sich nicht im Nachhinein als halbherziger Workaround entpuppt?
Eine Möglichkeit wäre, zwischen die Schnittstelle IController und die jeweils konkreten Implementierungen eine weitere Schnittstelle einzuziehen – wie IHomeController oder IDownloadController. Diese wären geeignet, eine semantische Unterscheidung zwischen den einzelnen Controllern zu begründen, würden benannte Registrierungen überflüssig machen, bei all dem aber die gemeinsame Syntax auf Grund der gemeinsamen Basis IController erhalten.
Abgesehen von der dadurch fehlenden Notwendigkeit für benannte Registrierungen hätte diese Variante zum anderen den Vorteil der echten Austauschbarkeit: Da nun auf eine semantisch korrekt definierte Schnittstelle registriert wird, könnten auch verschiedene Implementierungen beispielsweise des HomeControllers problemlos gegeneinander ausgetauscht werden.
Letztlich wage ich die Behauptung aufzustellen, dass bereits die Existenz benannter Registrierungen in Microkerneln ein Fehler ist – denn entweder ist es tatsächlich sinnvoll, verschiedene gleichwertige konkrete Typen auf die gleiche Schnittstelle zu registrieren – dann sollten diese aber auch als gleichwertig behandelt werden – oder eben nicht. Ein Mischmasch aus beidem ergibt keinen Sinn.
Sofern die Einführung zwischengeschalteter Schnittstellen nicht möglich ist, gibt es darüber hinaus immer noch die Möglichkeit, einzelne Typen mit einem entsprechenden Attribut oder einer Markerschnittstelle auszustatten – um diese dann nach dem Laden durch den Microkernel eindeutig identifizieren zu können.
Abschließend lautet mein Plädoyer daher, dass die Fähigkeit, benannte Registrierungen durchzuführen, kein Feature eines Microkernels ist, sondern ein Bug – der enthalten ist, weil alle anderen es ebenso machen. Wünschenswert wäre aus meiner Sicht, die Unterstützung für benannte Registrierungen zu entfernen und statt dessen auf ein sauberes Verfahren zu wechseln – einige Ansätze hierfür habe ich genannt.