Читать книгу API-Design - Kai Spichale - Страница 47

Оглавление

4Ausprägungen

Java-APIs sind sehr vielfältig, sodass man sie auf verschiedenen Ebenen diskutieren kann. Das Spektrum reicht von den APIs einzelner Klassen und Komponenten bis zu den APIs ganzer Frameworks. Zwischen den folgenden Ausprägungen kann man unterscheiden:

 Implizite Objekt-API

 Utility-Bibliothek

 Service

 Framework

4.1Implizite Objekt-API

Jedes Objekt hat eine extern sichtbare Schnittstelle, die im Wesentlichen nicht private Methoden und Konstruktoren umfasst. Diese Schnittstelle bildet per Definition die API des Objektes. Objekte sind untereinander verbunden, sodass APIs typischerweise nicht nur auf Basis einzelner Klassen, sondern auf Basis mehrerer miteinander verbundener Klassen zu betrachten sind. Nichtsdestotrotz ist API-Design auf dieser Ebene eng verbunden mit allgemeineren Konzepten wie dem Geheimnisprinzip, Datenkapselung und der Trennung von Zuständigkeiten:

Geheimnisprinzip (information hiding)

 Das Geheimnisprinzip wurde bereits 1972 von David Parnas in seinem Aufsatz »On the Criteria To Be Used in Decomposing Systems Into Modules« [Parnas 1972] eingeführt. Als Kriterium zur Modularisierung schlug Parnas das Kapseln von Implementierungsentscheidungen vor. Diese Entscheidungen sollen hinter wohldefinierten Schnittstellen verborgen bleiben, sodass diese geändert werden können, ohne dass alle abhängigen Module angepasst werden müssen. Das Geheimnisprinzip verbessert die Wartbarkeit von Software und macht sie stabiler. Module anderer Entwickler können wiederverwendet werden, ohne Details über deren Implementierung kennen zu müssen. Für eine korrekte Benutzung reicht es, zu wissen, welche Funktionen das Modul bietet und wie diese über die Schnittstelle des Moduls genutzt werden können.

Datenkapselung (encapsulation)

 Geheimnisprinzip und Datenkapselung stehen im engen Zusammenhang und werden häufig synonym verwendet. Trotzdem handelt es sich um unterschiedliche Softwaredesignprinzipien. Das Hauptanliegen von Kapselung ist das Ziehen einer Grenze um etwas, sodass man zwischen innen und außen unterscheiden kann. Eine objektorientierte Sprache wie Java bietet Objekte für diesen Zweck. Datenkapselung ist demzufolge das Feature einer Programmiersprache, mit dessen Hilfe das Geheimnisprinzip umgesetzt werden kann.

Trennung von Zuständigkeiten (separation of concerns)

 Die Trennung von Zuständigkeiten ist ein Softwaredesignprinzip zur Aufteilung der Software in unterscheidbare Sektionen, die jeweils eine Zuständigkeit haben. Diese Aufteilung unterstützt einen modularen Entwurf im Zusammenspiel mit Datenkapselung und dem Geheimnisprinzip. Vorteile dieser Aufteilung sind vereinfachte Entwicklung und Wartung, weil indivduelle Sektionen unabhängig voneinander wiederverwendet oder weiterentwickelt werden können.

APIs nicht nur für Komponenten- und Teamgrenzen

APIs werden häufig zur Integration von Komponenten eingesetzt. Nach dem Gesetz von Conway stimmen diese technischen Schnittstellen häufig mit Team- oder Organisationsgrenzen überein. APIs – insbesondere die auf der hier betrachteten Objektebene – sind jedoch auch innerhalb einer Komponente oder eines Teams zu finden, denn prinzipiell kann API-Design in der gesamten Codebasis eingesetzt werden, um den Code zu verbessern.

public class NoEncapsulationOrInformationHiding {

public static final int STATUS_ENABLED = 0;

public static final int STATUS_DISABLED = 1;

public int currentStatus = STATUS_ENABLED;

}

Dieses Beispiel zeigt, dass der Einsatz einer objektorientieren Sprache nicht automatisch Datenkapselung und das Geheimnisprinzip sicherstellen.

public class EncapsulationWithoutInformationHiding {

public static final int STATUS_ENABLED = 0;

public static final int STATUS_DISABLED = 1;

private int currentStatus = STATUS_ENABLED;

public int getStatus() {

return currentStatus;

}

}

Diese zweite Variante ist insofern besser, weil der interne Zustand des Objektes gekapselt wird. Kein Benutzer dieser Klasse ist in der Lage, unkontrolliert den Wert des Attributes currentStatus zu ändern. Das Geheimnisprinzip wird dennoch nicht befolgt, weil implementierungsspezifische Details über eine Zugriffsmethode (accessor method) exponiert werden.

public class EncapsulationAndInformationHiding {

private static final int STATUS_ENABLED = 0;

private static final int STATUS_DISABLED = 1;

private int currentStatus = STATUS_ENABLED;

private int getStatus() {

return currentStatus;

}

public boolean isEnabled() {

return getStatus() == STATUS_ENABLED;

}

}

Diese Variante nutzt Datenkapselung zur korrekten Umsetzung des Geheimnisprinzips. Benutzer dieser Klasse können den Status abfragen, ohne dass sie erfahren, wie dieser realisiert ist. Auch das Design dieser API hat sich verbessert, sodass die Klasse intuitiver benutzt werden kann.

API-Design fördert Clean Code

Wie das vorherige Beispiel zeigt, kann eine gute API das Schreiben von lesbarem und wartbarem Clientcode unterstützen. Clean Code [Martin 2008] – eine Sammlung verschiedener Maßnahmen zur Verbesserung der Verständlichkeit von Quellcode – verfolgt ähnliche Ziele. Das gleichnamige Buch von Robert C. Martin deckt viele Themen ab, die ebenfalls für gutes API-Design relevant sind:

 Auswahl aussagekräftiger Namen

 Entwurf von Klassen und Methoden

 Fehlerbehandlung

 Boundaries

Im Gegensatz zu Clean Code schaut API-Design jedoch nicht in das Innere von Klassen und Methoden. Deswegen enthält dieses Buch auch keine Empfehlung zur Formatierung von Quellcode, zur Benutzung von Instanzvariablen oder Schachtelungstiefe von If-Anweisungen.

4.2Utility-Bibliothek

Auch Utility-Bibliotheken haben eine API. Diese Bibliotheken sind häufig klein und haben keinen globalen Zustand. Globaler Zustand entsteht beispielsweise durch eine Datenbank, eine Konfiguration oder Initialisierung. Charakteristisch für diese APIs sind statische Methoden, wie man sie in Apache Commons Lang findet. Ein passendes Beispiel sind die statischen Methoden der Klasse StringUtils:

 chomp

 contains

 appendIfMissing

 equals

Auch Joda-Time, Guava und FEST sind Utility-Bibliotheken. Selbst das JDK kann als Beispiel genannt werden. Typischerweise bieten Utility-Bibliotheken konkrete Klassen zur Benutzung an.

4.3Service

Charakteristisch für APIs dieser Kategorie sind Fassaden und Datentransferobjekte zur Kapselung einer zugrunde liegenden Komponente, sodass man diese Kategorie auch »Component Boundary« nennen könnte. Die API eines Service kapselt das Klassenmodell der Komponente vollständig oder partiell. Bei einer partiellen Kapselung werden Elemente der internen Implementierung der Komponente in der API verwendet, sodass Benutzer ebenfalls davon abhängig werden.

Den überladenen Begriff »Service« finden man im Domain-Driven Design (DDD) nach Eric Evans [Evans 2004] als Bezeichnung für eine Operation, die nicht natürlich einem anderen Domänenobjekt zugeordnet werden kann und deswegen Teil eines Serviceobjektes ist. Diese Bedeutung von »Service« passt nicht zu dieser API-Kategorie, wenngleich die Platzierung von Methoden eine wichtige Aufgabe beim API-Design darstellt.

Domain-Driven Design, Application Services und Onion Architecture

Ein Application Service – ebenfalls ein Baustein im Domain-Driven Design nach Vaughn Vernon [Vernon 2013] – ist ein treffenderes Beispiel. Diese Services bieten Operationen für externe Benutzer und kapseln das zugrunde liegende Domänenmodell. Die Application Services sind ebenfalls ein Bestandteil der Zwiebelarchitektur (Onion Architecture) nach Jeffrey Palermo [Palermo 2008]. Die API eines Application Service dient als Einstiegspunkt in die Domäne und nutzt deren Begriffe und Objekte. Die API sollte nur unveränderliche Objekte oder Datentransferobjekte veröffentlichen, sodass Benutzer der API nicht Zugriff auf die darunterliegende Domäne erhalten und diese manipulieren können. Mit einem testgetriebenen Ansatz würde man mit einem funktionalen High-Level-Test auf Basis eines Serviceentwurfs beginnen und die fehlende Geschäftslogik in der Domäne implementieren, bis der Test erfolgreich durchläuft.


Abb. 4–1In der Onion Architecture bilden die Application Services die API-Schicht des Application Core.

Component Boundary und Vertical Slices

Nicht nur im API-Layer findet man Services, denn in einer komponentenbasierten Architektur kann die Applikation nicht nur in Schichten, sondern auch in vertikale Schnitte (vertical slices) eingeteilt werden, sodass kleinere Komponenten entstehen, die durch definierte Schnittstellen miteinander verbunden sind. In diesem Sinne unterstützt API-Design bei der Bildung einer modularen Codebasis.


Abb. 4–2Die Applikation ist in Schichten und vertikale Schnitte eingeteilt. Die entstandenen Komponenten sind durch wohldefinierte Schnittstellen integriert.

4.4Framework

Frameworks bieten wiederverwendbare Funktionen und Erweiterungspunkte in Form von Interfaces und abstrakten Klassen, die von Benutzern des Frameworks zu erweitern sind. Auch objektorientierte Ansätze wie Callback- und Template-Methoden sind charakteristisch für Frameworks. Im Gegensatz zu einem Service nutzt ein Framework häufig eine Umkehrung der Steuerung (inversion of control), bei der der Entwickler eine konkrete Implementierung registriert, die dann durch das Framework genutzt und gesteuert wird.

Frameworks können großen Einfluss auf die Architektur einer Applikation haben. Charakteristisch sind Callback-Methoden, Dependency Injection und Annotationen zur Steuerung des Lebenszyklus von Objekten. Beispiele sind die Beans des Spring Framework und die Enterprise JavaBeans (EJBs) des Java-EE-Standards. Auch das Framework JUnit prägt die Funktionsweise von Testfällen, denn Testfälle durchlaufen einen fest definierten Lebenszyklus. Mit speziellen Annotationen des Frameworks markierte Methoden können vor und nach der Testfallausführung aufgerufen werden.

4.5Eine Frage der Priorität

Die Frage lautet nicht, ob man API-Design anwendet, sondern wie viel.

Die obige Liste, die sich wahrscheinlich fortsetzen ließe, zeigt, in welchen unterschiedlichen Ausprägungen objektorientierte APIs diskutiert werden können. APIs sind allgegenwärtig und nicht ausschließlich ein Integrationsthema. Der Aufwand, der mit dem Erreichen der API-Qualitätsmerkmale verbunden ist, ist jedoch nicht immer gerechtfertigt. API-Design ist daher eine Frage der Priorität. Typischerweise investiert man mehr in das API-Design für Framework- und Komponentengrenzen als für eine nur für wenige Entwickler sichtbare interne Implementierung. Die Priorität sollte von der Sichtbarkeit der API und der beabsichtigten Arbeitsteilung abhängig gemacht werden.

4.6Zusammenfassung

 APIs sind allgegenwärtig, weil selbst einzelne Objekte eine implizite API besitzen.

 Das Design von APIs ist aufwendig und wird deswegen typischerweise nur intensiv an Komponenten- bzw. Teamgrenzen eingesetzt.

 APIs unterstützen das Geheimnisprinzip, Datenkapselung und die Trennung von Zuständigkeiten.

 APIs helfen, eine modulare Codebasis zu bilden.

Das folgende Kapitel geht auf eine Vielzahl unterschiedlicher Aspekte und Empfehlungen zum praktischen Design von Java-APIs ein.

API-Design

Подняться наверх