Heim  >  Artikel  >  Java  >  API-Designpraktiken für Java

API-Designpraktiken für Java

WBOY
WBOYOriginal
2024-08-30 06:02:02329Durchsuche

API design practices for Java

Von: BJ Hargrave

Verstehen Sie einige der API-Entwurfspraktiken, die beim Entwerfen einer Java-API angewendet werden sollten. Diese Vorgehensweisen sind im Allgemeinen nützlich und stellen sicher, dass die API in einer modularen Umgebung wie OSGi und dem Java Platform Module System (JPMS) ordnungsgemäß verwendet werden kann. Einige der Praktiken sind präskriptiv, andere proskriptiv. Und natürlich gelten auch andere gute API-Designpraktiken.

Die OSGi-Umgebung bietet eine modulare Laufzeit, die das Java-Klassenlader-Konzept verwendet, um Typ-Sichtbarkeit-Kapselung zu erzwingen. Jedes Modul verfügt über einen eigenen Klassenlader, der mit den Klassenladern anderer Module verkabelt ist, um exportierte Pakete gemeinsam zu nutzen und importierte Pakete zu nutzen.

JPMS, eingeführt in Java 9, bietet eine modulare Plattform, die das Zugriffskontrollkonzept der Java Language Specification nutzt, um Typ-Zugänglichkeit-Kapselung zu erzwingen. Jedes Modul definiert, welche Pakete exportiert werden und somit für andere Module zugänglich sind. Standardmäßig befinden sich die Module in einer JMPS-Ebene alle im selben Klassenlader.

Ein Paket kann eine API enthalten. Es gibt zwei Rollen von Clients für diese API-Pakete: API-Konsumenten und API-Anbieter. API-Konsumenten verwenden die API, die von einem API-Anbieter implementiert wird.

In den folgenden Entwurfspraktiken diskutieren wir die öffentlichen Teile eines Pakets. Die Mitglieder und Typen eines Pakets, die nicht öffentlich oder geschützt (d. h. privat oder standardmäßig zugänglich) sind, sind außerhalb des Pakets nicht zugänglich und stellen somit Implementierungsdetails des Pakets dar.

Java-Pakete müssen eine zusammenhängende, stabile Einheit sein

Ein Java-Paket muss so gestaltet sein, dass es eine kohäsive und stabile Einheit ist. In modularem Java ist das Paket die gemeinsame Einheit zwischen Modulen. Ein Modul kann ein Paket exportieren, sodass andere Module das Paket verwenden können. Da das Paket die gemeinsame Einheit zwischen Modulen ist, muss ein Paket insofern zusammenhängend sein, als alle Typen im Paket mit dem spezifischen Zweck des Pakets in Zusammenhang stehen müssen. Von Grab-Bag-Paketen wie java.util wird abgeraten, da die Typen in einem solchen Paket oft keine Beziehung zueinander haben. Solche nicht zusammenhängenden Pakete können zu vielen Abhängigkeiten führen, da die nicht zusammenhängenden Teile des Pakets auf andere nicht verwandte Pakete verweisen und Änderungen an einem Aspekt des Pakets Auswirkungen auf alle Module haben, die von dem Paket abhängen, auch wenn ein Modul den Teil des Pakets möglicherweise nicht tatsächlich verwendet Paket, das geändert wurde.

Da es sich bei dem Paket um das von der Einheit geteilte Paket handelt, muss sein Inhalt bekannt sein und die enthaltene API kann nur auf kompatible Weise geändert werden, wenn das Paket in zukünftigen Versionen weiterentwickelt wird. Das bedeutet, dass ein Paket keine API-Obermengen oder -Untermengen unterstützen darf; Betrachten Sie beispielsweise javax.transaction als ein Paket, dessen Inhalt instabil ist. Der Benutzer eines Pakets muss wissen können, welche Typen im Paket verfügbar sind. Dies bedeutet auch, dass Pakete von einer einzelnen Entität (z. B. einem Glas
) geliefert werden sollten Datei) und nicht auf mehrere Entitäten aufgeteilt, da der Benutzer des Pakets wissen muss, dass das gesamte Paket vorhanden ist.

Außerdem muss sich das Paket in zukünftigen Versionen auf kompatible Weise weiterentwickeln. Daher sollte ein Paket versioniert sein und seine Versionsnummer muss sich gemäß den Regeln der semantischen Versionierung weiterentwickeln. Es gibt auch ein OSGi-Whitepaper zur semantischen Versionierung.

Allerdings sind die semantischen Versionierungsempfehlungen für größere Versionsänderungen für Pakete problematisch. Die Paketentwicklung muss eine Funktionserweiterung sein. Bei der semantischen Versionierung bedeutet dies die Erhöhung der Nebenversion. Wenn Sie eine Funktion entfernen, führt dies zu einer inkompatiblen Änderung am Paket, anstatt die Hauptänderung zu erhöhen
Version müssen Sie auf einen neuen Paketnamen umstellen, wobei das Originalpaket weiterhin kompatibel bleibt. Um zu verstehen, warum dies wichtig und notwendig ist, lesen Sie diesen Artikel über semantische Importversionierung für Go und diese hervorragende Keynote-Präsentation von Rich Hickey auf der Clojure/conj 2016. Beide sprechen dafür, auf einen neuen Paketnamen umzusteigen, anstatt den Hauptpaketnamen zu ändern Version, wenn Sie inkompatible Änderungen an einem Paket vornehmen.

Paketkopplung minimieren

Die Typen in einem Paket können auf die Typen in anderen Paketen verweisen. Zum Beispiel die Parametertypen und der Rückgabetyp einer Methode sowie der Typ eines Feldes. Durch diese Kopplung zwischen Paketen entstehen sogenannte uses-Einschränkungen für das Paket. Das bedeutet, dass ein API-Verbraucher dieselben referenzierten Pakete wie der API-Anbieter verwenden muss, damit beide die referenzierten Typen verstehen.

Im Allgemeinen möchten wir diese Paketkopplung minimieren, um die Nutzungsbeschränkungen für ein Paket zu minimieren. Dies vereinfacht die Verkabelungsauflösung in der OSGi-Umgebung und minimiert das Abhängigkeits-Fanout, was die Bereitstellung vereinfacht.

Schnittstellen werden gegenüber Klassen bevorzugt

Für eine API werden Schnittstellen gegenüber Klassen bevorzugt. Dies ist eine recht gängige API-Designpraxis, die auch für modulares Java wichtig ist. Die Verwendung von Schnittstellen ermöglicht Implementierungsfreiheit sowie Mehrfachimplementierungen. Schnittstellen sind wichtig, um den API-Konsumenten vom API-Anbieter zu entkoppeln. Dadurch kann ein Paket, das die API-Schnittstellen enthält, sowohl vom API-Anbieter, der die Schnittstellen implementiert, als auch vom API-Konsumenten, der Methoden auf den Schnittstellen aufruft, verwendet werden. Auf diese Weise haben API-Konsumenten keine direkten Abhängigkeiten von einem API-Anbieter. Beide hängen nur vom API-Paket ab.

Abstrakte Klassen sind manchmal eine gültige Designwahl anstelle von Schnittstellen, aber im Allgemeinen sind Schnittstellen die erste Wahl, insbesondere da Standardmethoden zu einer Schnittstelle hinzugefügt werden können.

Schließlich benötigt eine API häufig eine Reihe kleiner konkreter Klassen wie Ereignistypen und Ausnahmetypen. Das ist in Ordnung, aber die Typen sollten im Allgemeinen unveränderlich sein und nicht für die Unterklassenbildung durch API-Konsumenten gedacht sein.

Vermeiden Sie statische Aufladung

Statik sollte in einer API vermieden werden. Typen sollten keine statischen Mitglieder haben. Statische Fabriken sollten vermieden werden. Die Instanzerstellung sollte von der API entkoppelt sein. Beispielsweise sollten API-Konsumenten Objektinstanzen von API-Typen durch Abhängigkeitsinjektion oder eine Objektregistrierung wie die OSGi-Dienstregistrierung oder den java.util.ServiceLoader in JPMS erhalten.

Die Vermeidung von Statik ist auch eine gute Vorgehensweise, um eine testbare API zu erstellen, da Statik nicht einfach verspottet werden kann.

Singletons

Manchmal gibt es Singleton-Objekte in einem API-Design. Der Zugriff auf das Singleton-Objekt sollte jedoch nicht über statische Elemente wie eine statische getInstance-Methode oder ein statisches Feld erfolgen. Wenn ein Singleton-Objekt erforderlich ist, sollte das Objekt von der API als Singleton definiert und den API-Konsumenten durch Abhängigkeitsinjektion oder eine Objektregistrierung wie oben erwähnt bereitgestellt werden.

Vermeiden Sie Klassenlader-Annahmen

APIs verfügen oft über Erweiterbarkeitsmechanismen, bei denen der API-Konsumer den Namen einer Klasse angeben kann, die der API-Anbieter laden muss. Der API-Anbieter muss dann Class.forName verwenden (möglicherweise mithilfe des Thread-Kontext-Klassenladers), um die Klasse zu laden. Diese Art von Mechanismus setzt Klassensichtbarkeit vom API-Anbieter (oder Thread-Kontext-Klassenlader) bis zum API-Konsumenten voraus. API-Designs müssen Klassenlader-Annahmen vermeiden. Einer der Hauptpunkte der Modularität ist die Typkapselung. Ein Modul (z. B. API-Anbieter) darf keine Sichtbarkeit/Zugriff auf die Implementierungsdetails eines anderen Moduls (z. B. API-Konsumenten) haben.

API-Designs müssen die Weitergabe von Klassennamen zwischen dem API-Konsumenten und dem API-Anbieter vermeiden und müssen Annahmen hinsichtlich der Klassenlader-Hierarchie und der Typsichtbarkeit/-zugänglichkeit vermeiden. Um ein Erweiterbarkeitsmodell bereitzustellen, sollte ein API-Design dafür sorgen, dass der API-Verbraucher Klassenobjekte oder noch besser Instanzobjekte an den API-Anbieter übergibt. Dies kann über eine Methode in der API oder über eine Objektregistrierung wie die OSGi-Dienstregistrierung erfolgen. Sehen Sie sich das Whiteboard-Muster an.

Wenn die Klasse java.util.ServiceLoader nicht in JPMS-Modulen verwendet wird, unterliegt sie auch den Annahmen des Klassenladers, da sie davon ausgeht, dass alle Anbieter vom Thread-Kontext-Klassenlader oder dem bereitgestellten Klassenlader aus sichtbar sind. Diese Annahme trifft in einer modularen Umgebung im Allgemeinen nicht zu, obwohl JPMS die Moduldeklaration ermöglicht, um zu deklarieren, dass Module ein
bereitstellen oder verwenden Von ServiceLoader verwalteter Dienst.

Gehen Sie nicht von Dauerhaftigkeit aus

Viele API-Designs gehen nur von einer Konstruktionsphase aus, in der Objekte instanziiert und zur API hinzugefügt werden, ignorieren jedoch die Zerstörungsphase, die in einem dynamischen System auftreten kann. API-Designs sollten berücksichtigen, dass Objekte kommen und gehen können. Beispielsweise ermöglichen die meisten Listener-APIs das Hinzufügen und Entfernen von Listenern. Viele API-Designs gehen jedoch nur davon aus, dass Objekte hinzugefügt und niemals entfernt werden. Beispielsweise verfügen viele Abhängigkeitsinjektionssysteme nicht über die Möglichkeit, ein injiziertes Objekt zurückzuziehen.

In einer OSGi-Umgebung können Module hinzugefügt und entfernt werden, daher ist ein API-Design wichtig, das diese Dynamik berücksichtigen kann. Die OSGi Declarative Services-Spezifikation
definiert ein Abhängigkeitsinjektionsmodell für OSGi, das diese Dynamik unterstützt, einschließlich des Rückzugs injizierter Objekte.

Dokumentieren Sie klar die Typrollen für API-Konsumenten und API-Anbieter

Wie in der Einleitung erwähnt, gibt es zwei Rollen für Clients eines API-Pakets: API-Konsumenten und API-Anbieter. API-Konsumenten nutzen die API und API-Anbieter implementieren die API. Für die Schnittstellentypen (und abstrakten Klassentypen) in einer API ist es wichtig, dass im API-Design klar dokumentiert wird, welche dieser Typen nur von API-Anbietern implementiert werden dürfen und welche Typen von API-Konsumenten implementiert werden können. Beispielsweise werden Listener-Schnittstellen im Allgemeinen von API-Konsumenten implementiert
und Instanzen, die an API-Anbieter übergeben werden.

API-Anbieter reagieren empfindlich auf Änderungen der Typen, die sowohl von API-Konsumenten als auch von API-Anbietern implementiert werden. Der Anbieter muss alle neuen Änderungen an den API-Anbietertypen implementieren und alle neuen Änderungen an den API-Konsumententypen verstehen und wahrscheinlich umsetzen. Ein API-Verbraucher kann im Allgemeinen (kompatible) Änderungen am API-Anbietertyp ignorieren, es sei denn, der API-Verbraucher möchte eine Änderung vornehmen, um die neue Funktion aufzurufen. Ein API-Verbraucher reagiert jedoch empfindlich auf Änderungen der API-Verbrauchertypen und muss wahrscheinlich geändert werden, um die neue Funktion zu implementieren. Beispielsweise wird im Paket javax.servlet der Typ ServletContext von API-Anbietern wie einem Servlet-Container implementiert. Das Hinzufügen einer neuen Methode zu ServletContext erfordert eine Aktualisierung aller API-Anbieter, um die neue Methode zu implementieren. API-Konsumenten müssen jedoch keine Änderungen vornehmen, es sei denn, sie möchten die neue Methode aufrufen. Der Servlet-Typ wird jedoch von API-Konsumenten implementiert. Wenn Sie Servlet eine neue Methode hinzufügen, müssen alle API-Konsumenten geändert werden, um die neue Methode zu implementieren, und auch alle API-Anbieter müssen geändert werden, um die neue Methode zu verwenden. Somit hat der ServletContext-Typ eine API-Provider-Rolle und der Servlet-Typ eine API-Consumer-Rolle.

Da es im Allgemeinen viele API-Konsumenten und wenige API-Anbieter gibt, muss die API-Entwicklung bei der Prüfung von Änderungen an API-Konsumententypen sehr vorsichtig sein und gleichzeitig entspannter mit der Änderung von API-Anbietertypen umgehen. Dies liegt daran, dass Sie die wenigen API-Anbieter ändern müssen, um eine aktualisierte API zu unterstützen, aber Sie möchten nicht, dass sich die vielen vorhandenen API-Konsumenten ändern, wenn eine API aktualisiert wird. API-Konsumenten sollten nur dann wechseln müssen, wenn der API-Konsumenten die Vorteile einer neuen API nutzen möchte.

Die OSGi Alliance definiert Dokumentationsanmerkungen, ProviderType und ConsumerType, um die Rollen von Typen in einem API-Paket zu markieren. Diese Anmerkungen stehen im osgi.annotation-JAR zur Verwendung in Ihrer API zur Verfügung.

Abschluss

Berücksichtigen Sie beim nächsten Entwurf einer API bitte diese API-Entwurfspraktiken. Ihre API ist dann sowohl in modularen Java- als auch in nicht-modularen Java-Umgebungen nutzbar.

Das obige ist der detaillierte Inhalt vonAPI-Designpraktiken für Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn