user-icon Luc Weinbrecht
08. Juli 2022
timer-icon 6 Min.

Migration von Camunda Platform 7 auf 8 mit Clean Architecture

Die Migration oder Aktualisierung eines Camunda Platform 7 Projekts auf Camunda Platform 8 sollte keine Anpassung der eigentliche Geschäftslogik nachsichtig ziehen. Durch die Anwendung des DIP in einem Architekturstil wie Clean Architecture kann dieses Ziel erreicht und leichter auf ein weiteres Framework wie z.B. Camunda Platform 8 migriert werden.

Als Software Engineers haben wir das Ziel Software zu entwickeln, die über einen längeren Zeitraum Business Value generiert, dazu muss die Software wartbar sein. Um wartbare und nachhaltige Software zu entwickeln, gibt es einige Prinzipien und Ziele, die wir als disziplinierte Software Engineers anwenden sollten. Eine der größten Herausforderungen von diszipliniertem Software Engineering besteht darin, Änderungen lokal (siehe z.B. Single Responsibility Principle) und die Geschäftsregeln sowie den Domänencode stabil und wartbar zu halten. Befolgen wie diese Regeln, betreffen die Änderungen bei der Migration oder dem Update eines Projekts von Camunda Platform 7 auf Camunda Platform 8, nicht den gesamten Code – insbesondere nicht den domänenzentrierten Code. Dieses Ziel kann z.B. durch die Verwendung einer Clean Architecture erreicht werden. Mit diesem Blogbeitrag und einem passenden Codebeispiel möchte ich aufzeigen, wie dies erreicht werden kann.Der vorgeschlagene Migrationsplan setzt eine Clean Architecture oder eine ähnliche Trennung von Business- und Framework-Code voraus, z.B. ein hexagonales Architekturmuster. Nichtsdestotrotz kann das zugrunde liegende Prinzip, wie z.B. das Dependency Inversion Principle, auf jedes Softwareprojekt angewendet werden, das andere ähnliche architektonische Ansätze, wie z.B. eine Multitier-Architektur, verwendet.

Bernd Rücker beschreibt in seinem Blogbeitrag „What to do When You Can’t Quickly Migrate to Camunda 8“ ebenfalls den Ansatz die eigentliche Geschäftslogik von den Delegates und allen Camunda APIs zu trennen. Ich habe das Thema „clean delegates“ etwas weiter gefasst und werde mit Clean Architecture eine Möglichkeit beschreiben, die Ziele – die Bernd Rücker ebenfalls erwähnt – umzusetzen.

Camunda Platform 7 und Camunda Platform 8

Im April 2022 veröffentlichte Camunda die neue Camunda Platform 8 (eine neue Ära des „Universal Process Orchestrator“). Camunda Platform 8 wurde von Grund auf neu entwickelt, basierend auf den Erfahrungen aus Camunda Platform 7. Die größte Änderung ist Zeebe als Remote Engine.

Der empfohlene Camunda Platform 7 Greenfield-Stack ist heute Camunda Run mit External Task Workern (siehe Deciding about your Camunda 7 Stack). Vor der Veröffentlichung von Camunda Platform 8 war der empfohlene Greenfield-Stack eine embedded Engine, d.h. die Prozess-Engine wird als Abhängigkeit zu der Anwendung hinzugefügt, was zu einem einzigen Deployment führt. Die Verwendung einer embedded Engine war ein sehr weit verbreiteter Ansatz und wird in der Regel von der Verwendung von JavaDelegates begleitet. Die meisten Camunda Platform 7 Projekte verwenden vermutlich den Ansatz der embedded Engine. Das in diesem Blogpost gezeigte Beispiel basiert daher auf diesem Ansatz, kann aber auch leicht auf das External Task Pattern angewendet werden.

Camunda Platform 8 verwendet Zeebe als Remote-Engine und bietet daher nicht die Möglichkeit, JavaDelegates zur Ausführung von Code, z.B. während ServiceTasks, zu nutzen. Zeebe verwendet Job Worker, um Aufgaben in einem Prozess auszuführen (bspw. das Abschließen einer Aufgabe). Das Konzept der Job Worker ist vergleichbar mit dem External Task Worker in der Camunda Platform 7. Um mehr über die Änderungen von Camunda Platform 7 zu 8 zu erfahren, empfehle ich die Dokumentation Migration von Camunda Platform 7.

In diesem Blogbeitrag konzentriere ich mich nur auf die Änderungen der Entwickler-API, die mit der Migration auf Camunda Platform 8 einhergehen, wie z.B. die Verwendung des ZeebeClient anstelle des RuntimeService zum Starten einer Message Correlation. Daneben gibt es einige weitere Änderungen wie GraphQL für die Tasklist API, gRPC als Protokoll für die Zeebe Job Worker API und die Nutzung von Kubernetes.

Motivation für Clean Architecture

Eine Software-Architektur, die Veränderbarkeit in ihre Paket- und Klassenstruktur zugrunde legt, vereinfacht einen Wechsel oder eine Migration eines Frameworks. So ist die Migration von Camunda Platform 7 auf 8 mit einer guten Architektur viel weniger invasiv.

Camunda and clean architecture

Die Schichten der Clean Architecture (basierend auf Clean Architecture von Robert C. Martin)

Robert C. Martin beschreibt in seinem Buch „Clean Architecture“ architektonische Richtlinien, die eine Unabhängigkeit von Frameworks, Datenbanken, Benutzeroberfläche (UI) und anderen Technologien ermöglichen sollen. Seiner Meinung nach gewährleistet die Clean Architecture durch ihr Design die Testbarkeit von Geschäftsregeln. In der obigen Abbildung sind die Schichten als konzentrische Kreise dargestellt, die sich gegenseitig umhüllen. Jede Schicht steht für einen anderen Teil der Software. Das Zentrum des Kreises repräsentiert „Richtlinien“ und damit ihre Geschäftsregeln und ihr Domänenwissen. Die äußeren Kreise sind „Mechanismen“, die unser Domänenzentrum unterstützen. Neben den Schichten zeigen die Pfeile die Abhängigkeitsregel – nur nach innen gerichtete Abhängigkeiten! Um die Ziele der Clean Architecture zu erreichen, darf der Domänencode keine Abhängigkeiten haben, die nach außen zeigen. Stattdessen zeigen alle Abhängigkeiten in Richtung des Domänencodes.

Der wesentliche Aspekt der Geschäftsdomäne befindet sich im Kern der Architektur: Die Entitäten. Auf sie wird nur von der umgebenden Schicht zugegriffen: Den Anwendungsfällen. Services in einer klassischen Schichtenarchitektur stellen Use Cases in der Clean Architecture dar, aber diese Services sollten feingranularer sein, so dass sie nur eine Aufgabe bzw. Verantwortung haben. Es sollte nicht das Ziel sein, einen einzigen großen Service zu implementieren, der alle Geschäftsregeln anwendet. Unterstützende Komponenten werden um den Kern (Entitäten und Use Cases) herum platziert, z. B. Persistenz oder Benutzerschnittstellen.

Das Dependency Inversion Principle

Wird die Abhängigkeitsregel angewendet, so hat die Domäne keine Kenntnis darüber, wie die Daten persistiert oder in einem Client anzeigen werden. Die Domäne sollte keinen Framework-Code enthalten (außer evtl. Dependency Injection). Wie bereits erwähnt, kann das Dependency Inversion Principle (DIP) verwendet werden, um die Abhängigkeitsregel der Clean Architecture anzuwenden.

Das DIP gibt die Umkehrung der Abhängigkeitsrichtung vor. Das erinnert vielleicht an das IoC-Designmuster (Inversion of Control). Es ist jedoch nicht dasselbe wie DIP, obwohl beide gut zusammenpassen. Für eine genaue Unterscheidung empfehle ich Martin Fowlers Artikel „DIP in the Wild“ (kurz gesagt: „[…] IoC is about direction, and DIP is about shape.“). Die nachfolgende Abbildung zeigt exemplarisch wie das DIP funktioniert.

Dependency Inversion Principle

Das Dependency Inversion Principle (DIP)

Stellen Sie sich einen Service (DomainService im Bild) vor, der einen Camunda Prozess startet. Um den Service (bzw. die Geschäftslogik) vom Framework zu isolieren, könnte man einen weiteren Service erstellen, der die Camunda Java API nutzt, um eine Prozessinstanz zu starten. Der linke Rahmen des Bildes zeigt dieses Szenario ohne Anwendung des DIP. Der Domain Service ruft den ProcessEngineService direkt auf. Was ist hier das Problem? Das Starten eines Prozesses ist eine Kernfunktionalität unserer Domäne, also wollen wir diese in unsere Domäne holen. Damit brechen wir die Regel, dass unser Domänen-Framework unabhängig sein soll. Durch die Platzierung einer Schnittstelle in unserem Domänenkern anstelle der konkreten Implementierung und die Verschiebung der eigentlichen Implementierung außerhalb unserer Domänenschicht lässt sich der Regelbruch verhindern – et voilà wir wenden das DIP an.

Kombiniert man das DIP mit dem Architekturmuster der Ports und Adapter (aus der die Clean Architecture hervorging), erhält man das unten gezeigte Bild.

Clean Architecture DIP

Clean Architecture DIP und Ports und Adapter.

Die Trennung von Ports / Anwendungsfällen und Adaptern, die unsere Anwendung steuern (input-ports) oder von unserer Anwendung gesteuert werden (output-ports), hilft uns, unseren Code noch besser zu strukturieren und die Grenzen klarer zu halten.

Mapping zwischen Schichten

Das folgende Bild zeigt, wie Schichten mit und ohne Mapping mit dem Domänenobjekt interagieren. Ohne Mapping verliert man den größten Vorteil der Clean Architecture: Die Entkopplung des Domänenkerns von äußeren (Infrastruktur-) Schichten. Wenn man kein Mapping zwischen inneren und äußeren Schichten vornimmt, sind diese nicht isoliert. Ändert ein externes System sein Datenmodell, so muss sich auch das Domänenmodell ändern. Um die Abhängigkeit von externen Einflussfaktoren zu verhindern und die Unabhängigkeit und Entkopplung zu fördern, ist ein Mapping zwischen den Schichten notwendig. Die Input- und Output-Ports (Use-Case-Schicht) dienen als definierten Eingangspunkt in den Domänenkern und definieren, wie die Kommunikation und Interaktion mit der Anwendung erfolgen soll. Sie bieten eine klare API, und durch das Mappen in die Domäne wird man unabhängig von Änderungen am Framework oder Technologiemodell.

Camunda and clean architecture mapping

Mit und ohne Mapping zwischen den Ebenen

Der untere Ausschnitt, der den Mapping-Ansatz erklärt, ist nur eine mögliche Art des Mappings. Wenn Sie mehr über die verschiedenen Stufen des Mappings erfahren möchten, empfehle ich einen Blick in Tom Hombergs Buch „Get Your Hands Dirty on Clean Architecture„.

Zusammenfassend lässt sich sagen, dass Mapping dazu dient, eine größere Entkopplung zu erreichen. Auf der anderen Seite kann das Mapping zwischen den einzelnen Schichten zu einer Menge Boilerplate Code führen, was je nach Anwendungsfall und den angestrebten Zielen nicht zwingend notwendig ist.

Austausch der Adapter beim Upgrade auf Camunda Platform 8

Die unten dargestellte Diff-Ansicht zeigt ein symbolisches Beispiel: Die Migration eines JavaDelegates zu einem Job Worker. Sie zeigt, wie die JavaDelegate Implementierung verschwindet. Stattdessen übernimmt eine mit @ZeebeWorker annotierte Methode alle Aufgaben, die z.B. während eines Service-Tasks durchgeführt werden sollen. Das Snippet zeigt auch, dass die Migration keine tiefgreifenden Änderungen am Domänenkern erfordert, wenn der Code richtig strukturiert ist: Die Implementierung des Service-Tasks (hier das Auswählen eines Inhalts) ändert sich nicht, da der „cleane“ Service-Task den zugrunde liegenden Port zu der Domäne aufruft. Durch die Änderung der Adapterimplementierung – der Interaktion mit Ihrer Prozess-Engine – ändert sich die Implementierung der Domäne nicht. Es wird immer noch die gleiche Schnittstelle recommendationPicker.pickContent() aufgerufen.

camunda 7 and 8 diff

Git-Diff einer Service-Task-Implementierung Camunda Platform 7 zu 8

Durch die Anwendung der Clean Architecture beschränken sich die einzigen geänderten Dateien auf die Adapterschicht (plus BPMN-Modell-Anpassungen). Die Domäne muss also nicht angerührt werden, um ein Framework zu ändern. Alle notwendigen Änderungen für ein Upgrade von Camunda Platform 7 auf 8 im zugrunde liegenden Beispielprojekt sind in diesem Pull Request aufgeführt. Er zeigt, die einzigen Änderungen in der Adapterschicht und die Unberührtheit der Domäne, beim Austausch eins Frameworks.

Dies war nur eine kurze Zusammenfassung einer Migration auf die Camunda Platform 8. Für einen tieferen Einblick in die Änderungen sollten sie einen Blick auf das zugrundeliegende Beispielprojekt werfen. In dem vorgeschlagenen Migrationsplan wird eine Clean Architecture vorausgesetzt. Wie bereits beschrieben, können die Prinzipien jedoch auch auf andere Architekturansätze angewendet werden, wie z.B. eine Schichtenarchitektur.

Zusammenfassung

Ein isolierter Domänenkern ist die Voraussetzung für Stabilität und einen leichteren Wechsel von Frameworks. Durch die Anwendung des DIP in einem Architekturstil wie der Clean Architecture können wir unsere Domänenlogik von allen Frameworks, Persistenz- und UI-spezifischen Problemen entkoppeln, da sich die erforderlichen Änderungen lediglich auf die äußeren Schichten reduziert und der Domänenkern somit isoliert bleibt.

Werfen Sie einen Blick auf das Beispielprojekt, welches zeigt, wie Clean Architecture beim Upgrade von Camunda Platform 7 auf 8 hilft. Insbesondere der Pull-Request zeigt, dass nur die äußeren Schichten angepasst werden müssen und der Business-Code somit stabil bleibt.

Und wie immer: Wir unterstützen Sie gerne mit unserer BPM-Technologieberatung.

Artikel kommentieren