An der Schnittstelle

Schnittstellen sind das Aushängeschild einer Komponente. Mit ihrer Qualität stehen und fallen sowohl der Entwurf als auch die Benutzbarkeit einer Komponente. Nur leider berücksichtigen dies nicht alle Schnittstellen in der freien Wildbahn

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 6 Min.
Von
  • Dr. Michael Stal
Inhaltsverzeichnis

Schnittstellen sind das Aushängeschild einer Komponente. Mit ihrer Qualität stehen und fallen sowohl der Entwurf als auch die Benutzbarkeit einer Komponente. Nur leider berücksichtigen das nicht alle Schnittstellen in der freien Wildbahn.

In der Steinzeit der Objektorientierung war ich in einem Architekturreview für ein CORBA-basiertes Telekommunikationssystem involviert. Die Herren Architekten – weibliche Vertreter dieser Spezies sind leider nur selten anzutreffen – überreichten mir ein Dokument über mehrere Dutzend Seiten. Auf meine Frage, wie viele CORBA-Objekte sich wohl hinter der Beschreibung verbergen, bekam ich als Antwort: "Wieso CORBA-Objekte? Es geht um ein einziges CORBA-Objekt." Haben Sie schon einmal eine seitenlange Schnittstellenbeschreibung für die einzige Schnittstelle einer einzelnen Komponente gesehen? Ich hoffe, Ihnen begegnet der Fall zumindest nicht täglich. Wie man sieht, "size matters!".

Daneben gibt es allerdings auch die Fraktion der Minimalisten, deren Schnittstelle wohl wie folgt ausschauen könnte:

interface ISimplistic {
void justDoIt(parameterWithAVeryLongAndComplexDatatypeThatContainsAllYouNeed)
}

Dass diese Schnittstelle einfach ist, dürfte unbestritten sein. Sie ist sogar schrecklich einfach und einfach schrecklich. Syntaktisch gibt sie sich sehr übersichtlich. Semantisch dürften die Entwickler die wirkliche Komplexität einfach unter dem Teppich gekehrt haben.

Zu groß ist schlecht, zu klein auch nicht besser. Was nun?

Eine gute Schnittstellendefinition dürfte irgendwo zwischen dem maximalistischen Ansatz weiter oben und dem eben vorgestellten minimalistischen Ansatz liegen. Diese Erkenntnis bringt Sie allerdings noch keinen Deut weiter. Zum Glück gibt es einige bewährte Prinzipien, die uns helfen können.

Da wäre zum Beispiel das Single-Responsibility-Prinzip. Jede Schnittstelle darf demzufolge nur eine einzige Verantwortung haben, die aber exklusiv. Wenn wir also eine Komponente betrachten, sollte diese statt einer riesigen aufgeblähten Schnittstelle ein paar übersichtliche rollenbasierte Schnittstellen bereitstellen. Etwa eine Testschnittstelle, eine Monitoring-Schnittstelle, eine oder mehrere fachliche Schnittstellen, eine Schnittstelle für Ereignismeldungen von der Komponente, eine für Ereignismeldungen an die Komponente, und eine Konfigurationsschnittstelle.

Das zweite Prinzip ist der kontraktbasierter Entwurf. Eine Schnittstelle ist demnach kein Kreidehaufen, sondern definiert ein Verhalten. Einzelne Methoden besitzen dazu Vorbedingungen, Nachbedingungen und Invarianten. Zudem kann eine Schnittstelle auch ein Protokoll vorschreiben, etwa dass ein OpenFile()-Aufruf notwendig ist, bevor Read() aufgerufen werden kann. So etwas lässt sich ganz gut mit Zustandsmaschinen beschreiben. Eine Schnittstelle muss aber kein Protokoll definieren, denn Protokolle machen selbstverständlich nur für zustandsbehaftete Interaktionen Sinn.

Gemäß dem "Law of Demeter"-Prinzip darf eine Schnittstelle keine Implementierungsdetails einer Komponente nach außen sichtbar machen. Sobald sich die Implementierung einer Komponente ändert, müssten Entwickler sonst auch die Schnittstellen anpassen, und die Komponentennutzer wiederum ihren Code, was unangenehme Dominoeffekte zur Folge haben kann. Und natürlich sollte eine Schnittstelle wesentlich stabiler sein als ihre Implementierungen.

Das Prinzip der idiomatischen Gestaltung bedeutet, dass Schnittstellen eine Art von Sprache definieren sollten. Wenn ein Textstream Methoden wie Open() und Close() besitzt, sollten andere Arten von Streams ebenfalls diese Methoden bereitstellen. Die Namenskonventionen müssen einheitlich und die Namen auch semantisch verständlich sein. Man könnte das auch als Prinzip der kleinstmöglichen Überraschung bezeichnen. Idiomatische Ausrichtung hat nur rudimentär mit Entwurfsschönheit zu tun, sondern primär mit Verständlichkeit und Ergonomie.

Laut Substitutionsprinzip von Barbara Liskov können mehrere Komponenten mit einem gemeinsamen Schnittstellentyp bezüglich dieser Schnittstelle gleich behandelt werden. Wenn also mehrere Komponenten eine Schnittstelle IPersistence implementieren, könnte eine PersistenceStorage-Komponente mit allen IPersistence-Instanzen umgehen, egal welcher Komponententyp jeweils dahinter steckt.

Wer vermutet, dass ich hier nur von Schnittstellen wie in Java spreche, liegt falsch. Schnittstellen gibt es schließlich auch bei Klassen, Traits, Mixins, Services ...

Wesentlich komplexer sind Klassenschnittstellen. Die besitzen zum Beispiel dank Zugriffsattributen wie "public", "private" und "protected" (Achtung, Sprachabhängigkeit!) verschiedene Schutzschichten:

  • eine private-Schnittstelle nach innen für den Zugriff durch die eigene Implementierung,
  • eine protected-Schnittstelle zum Zugriff über Kindklassen und
  • eine public-Schnittstelle für den Rest der Welt.

Zudem gibt es dort abstrakte und konkrete Methoden. Abstrakte Methoden einer Klasse zusammengenommen bilden auch nur eine normale Schnittstelle à la Java. Abgesehen davon, gelten für Klassenschnittstellen dieselben Entwurfsrichtlinien wie die eingangs erwähnten.

Ein Klasse könnte übrigens dank Mehrfachvererbung mehrere Schnittstellen besitzen, wenn die Sprache kein Interface-, Mixin- oder Trait-Konzept unterstützt. Allerdings gilt Mehrfachvererbung inzwischen als schlechtes Design. Das ist allerdings ein anderes Thema, ebenso wie die Besonderheiten bei parametrisierten Typen (Generics).

Interessant in diesem Zusammenhang ist das Liskovsche Substitutionsprinzip, das bei Anwendung auf Vererbung von Klassen bedeutet: Eine Kindklasseninstanz darf dort benutzt werden, wo eine Elternklasseninstanz erwartet wird, weil beide als gemeinsame Schnittstelle die der Elternklasse implementieren.

Schnittstellen bilden die Firewall zwischen ihren Nutzern und ihrer Implementierung. Daher hängen Qualitätsfaktoren wie Verständlichkeit oder Änderbarkeit einer Implementierung auch von der Schnittstellenspezifikation ab. Da anzunehmen ist, dass Schnittstellen stabiler sind als Implementierungen, sollten Architekten und Entwickler Schnittstellen mit Sorgfalt spezifizieren. Änderungen von Schnittstellen sind hingegen sind als seltene und nur als Ultima Ratio, gut begründete Ausnahmen zulässig. Wenn unterschiedliche Komponenten dieselbe Rolle nach außen unterstützen, sollten sie dafür auch denselben Schnittstellentyp verwenden. Beispiel: eine Iteratorschnittstelle für alle Collections.

Und als Warnung am Schluss: umdokumentierte Schnittstellen sind die Ursache vieler Sicherheitsprobleme, ebenso wie Schnittstellen, die vor der Auslieferung nicht eliminiert wurden wie Testschnittstellen. Schnittstellendesign darf schon deshalb kein Nebenprodukt sein. ()