1. Schnittstellen als Vertrag: das Fundament der Architektur
In Java (und nicht nur dort) ist eine Schnittstelle nicht einfach nur eine Menge von Methoden. Sie ist ein Vertrag: die Zusicherung, dass jede Klasse, die die Schnittstelle implementiert, ein bestimmtes Verhalten unterstützt. Die Schnittstelle legt fest, was umgesetzt werden muss, nicht wie.
Warum ist das wichtig?
- Schichtenbildung des Codes. Dank Schnittstellen können wir trennen, „was etwas tut“, von „wie es das tut“. Wenn Sie z. B. eine Schnittstelle PaymentService haben, können verschiedene Implementierungen Zahlungen per Kreditkarte, PayPal oder Kryptowährung verarbeiten, aber der Code, der pay() aufruft, kümmert sich nicht um die Details.
- Flexibilität und Erweiterbarkeit. Sie können eine neue Implementierung der Schnittstelle hinzufügen, ohne den restlichen Code zu ändern. Das ist besonders wichtig in großen Teams und langlebigen Projekten.
- Testbarkeit. Dank Schnittstellen lassen sich Implementierungen in Tests leicht durch Testdoubles (Mocks) ersetzen, ohne den Produktionscode anzufassen.
Beispiel: Service-Schicht und DAO
Betrachten wir ein klassisches Beispiel aus Business-Anwendungen. Angenommen, wir haben eine Schnittstelle für die Arbeit mit Benutzern:
public interface UserRepository {
User findById(int id);
void save(User user);
}
In verschiedenen Situationen können wir diese Schnittstelle unterschiedlich implementieren:
- DatabaseUserRepository – speichert Benutzer in einer Datenbank.
- InMemoryUserRepository – speichert Benutzer im Speicher (praktisch für Tests).
- FileUserRepository – speichert Benutzer in einer Datei.
Der Code, der mit Benutzern arbeitet, hängt nur von der Schnittstelle ab:
public class UserService {
private final UserRepository userRepository;
// Dependency Injection über den Konstruktor
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void registerUser(User user) {
userRepository.save(user);
}
}
Jetzt können wir die Implementierung von UserRepository leicht austauschen, ohne den Service-Code zu ändern.
2. Dependency Injection (Abhängigkeitsinjektion) und die Rolle von Schnittstellen
Dependency Injection (DI, Abhängigkeitsinjektion) ist ein architektonischer Ansatz, bei dem Abhängigkeiten (z. B. Implementierungen von Schnittstellen) einem Objekt von außen übergeben werden, meist via Konstruktor oder Setter. So entstehen flexible, leicht testbare und erweiterbare Anwendungen.
Warum sind Schnittstellen für DI wichtig?
Wenn wir eine Implementierung hart im Code verdrahten würden, wäre ihr Austausch schwierig. Mit Schnittstellen können wir jede beliebige Implementierung einsetzen, ohne den Produktionscode zu ändern.
Beispiel für Dependency Injection
public interface NotificationSender {
void send(String message);
}
public class EmailNotificationSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Sende E-Mail: " + message);
}
}
public class SmsNotificationSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Sende SMS: " + message);
}
}
// Klasse, die NotificationSender verwendet
public class NotificationService {
private final NotificationSender sender;
public NotificationService(NotificationSender sender) {
this.sender = sender;
}
public void notifyUser(String message) {
sender.send(message);
}
}
Jetzt können Sie den NotificationService leicht testen, indem Sie ihm z. B. einen „Stub“ anstelle eines echten Nachrichtensenders übergeben.
3. Entwurfsmuster und Schnittstellen
Schnittstellen betreffen nicht nur die Architektur, sondern auch Entwurfsmuster. Viele Muster lassen sich ohne Schnittstellen nicht sinnvoll umsetzen. Schauen wir uns die populärsten an.
Observer (Beobachter)
Observer ist ein Muster, das es einem Objekt (Subjekt) ermöglicht, andere Objekte (Beobachter) über Änderungen seines Zustands zu informieren.
UML-Diagramm (vereinfacht):
+------------------+ +------------------------+
| Subject |<------->| Observer |
+------------------+ +------------------------+
| +addObserver() | | +update() |
| +removeObserver()| +------------------------+
| +notifyObservers()|
+------------------+
Beispielcode:
import java.util.ArrayList;
import java.util.List;
// Beobachter-Schnittstelle
public interface Observer {
void update(String event);
}
// Subjekt-Schnittstelle
public interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String event);
}
// Subjekt-Implementierung
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
}
// Beobachter-Implementierung
public class NewsReader implements Observer {
private String name;
public NewsReader(String name) {
this.name = name;
}
@Override
public void update(String event) {
System.out.println(name + " hat eine Nachricht erhalten: " + event);
}
}
// Hauptklasse zum Starten des Beispiels
public class ObserverExample {
public static void main(String[] args) {
// Wir erzeugen eine „Nachrichtenagentur“ (Subjekt)
NewsAgency agency = new NewsAgency();
// Wir erzeugen Beobachter
Observer alice = new NewsReader("Alice");
Observer bob = new NewsReader("Bob");
// Beobachter abonnieren die Nachrichten
agency.addObserver(alice);
agency.addObserver(bob);
// Senden einer Nachricht
agency.notifyObservers("Neue Java-Version veröffentlicht!");
// Einen Beobachter entfernen und noch eine Nachricht senden
agency.removeObserver(bob);
agency.notifyObservers("Nächste Nachricht für Abonnenten");
}
}
Ausgabe:
Alice hat eine Nachricht erhalten: Neue Java-Version veröffentlicht!
Bob hat eine Nachricht erhalten: Neue Java-Version veröffentlicht!
Strategy (Strategie)
Strategy ist ein Muster, das es ermöglicht, den Algorithmus zur Laufzeit auszuwählen, ohne den Client-Code zu ändern.
UML-Diagramm (vereinfacht):
+------------------+
| Context |
+------------------+
| -strategy: Strat.|
| +setStrategy() |
| +execute() |
+------------------+
|
v
+------------------+
| Strategy |<-------------------------+
+------------------+ |
| +execute() | |
+------------------+ |
^ |
| |
+------------------+ +------------------+ |
| ConcreteA | | ConcreteB |---+
+------------------+ +------------------+
| +execute() | | +execute() |
+------------------+ +------------------+
Beispielcode:
public interface PaymentStrategy {
void pay(int amount);
}
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Zahlung " + amount + " RUB per Kreditkarte");
}
}
public class PaypalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Zahlung " + amount + " RUB per PayPal");
}
}
public class OnlineStore {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Verwendung:
OnlineStore store = new OnlineStore();
store.setPaymentStrategy(new CreditCardPayment());
store.checkout(1000);
store.setPaymentStrategy(new PaypalPayment());
store.checkout(500);
Ausgabe:
Zahlung 1000 RUB per Kreditkarte
Zahlung 500 RUB per PayPal
Command (Befehl)
Command ist ein Muster, das eine Anforderung als Objekt kapselt und es ermöglicht, Aktionen als Parameter zu übergeben.
Beispielcode:
public interface Command {
void execute();
}
public class LightOnCommand implements Command {
@Override
public void execute() {
System.out.println("Licht eingeschaltet!");
}
}
public class LightOffCommand implements Command {
@Override
public void execute() {
System.out.println("Licht ausgeschaltet!");
}
}
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
// Verwendung:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand());
remote.pressButton(); // Licht eingeschaltet!
remote.setCommand(new LightOffCommand());
remote.pressButton(); // Licht ausgeschaltet!
4. Vorteile der Verwendung von Schnittstellen in der Architektur
- Lose Kopplung (Low Coupling). Der Code hängt nur von der Schnittstelle ab, nicht von einer konkreten Implementierung. Das erleichtert Austausch, Tests und Erweiterung.
- Testbarkeit. Produktionsimplementierungen lassen sich beim Schreiben von Unit-Tests leicht durch Mocks/Stubs ersetzen.
- Erweiterbarkeit. Neue Implementierungen der Schnittstelle können hinzugefügt werden, ohne bestehenden Code zu ändern – das Open-Closed-Prinzip (OCP).
- Parallele Entwicklung. Mehrere Teams können unabhängig verschiedene Teile des Systems implementieren, solange sie eine gemeinsame Schnittstelle haben.
- Architekturelle Flexibilität. Neue Muster und Ansätze lassen sich leicht einführen.
5. Typische Fehler beim Einsatz von Schnittstellen in der Architektur
Fehler Nr. 1: Starre Bindung an die Implementierung.
Wenn Sie überall konkrete Klassen statt Schnittstellen verwenden, erfordert jede Änderung der Implementierung Anpassungen an vielen Stellen. Programmieren Sie nach Möglichkeit immer „auf Schnittstellenebene“.
Fehler Nr. 2: Zu große Schnittstellen (God Interface).
Eine Schnittstelle sollte kompakt sein und nur einen Verantwortungsbereich abdecken. Stopfen Sie nicht alles in eine Schnittstelle – sonst wird die Implementierung schwergewichtig und unübersichtlich.
Fehler Nr. 3: Ignorieren der Vorteile der Testbarkeit.
Wenn Sie Schnittstellen nicht zum Austausch von Abhängigkeiten in Tests nutzen, können Ihre Tests langsam und unzuverlässig werden, insbesondere wenn sie mit echten Datenbanken oder Netzwerken arbeiten.
Fehler Nr. 4: Mehrere Implementierungen, aber kein DI.
Wenn Sie mehrere Implementierungen einer Schnittstelle haben, aber eine davon im Code hart verdrahten, verlieren Sie die gesamte architektonische Flexibilität. Nutzen Sie Dependency Injection (DI)!
GO TO FULL VERSION