API Design

Ich möchte hier meine Erfahrungen und Erkenntnisse zum Thema API Design zusammenfassen. Dieses Thema hat soviele Facetten und Stolperfallen, die einem später manchmal sehr teuer zu stehen kommen können. Es ist wichtig, möglichst viel in die Überlegungen beim API-Bau einzubeziehen.

Was verstehe ich unter API?

Ein API ist ein Programmierschnittstelle für die Verwendung einer Komponente von "Aussen". Heute werden Applikationen in Module unterteilt und man definiert üblicherweise APIs bei einem Modul für die Kommunikation mit anderen Modulen. Aber auch innerhalb von Modulen finden sich viele API, die die einzelnen Applikationsteile von einander abtrennen ("teile und herrsche").

Wieso ist es wichtig ein gutes API zu bauen?

Dafür gibt es massenweise Gründe die verschiedene Personenkreise betreffen:

1. Entwickler in anderen Teams verwenden unser API und binden sich damit an unseren Vorschlag ein Problem zu lösen.

2. Entwickler im eigenen Team sind durch das API schlimmstenfalls an gewisse Strukturen und Lösungsansätze gebunden. Weiterentwicklung kann behindert werden, das Schreiben von Testklassen kann schwierig werden und die Innovationszyklen werden länger.

Dies hat natürlich direkten Einfluss auf die Attraktivität der Gesamtlösung. Je schneller und zuverlässiger Anpassungen gemacht werden können, umso besser ist das API.

Was macht ein API zu einem guten API?

Damit ein API gut ist, muss es folgende Kriterien erfüllen:

  1. es ist leicht zu verstehen und zu verwenden
  2. es ist konsistent
  3. es hat einen klar definierten Verwendungszweck
  4. es "zwingt" den Entwickler es richtig zu verwenden
  5. es versteckt Internas
  6. es ist gut dokumentiert

Wie stelle ich sicher, dass das API leicht zu verstehen ist?
Grundsätzlich macht es Sinn, sich wo möglich an APIs von JavaSE zu orientieren. Die kennt Jeder und wenn man sein API so strukturiert, wie es von Java bekannt ist, finden sich Entwickler schneller damit zurecht. Wichtig ist hier die Erkenntnis für wen es einfach zu verstehen sein soll. Kunde eines API ist immer der Entwickler, der mit dem API arbeiten wird. Ich muss mir also überlegen, was will er von diesem Modul, in welchem Kontext bewegt er sich und welche einzelnen Arbeitsschritte sind notwendig.

Da ich nicht immer einen solchen Entwickler zur Hand habe, muss ich mir selbst dessen Hut aufsetzen und den Code schreiben, der notwendig ist, mein API zu verwenden. Dieses Vorgehen führt quasi automatisch zu TDD (test driven design) bzw. BDD (behavior driven design). Ein weiterer wichtiger Aspekt sind die verwendeten Begriffe: wie heissen die Klassen, wie die Methoden und die Parameter? Eine präzise Wahl der Namen kann das API klar und verständlich machen. Wenn es schwierig ist einen Namen für etwas zu finden, dann ist üblicherweise konzeptionell der Wurm drin.

Da alle Klassen, Interfaces und Methoden im JDK englische Namen haben und sich Jeder an diese Namen gewöhnt hat, ist es absolut sinnvoll die eigene API auch komplett auf englisch zu entwickeln und zu benamsen. Wenn man unsicher ist, wie etwas auf englisch heisst, macht es sicher sein einen muttersprachlichen Entwickler zu fragen.

Was macht ein API Konsistenz und wieso ist das wichtig?
Der Programmierer, der mein API verwendet, tut dies um Zeit zu sparen. Er möchte sich auf seine Aufgabe konzentrieren und verwendet deshalb meinen Code, um nicht abgelenkt zu werden. Wenn sich nun mein API an unterschiedlichen Stellen unterschiedlich verhält oder ich verschiedene Namen für die gleichen Dinge verwende, verwirre ich den Kollegen und er wird wahrscheinlich aufgeben mein API zu verwenden.
Also: wichtig ist, dass alle Methoden, die mein API nach aussen zur Verfügung stellt eine ähnliche "Flughöhe" haben und, dass ich gleichen Dingen den gleichen Namen gebe. Ausserdem sollte ich darauf achten, dass ich möglichst wenig Ballast mitbringen, So ist es beispielsweise hinderlich, wenn ich eine lange Liste von Abhängigkeiten (JAR's und/oder AppServer mit spezifischen Versionen) voraussetze, damit mein API funktioniert. Dies schränkt die Einsatzmöglichkeiten deutlich ein.

Wieso ist es wichtig den Verwendungszweck klar zu definieren und daran festzuhalten?
Mit einem klar definierten Funktionsumfang, weiss der Entwickler, was er erwarten kann und was nicht. Es wird immer Wünsche für Erweiterungen und Generalisierungen geben. Diesen nur nachzugeben, wenn es ins Gesamtbild des API passt, ist Aufgabe des API-Leaders. Im Zweifelsfall sollte auf zusätzliche Funktionen verzichtet werden. Funktionen, die sich später als fälschlicherweise hinzugefügt herausstellen, können nicht mehr entfernt werden, weil irgendjemand sie braucht.

Wie zwinge ich den Entwickler mein API richtig zu verwenden?
Diese Frage generell zu beantworten ist kaum möglich. Üblicherweise führt TDD dazu, dass ein API klare Vorgaben über dessen Verwendung macht. Damit dies allerdings gelingt, braucht es viel Disziplin beim Schreiben der Tests. Wenn man beim TDD über Unzulänglichkeiten des APIs stolpert reicht Jammern nicht. TDD kann zu einem kompletten Umbau des API und gegebenenfalls der dahinterliegenden Strukturen führen. Da muss man durch!

Ein wichtiger Aspekt in diesem Zusammenhang ist "den Benutzer des API nicht zu überraschen". Damit meine ich, dass das API das macht, was erwartet wird und dass alle Parameter sinnvoll Initialwerte haben (Convention-over-Configuration).

Wieso soll ich Internas verstecken und wie mache ich das?
Bei den Internas, die versteckt werden sollen, handelt es sich häufig um Datenstrukturen. Beispielsweise wird intern eine HashMap oder eine LinkedList verwendet, um einen Datensammlung zu verwalten. Der Entwickler, der mein API verwendet, soll es nicht wissen. Seine Erwartungen wie die Daten aufgebaut sind, sind wahrscheinlich ganz anders als die bestgeeignetste Struktur, die ich intern verwenden möchte. Und genau das muss das Ziel sein. Es schadet nichts, die Daten von einem Collection-Typ in einen anderen umzukopieren, wenn man dafür die Freiheit hat in einer zukünftigen Version intern alles auf den Kopf zu stellen.

Es sollten keine Klassen, die Serializable implementieren im API verwendet werden, da durch Serializable sämtliche Attribute (auch die privaten) nach aussen weitergegeben werden.

Ein weiteres, klassisches Thema sind Exceptions. Wenn ich hinter meinem API eine SQL-basierte DB verwende, sollte ich dies nicht dem User mitteilen, indem ich ihm SQLExceptions entgegen werfe.

Ausserdem können auch Strings zu Problemen in diesem Zusammenhang führen. Wenn ich in einem String Daten in einem bestimmten Format aufbereite, dann schaffe ich hiermit eine gewisse Abhängigkeit, denn Benutzer meines API werden den String auslesen und parsen. Damit breche ich ihren Code, wenn ich meinen String anders formatiere.

Ein sehr wichtiges Thema in diesem Zusammenhang sind Verantwortlichkeiten. Wenn also mein Modul für die Richtigkeit und Vollständigkeit von gewissen Daten verantwortlich ist, dann gebe ich eine unveränderbare Liste dieser Daten zurück. Am Besten ist es anstatt Klassen Interfaces zu empfangen und zurück zu geben.

Wieso sollte ich eine Dokumentation schreiben?
Nicht alle Funktionalitäten eines API gehen aus dem Interface hervor: Wann werden welche Fehler ausgelöst? Was wird zurückgegeben, wenn keine Daten vorhanden sind (null / leere Liste / Exception)? Der Entwickler, der unser API verwendet, soll solche Dinge direkt aus dem API (bzw. dessen Dokumentation) erfahren. Das macht unser API attraktiv. Ausserdem ist es wichtig, dass gute Verwendungsbeispiele verfügbar sind. Oft werden diese per Copy-Paste von anderen Entwicklern übernommen und auf die eigenen Bedürfnisse angepasst.