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

Ich ziehe die hierarchische Darstellung auch deshalb vor, da man in großen Systemen sonst kein ausreichendes Mittel mehr besitzt, den Überblick zu bewahren. In einem groben Überblick zeigt man nur die Toplevel Pakete der Anwendung, durch "Zoomen" sieht man dann tiefer in die Substruktur der Pakete hinein und wird nicht sofort von der Vielzahl der Pakete, die gleichberechtigt nebeneinander stehen, erschlagen. So ein Toplevel Design sieht z.B. so aus:

Toplevel Package Design

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: Optionen Package Design

  1. Direkt im Paket, in der sich auch die Implementierung befindet.
  2. In einem Paket, das sich innerhalb des Implementierungspakets befindet.
  3. In einem Paket, das das Implementierungspaket enthält.
  4. In einem vollständig unabhängigen Paket.
(Wenn man statt einer hierarchischen Ordnung eine flache Liste annimmt, dann fallen b) und c) mit d) zusammen. Ich bespreche trotzdem alle vier Optionen.)

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:

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.