Vom Monolithen zu Microservices. Sam Newman
Implementierungskopplung ist meist die schädlichste Form der Kopplung, aber zum Glück lässt sie sich häufig am einfachsten verringern. Bei der Implementierungskopplung wird A mit B in Bezug darauf gekoppelt, wie B implementiert ist – ändert sich die Implementierung von B, ändert sich auch A.
Die Implementierungsdetails sind hier oft durch die Entwickler frei gewählt. Es gibt viele Wege, ein Problem zu lösen – wir entscheiden uns für einen, ändern aber eventuell später unsere Meinung. Wenn wir das tun, wollen wir die Anwender nicht stören (Sie erinnern sich: unabhängige Deploybarkeit).
Ein klassisches und häufig vorkommendes Beispiel der Implementierungskopplung ist das gemeinsame Verwenden einer Datenbank. In Abbildung 1-9 enthält unser Order-Service einen Datensatz mit allen Bestellungen des Systems. Der Recommendation-Service schlägt unseren Kunden abhängig von den bisherigen Bestellungen Artikel vor, die ihnen vielleicht gefallen könnten. Aktuell greift der Recommendation-Service auf die Daten direkt in der Datenbank zu.
Abbildung 1-9: Der Recommendation-Service greift direkt auf die Daten zu, die im Order-Service gespeichert sind.
Recommendations benötigen Informationen über die bisherigen Bestellungen. Bis zu einem gewissen Grad ist das eine unvermeidbare Domänenkopplung, auf die wir gleich eingehen werden. Aber in dieser speziellen Situation sind wir an ein spezifisches Schema, einen SQL-Dialekt und vielleicht sogar an den Inhalt der Zeilen gekoppelt. Ändert der Order-Service den Namen einer Spalte, teilt die Tabelle »Customer Order« auf oder tut sonst irgendetwas, enthält er konzeptionell immer noch Order-Informationen, aber wir verhindern, dass der Recommendation-Service diese Informationen abrufen kann. Besser ist es, dieses Implementierungsdetail zu verbergen (siehe Abbildung 1-10) – jetzt greift der Recommendation-Service auf die benötigten Informationen über einen API-Aufruf zu.
Abbildung 1-10: Der Recommendation-Service greift auf die Order-Informationen nun über eine API zu, die die internen Implementierungsdetails verbirgt.
Wir könnten den Order-Service auch als ein Dataset in Form einer Datenbank veröffentlichen lassen, was für den Bulk-Zugriff von Konsumenten gedacht wäre (siehe Abbildung 1-11). Solange der Order-Service die Daten passend veröffentlichen kann, sind jegliche Änderungen innerhalb des Order-Service für Konsumenten unsichtbar, da der öffentliche Vertrag eingehalten wird. Das ermöglicht auch ein Verbessern des Datenmodells, das für die Konsumenten genutzt wird, um es an ihre Bedürfnisse anzupassen. Wir werden solche Muster detaillierter in Kapitel 3 und 4 behandeln.
In beiden Varianten kommt Information Hiding zum Einsatz. Das Verbergen einer Datenbank hinter einer wohldefinierten Serviceschnittstelle erlaubt dem Service, den Scope dessen zu beschränken, was preisgegeben wird, und wir können dadurch die Repräsentation der Daten anpassen.
Abbildung 1-11: Der Recommendation-Service greift auf die Order-Informationen nun über eine bereitgestellte Datenbank zu, die anders als die interne Datenbank strukturiert ist.
Ein weiterer nützlicher Trick ist bei der Definition einer Serviceschnittstelle ein »Outside-in«-Denken – entwerfen Sie die Serviceschnittstelle aus Sicht des Servicekonsumenten, dann überlegen Sie sich, wie Sie diesen Servicevertrag implementieren. Der alternative Ansatz (dem ich leider allzu häufig begegnet bin) geht genau umgekehrt vor. Das Team, das an dem Service arbeitet, nimmt ein Datenmodell oder ein anderes internes Implementierungsdetail, und dann überlegt es sich, dieses nach außen bereitzustellen.
Beim »Outside-in«-Denken fragen Sie sie stattdessen zuerst: »Was brauchen meine Servicekonsumenten?« Und ich meine nicht, dass Sie sich selbst fragen, was Ihre Konsumenten benötigen – Sie fragen tatsächlich diejenigen, die Ihren Service aufrufen werden!
|
Behandeln Sie die Serviceschnittstellen, die Ihr Microservice bereitstellt, wie eine Benutzeroberfläche. Nutzen Sie »Outside-in«-Denken, um das Schnittstellendesign zusammen mit denjenigen zu entwerfen, die Ihren Service verwenden werden. |
Stellen Sie sich Ihren Servicevertrag mit der Außenwelt wie eine Benutzeroberfläche vor. Beim Entwerfen einer Benutzeroberfläche fragen Sie die Anwender, was sie haben wollen, und iterieren zusammen mit ihnen über das Design. Sie sollten Ihren Servicevertrag auf die gleiche Art und Weise formen. Damit erhalten Sie nicht nur einen Service, der von Ihren Konsumenten leichter zu nutzen ist, es hilft auch dabei, eine gewisse Trennung zwischen dem externen Vertrag und der internen Implementierung herzustellen.
Zeitliche Kopplung
Zeitliche Kopplung ist vor allem ein Laufzeitthema, bei dem es allgemein um eine der zentralen Herausforderungen synchroner Aufrufe in einer verteilten Umgebung geht. Wann eine Nachricht geschickt und wie diese verarbeitet wird, ist zeitlich relevant, wir reden also von einer zeitlichen Kopplung. Das klingt ein bisschen seltsam, schauen wir uns daher das Beispiel in Abbildung 1-12 an.
Abbildung 1-12: Drei Services nutzen synchrone Aufrufe, um eine Operation durchzuführen – sie sind zeitlich gekoppelt.
Hier sehen wir einen synchronen HTTP-Aufruf von unserem Warehouse-Service zu einem Order-Service, um erforderliche Informationen über eine Bestellung abzurufen. Um den Request zu erfüllen, muss der Order-Service wiederum Informationen vom Customer-Service abrufen – ebenfalls über einen synchronen HTTP-Aufruf. Damit diese Gesamtoperation abgeschlossen werden kann, müssen Warehouse-, Order- und Customer-Service laufen und ansprechbar sein. Sie sind zeitlich gekoppelt.
Wir könnten dieses Problem auf verschiedenen Wegen lösen. So könnten wir über Caching nachdenken – würde der Order-Service die vom Customer-Service abzufragenden Informationen cachen, könnte eine zeitliche Kopplung zum Customer-Service in manchen Fällen vermieden werden. Wir könnten auch einen asynchronen Transportweg nutzen, um die Requests zu schicken, vielleicht so etwas wie einen Message Broker. Damit könnte eine Nachricht an einen folgenden Service geschickt werden, die dann verarbeitet wird, wenn der Folgeservice verfügbar ist.
Eine vollständige Behandlung dieser Art von Service-to-Service-Kommunikation geht über den Rahmen dieses Buchs hinaus, wird aber in Kapitel 4 von Building Microservices genauer besprochen.
Deployment-Kopplung
Stellen Sie sich einen einzelnen Prozess vor, der aus mehreren statisch verlinkten Modulen besteht. Es wird eine einzige Codezeile in einem der Module geändert, und wir wollen diese Änderung deployen. Dazu müssen wir den gesamten Monolithen deployen – auch die Module, die sich nicht geändert haben. Alles muss zusammen deployt werden, daher haben wir eine Deployment-Kopplung.
Deployment-Kopplung kann, wie in unserem Beispiel des statisch verlinkten Prozesses, erzwungenermaßen der Fall sein, aber es kann