Arno Haase, Sven Efftinge:
API-Design
? Entwurf stabiler und beständiger Schnittstellen
API - Application Programming Interface
SPI - Service Provider Interface -
- Art von API zur Konfiguration,
- siehe Strategy-Pattern, Listener, Callback-Funktionen
- in Java häufig Interface
- Clients müssen SPI implementieren
Bestandteile eines Java-APIs
- statische Elemente (klar)
- aber auch dynamische Elemente: Aufrufreihenfolgen, wann wird welche Exception geworfen, Anforderungen an implementierenden Client-Code, nicthfunktionale Eigenschaften
API-Bestandteile sprachabhängig
von schwache Typisierung (Ruby) bis
Design by Contract (Eiffel)
Weitere Bestandteile
- Dateiendungen, -formate und inhalte
- Umgebungsvariablen
Veröffentlichte Schnittstelle
- Schnittstellen sind explizit: klare Dokumentation, öffentliche Member (gehören nicht unbedingt zur Schnittstelle, können aber technisch notwendig sein)
- Schnittstellen bleiben stabil, aber Implementierung darf geändert werden
- Trennung schafft Herausforderungen: keine Kontrolle über die Benutzer der Schnittstelle
- Je weiter die Trennung, desto schwieriger sind Änderungen
Was ist ein gutes API?
- klarer Scope
- schlank
- einfach zu benutzen
- lesbar
- intuitiv und überraschungsfrei
- komplett und redundanzfrei
Best Practices (Vorgehen)
Use Case-zentriert
Design aus Sicht des Clients
- Validierung der Abstraktionen
- welche Probleme soll die Bibliothek für Clients lösen
- Test-Driven Development (TDD)
- "Eat your own dog food"
Gegengewicht zu Abstraktionen aus Sicht
der Bibliothek
- beide Richtungen müssen "stimmig" sein
Klare Abstraktionen
gute Abstraktionen sind
- intuitiv
- einfach, aber nicht zu einfach
- passend zur Domäne
- redundanzfrei und dadurch wiederspruchsfrei
Domänenanalyse
- Use Cases/Szenarien durchspielen
- klarer Scope
Iterative Entwickung
Schnittstelle vor der Veröffentlichung
reifen lassen
- erst zwei, drei Systeme bauen, die die API nutzen
- "Three strikes and you refactor"
Explizite Releases
Anders als "normaler" Code
hat ein API zwingend klare Releases
- eingefroren
- Versionierung
Vorab-Tests
sind möglich
Mut zur Lücke
Es gibt typischerweise Druck, schnell
zu veröffentlichen
- Termindruck beim API-Entwurf und durch Kunden, die die Lösung brauchen
Best Practices (Design)
Explizites API
Trennung von API und Implementierung
- Klare Liste der "öffentlichen" Klassen und Interfaces
- Klare Beschreibung des "garantierten" Verhaltens, Abgrenzung von Implementierungsdetails
- erlaubte Verwendungsmuster (Reihenfolge, Threading, etc.)
- API muss vollständig sein
Einfaches API
Man kann APIs leicht erweitern, aber
schwer wieder Elemente entfernen
Einfach zu lernen
- möglichst wenige Konzepte
- möglichst wenige Aufrufe
Einfach
zu pflegen und zu erweitern
- Redundanzfreiheit
Gute
Defaults
- Configuration by Exception
- Programming by Convention
Facade
- einfache Facade für häufige Use Cases anbieten, um Komplexität optional zu machen
Convenience-Methoden
für häufig verwendete Aktionen
Checked Exceptions sinnvoll?
Einfachheit SPI
Ein Interface kann nicht erweitert werden!
Ein Interface sollte möglichst wenig
Methoden deklarieren
Trennung von API (nicht reduzierbar)
und SPI (nicht erweiterbar)
Anbieten einer abstrakten Defaultimplementierung
Überraschungsfreiheit
Unveränderliche Datentypen
Annahmen im Code explizit machen
- Sichtbarkeiten, Dokumentation
Gute
Name wählen
Fehlbedienung zur Laufzeit melden
- Contract-Verletzungen sofort abweisen
- klare Fehlermeldungen
API-Separierung
Trennung eines großen APIs in überschaubare
Teile mit klarem Fokus
Beispiele
- Swing (schlecht)
- SWT --> JFace --> Workbench (gut)
Best Practices (Implementierung)
Interfaces oder Klassen?
kein globaler Zustand
globaler Zustand verschleiert Abhängigkeiten
- z. B. Konfiguration in einem Singleton, das sich aus einer Datei initialisiert
- Initialisierung des globalen Zustands ist implizierte Vorbedingung für die Verwendung
- Infrastruktur-Anforderungen für Client-Tests
Unveränderlichkeit (Immutability)
Pro
- einfach
- threadsicher
- wiederverwendbar
Contra
- für jeden Wert ein neues Objekt
Methoden statt Feldern
Methoden kapseln Implementierungsdetails
- z. B. bei einfachen Parameterklassen ohne Interface
- Wertebereichsprüfung auf Contract-Konformität
- Namen und Typen der Felder
aber
nicht dogmatisch?
Factories
Factories kapseln Details der Erzeugung
- konkrete Typen
- Lifecycle
- Multiplizität - Reuse bei zustandslosen Services
Große Auswahl an Entwurfsmustern
Service-Repositories als Spezialfall
- DI im Stil von Spring
Verwendung explizit im Code
Java bietet Sprachmittel, um einen beabsichtigte
Verwendung im Quelltext explizit zu machen
- final
- private Konstruktoren
Listener
Listener erlauben die Erweiterung eines
API an wohldefinierten Stellen
- use case-getriebene Auswahl der Stellen
- clientspezifische Funktionaltät
Klarer
Contract für Listener
- u. U. andes als "normale" API-Verwendung
- evtl. Einschränkung der erlaubten Aufrufe
- Thread-Modell
API-Evolution: Problemstellung
APIs behindern die Evolutions der Bibliothek
Anforderungen ändern sich
Lebenszyklus von Schnittstellen
viergeteilter Lebenszyklus
- Draft
- Published
- Deprecated
- Retired
SPI-Evoution
Möglichkeiten
- neue Interfaces
- mit Vererbungshierarchie
- Refactoring mit API-Bruch
Neues Interface für Änderung
- bei jeder Änderung wird ein Interface hinzugefügt
Auswirkungen
- Explosion!
Framework-Code
Typ abfragen und casten
Vererbungshierarchie von Interfaces
neue Interfaces erweitern die alten
Refactoring mit API-Bruch
aufräumen!