Wohin mit den Schnittstellen?
16.11.2008 Permalink Wer sich ernsthaft um die Wartbarkeit seiner Software sorgt, weiss: über Komponentengrenzen hinweg programmiert man gegen Schnittstellen, nicht gegen Implementierungen. Zwar bieten Klassen durch ihre öffentlichen Methoden eine Schnittstelle zu ihrer Implementierung an, häufig aber lohnt es sich, die Schnittstelle komplett zu extrahieren, um sich unabhängig von konkreten Implementierungen zu machen.Die Schnittstelle im engeren Sinn ist das Interface. Im etwas weiter gefassten Sinn besteht sie aber auch aus Typen, die dort verwendet werden: alle Typen, die als Eingaben oder Ergebnisse einer Methode gelten sowie die deklarierten Exception-Typen. Eine Schnittstelle besteht also häufig aus einer Menge von Typen, nicht nur einem Interface.
package cartmgt; import java.util.List; public interface ShoppingCartManager { ShoppingCart getOrCreateCart (long userId); void mergeCarts (ShoppingCart destination, ShoppingCart source); void findAndRemoveUnusedCarts (); List<ShoppingCart> findCartsForProduct (long productId); }
Im obigen Beispiel gehören ShoppingCart und List zur Schnittstelle dazu.
Um eine Java Software zu strukturieren, verfügen wir neben Klassen über Pakete als Sprachmittel. Jede Klasse ist einem Paket zugeordnet. Für die folgende Diskussion ist es nützlich zu verstehen, dass man die Paketstruktur entweder als hierarchische Ordnung oder aber als flache Liste von Namensräumen verstehen kann. Für ersteres spricht
- Eine Paketstruktur wird im Dateisystem durch geschachtelte Verzeichnisse gebildet.
- UML Pakete, die ein auffällige Verwandschaft zu Java Packages haben, lassen sich ebenfalls schachteln.
- Nicht wenige Werkzeuge stellen das Paket de.organisation.bereich.anwendung.frontend als Teil des Pakets de.organisation.bereich.anwendung dar und ermöglichen so Darstellungen der Softwarestruktur auf mehreren Ebenen.
Bei der Diskussion mit Entwicklern und Architekten stelle ich gelegentlich eine gewisse Unsicherheit fest, nach welchen Regeln man Pakete bilden soll. Eine maßgebliche Rolle bei den nötigen Entscheidungen spielen Schnittstellen, die ja wohldefinierte Verbindungspunkte der Systemteile sein sollen.
Wo also legen wir die Schnittstelle, die aus mehreren Klassen und Interfaces bestehen kann, ab?
Es gibt mehrere Optionen:
- Direkt im Paket, in der sich auch die Implementierung befindet.
- In einem Paket, das sich innerhalb des Implementierungspakets befindet.
- In einem Paket, das das Implementierungspaket enthält.
- In einem vollständig unabhängigen Paket.
Offensichtlich macht a) wenig Sinn, denn die Paketsicht sagt uns, dass Implementierung und Schnittstelle eine untrennbare Einheit bilden. Das passt aber nicht so recht zu unserer Intention, uns von der Implementierung unabhängig zu machen, um ihre Austauschbarkeit zu gewährleisten.
b) und c) sagen gemäß hierarchischer Ordnung etwas aus wie "die Schnittstelle ist Teil der Implementierung" bzw. "die Implementierung ist Teil der Schnittstelle". Beides ist nicht in unserem Sinne, denn richtig ist: die Implementierung erfüllt die Schnittstelle. Möglicherweise erfüllt sie noch andere Schnittstellen, und möglicherweise erfüllen aber auch viele andere Implementierungen diese eine Schnittstelle. Ein eineindeutige Zuordnung ist mithin nicht angemessen.
Und so bleibt nur d): Schnittstellen gehören in eigenständige Pakete. Sie sind "Bürger erster Klasse" in jedem Design, sei es auf Papier oder im Code manifestiert.
Das passt nicht nur zu den Erkenntnissen, die R.C.Martin aus der Betrachtung von Instabilität und Abstraktheit gewinnt, die Anwendung dieser Regel ist in der Praxis an vielen Stellen zu beobachten:
- So macht es z.B. in einem verteilten System viel Sinn, die Schnittstellen in eigene, im SKM selbständig versionierte Projekte auszulagern. In einer Web Service basierten SOA liegen daher XML-Schema, WSDL, daraus generierter Code und die Beschreibung der Semantik in einem Projekt zusammen. Man unterscheidet dann zwischen Schnittstellen- und Implementierungsprojekten.
- Im SCA Assembly Model wird ein Service deutlich von der Implementierung einer Komponente getrennt und stellt damit die Schnittstelle dar, die von anderen Komponenten referenziert werden kann.
- Wir können die Anwendung dieser Unterscheidung auch in Java Specification Requests finden, bei denen häufig eine Schnittstelle als Java API entsteht, welche von Providern wie Open Source Projekten oder Produkthäusern implementiert wird, und dabei in gänzlich anderen Paketen residiert als die passenden Implementierungen.
Wenn wir nun Abhängigkeiten in unserem System ausschließlich zu Schnittstellen zulassen, dann wäre zusätzlich ein allgemeiner Mechanismus praktisch, wie diese Interface-Abhängigkeiten zur Laufzeit zu ihren Implementierungen kommen. Das ist der Moment, in dem Dependency Injection (DI) die Bühne betritt, und uns ein entkoppeltes Design realisieren hilft, in dem Schnittstellen eine mindestens gleichberechtige Bedeutung wie Implementierungen zukommt.
Mein Fazit ist also: Package Design ist ein wichtiger Teil, um wartbare Software zu bauen, und es gibt unter der Zuhilfenahme von DI Frameworks leicht zu befolgende Regeln, mit denen man ein gesundes Design hinkriegt.