Wiederverwendbarkeit vs. Wartbarkeit
Die Älteren unter uns werden sich erinnern. Früher haben wir viel Wert auf klassische Prinzipien der objektorientierten Softwareentwicklung gelegt: Wiederverwendbarkeit und Wartbarkeit beispielsweise. Mittlerweile wissen wir aber, dass sich diese Prinzipien und auch andere fundamentale Elemente der OOP als weniger vorteilhaft herausgestellt haben. Als Beispiel seien (mehrfache) Vererbung und die Zusammenlegung von Daten und deren Operationen genannt. Vererbung ist deshalb mit Vorsicht zu genießen, weil sie eine zu direkte Kopplung erzeugt. Heute gilt daher meistens die Faustregel, dass Vererbung nur dann erlaubt ist, wenn von einem Abstraktem Konstrukt geerbt wird. Aus den einstigen Objekten, die sowohl Daten und deren Operationen beinhalteten, sind Use-Case Klassen bzw. Interaktoren geworden, die keine harten Abhängigkeiten haben und lediglich PO*Os (Plaing Old [Java/C/…] Object ) hin- und her- schieben.
Doch der Mythos Wiederverwendbarkeit hat sich bis heute recht wacker gehalten. Nun erleben wir eine seltsame Entwicklung. Ausgerechnet der kometenhafte Aufstieg der Microservices Architekturen ist dabei, uns davon zu überzeigen, dass es evtl. bessere Ansätze gibt als die Wiederverwendung und dass Wiederverwendung sogar ein „Smell“ sein kann, also ein Muster, das ein Indiz für eine Fehlerquelle oder qualitativ schlechte Designentscheidung darstellt. Das schauen wir uns in diesem Artikel genauer an.
Die Folgen der Wiederverwendbarkeit
In der Theorie klingt das ja ganz gut. Wir entwickeln Komponenten, die so gebaut sind, dass sie von anderen Komponenten wiederverwendet werden können. Das soll Zeit und Geld sparen. Um hier die Folgen dieses Handelns zu verstehen müssen wir differenzieren, auf welcher Ebene wir in der Lage sein wollen, etwas wiederzuverwenden. Wiederverwendung auf Code-Ebene
So hat alles mal angefangen. Wir hatten C++, wir hatten Klassen und wir bauten weitere Klassen, die wiederverwendet werden sollten. Dafür gab es hauptsächlich zwei Mechanismen: Vererbung und direkter Aufruf. Ohne auf idiomatische Einzelheiten einzugehen möchte ich aber festhalten, dass meistens beide der o.g. Mechanismen eine harte Abhängigkeit zwischen den Klassen erzeugen. Und das ist der Moment, in der die Wartbarkeit, das andere Prinzip das wir aus der Zeit kennen, ins Spiel kommt. Wenn wir harte Abhängigkeiten zwischen Klassen erzeugen, zerstören wir damit die Wartbarkeit des Systems. Es scheint also so zu sein, dass zumindest auf der Code-Ebene, Wiederverwendbarkeit und Wartbarkeit sich gegenseitig ausschließen. Schauen wir, ob sich das auch auf andere Ebenen so auch feststellen lässt.
Wiederverwendung auf Modul-Ebene
Nach C++ kam Java. Mit Java bekamen wir JAR Archive und wir fingen an, stärker über Modularisierung nachzudenken, auch außerhalb der reinen Code-Domäne. Wir erzeugten Bibliotheken, die als verpackte Einheiten für die Wiederverwendung gedacht waren. Und das hat funktioniert. Wieviele JARs ziehen sich moderne Java Anwendungen automatisch über maven und gradle? Und wie nutzen unsere selbst entwickelten Komponenten diese? Meistens direkt, also wieder mit einer starken Kopplung und all den Nachteilen, die das mit sich bringt. Verändert sich ein JAR, das von vielen Komponenten direkt angesprochen wird, in einer großen Java Anwendung uns es wird überall geändert werden müssen. Also leidet auch hier die Wartbarkeit aufgrund der Wiederverwendbarkeit.
Wiederverwendbarkeit auf Microservice-Ebene
Kommen wir also endlich zu unseren Microservices. Hier gibt es gleich mehrere Unterarten von Wiederverwendbarkeit, die man anwenden könnte.
Wiederverwendbarkeit von Microservices
Wie kann ein Microservice wiederverwendet werden? Nun ja, überraschenderweise genauso wie es bei den C++ Klassen der Fall war: indem es von anderen Microservices entweder „vererbt“ oder direkt angesprochen wird. Ich habe „vererbt“ hier absichtlich in Hochkommatas geschrieben, da ich damit auf die Eigenschaft von Docker-Container abziele, ein anderes Image als Basis für ein Neues zu nutzen. Das kommt dem Konzepz von klassischer Vererbung insofern sehr Nahe, als dass auch hier eine harte Kopplung zwischen beiden Images erzeugt wird. Wird das Basis-Image verändert, muss auch das „abgeleitete“ Image neu gebaut werden. Aber auch wenn zwei Services direkt miteinander kommunizieren wird das als ein deutliches „Smell“ angesehen und zerstört wie Wartbarkeit einer Microservices-Architektur. Hauptsächlich aus diesem Grund sind wir der Meinung, dass Services ausschließlich indirekt und asynchron über einen Message-Queue oder ein Event-Bus kommunizieren sollten.
Wiederverwendbarkeit von Bibliotheken
Der andere Fall, den wir beleuchten wollen, ist dann gegeben, wenn es bspw. Bibliotheken gibt, die grundlegende Funktionen aller Services bereitstellen. Diese können unterteilt werden in folgende Kategorien:
- Selbstgeschriebene Infrastruktur-Bibliotheken (etwa um mit einem Logging-Service zu kommunizieren)
- Selbstgeschriebene Basis-Bibliotheken
- Generische Bibliotheken von Drittanbieter
Die erste Kategorie ist m.E. ein Punkt in Microservices-Architekturen, der tatsächlich von Wiederverwendung profitieren kann. Allerdings nur dann, wenn sichergestellt ist, dass einige Rahmenbedingungen gegeben sind. Am wichtigsten dabei ist, dass
- d ie Bibliothek auch für andere Sprachen existiert, damit die Microservices-Umgebung polyglott bleiben kann
- die Bibliothek nicht zu viele eigene Abhängigkeiten hat, die das System einschränkt
Bei den anderen Kategorien wäre ich sehr vorsichtig. Meine Faustregel: Wiederverwendung innerhalb eines Services gerne, über Service-Grenzen hinweg nur in Ausnahmefällen, die gut dokumentiert und argumentiert werden können.
Zusammenfassung
Wir haben gesehen, dass in vielen Fällen Wiederverwendbarkeit auf Kosten der Wartbarkeit eingesetzt wird. Daher behaupten wir schon seit einiger Zeit, dass wir die Wartbarkeit höher priorisieren sollten, da wir davon überzeug sind, dass dieser Fokus unseren Kunden letztendlich mehr bringen wird. Die Begründung ist dabei recht simpel. Der Nutzen der Wiederverwendbarkeit erreicht seinen Höhepunkt während der initialen Implementierung. Die Wartbarkeit dagegen erreicht ihren Höhepunkt im Betrieb.
Was machen wir dann mit der Wiederverwendbarkeit?
Ich glaube dass wir darüber nachdenken sollten, ob wir die Eigenschaften von Microservices-Architekturen dazu nutzen sollten, diese Frage zu beantworten. Wenn wir also kleine, abgegrenzte und einzeln deploybare Einheiten haben, die wir schnell erzeugen und auch wieder zerstören können, und die keine harten Abhängigkeiten untereinander haben, ist es dann nicht sinnvoller dafür zu sorgen, dass diese Eigenschaften stark ausgeprägt sind, auch wenn wir Code oder Bibliotheken duplizieren?
Wir sehen hier einen Trend entstehen und sind gespannt, ob sich das durchsetzen wird. Es gibt bereits Stimmen, die diesen Ansatz radikal formulieren. Auf einer Messe drückte ein Kollege Softwarearchitekt es mit folgenden Worten aus:
Entwickler dürfen innerhalb eines Services rumsauen wie sie wollen, solange der Service sich an die Regeln hält.
Zugegeben, das klingt ein wenig wie Häresie. Selbstverständlich müssen wir auch innerhalb eines Services für Qualität sorgen. Die Wartbarkeit endet ja nicht an der Microservice-Grenze. Trotzdem halte ich es für einen Vorteil, wenn wir zumindest immer den Trade-off zwischen Wartbarkeit und Wiederverwendung im Auge behalten, anstatt uns dogmatisch an evtl. veraltete Regeln zu halten. Wir werden dieses Thema mit Sicherheit weiter vertiefen und unsere Erkenntnisse wie immer hier veröffentlichen.