Contract Driven Development verbindet Frontend und Backend
Konzepte und Ideen zu Headless CMS und den möglichen Anwendungsfällen sind derzeit in aller Munde. Für unser Sitecore-Team begann die Reise in diese Welt mit der Einführung von Sitecore Javascript Services JSS für die neue Multisite-Plattform für den Flughafen Zürich. Tobias Studer hat das Neuland Schritt für Schritt erschlossen und gibt einen Einblick in die Überlegungen und die Herausforderungen, welche die finale Lösung geprägt haben.
Neuland JSS und Headless
Für unser Sitecore-Entwicklungsteam war nicht nur JSS als Technologie neu, sondern auch das Entwickeln mit einem Headless-Ansatz. JSS stellt «out of the box» Datenstrukturen bereit, indem Sitecore-Daten-Templates für die Aussenwelt verfügbar gemacht werden. Obwohl man dies mit integriertem GraphQL anpassen kann, liegt die «Quelle der Wahrheit» immer noch in Sitecore. Damit die Frontend-Entwicklerinnen und -Entwickler ihre Komponenten bauen können, müssen sie diese Sitecore-Strukturen kennen und verstehen.
Nun gibt es in unseren Entwicklungsteams die übliche Unterteilung in Backend und Frontend. Die Frontend-Entwicklung hat keine Sitecore-Instanzen lokal laufen, arbeitet fast ausschliesslich an MacBooks und kennt sich mit den Besonderheiten von Sitecore nicht besonders gut aus. Ohne eine Anpassung des JSS-Workflows wäre sie massiv vom Status der Sitecore-Umgebung und ihren Datenstrukturen abhängig.
An der Schnittstelle zwischen Frontend und Backend
Schon vor Entwicklungsbeginn haben wir uns einige zentrale Fragen gestellt, wie wir die Schnittstelle zwischen Frontend und Backend orchestrieren können:
Wie können wir die Frontend- und Backend-Entwicklung so weit wie möglich entkoppeln?
Wie können wir Informationen darüber bereitstellen, welche Daten für die Frontend-Komponenten verfügbar sind?
Wollen wir die internen Datenstrukturen von Sitecore offenlegen, oder wollen wir eine zusätzliche Trennung?
ViewModels als Bindeglied zwischen zwei Welten
Wenn man sich aus der vertrauten Sitecore-Umgebung herauswagt und sich ein klassisches ASP.NET MVC-Projekt mit, sagen wir mal, einem Entity Framework anschaut, würde man die Datenmodelle aus dem Datenbankkontext wohl lieber nicht offenlegen. Stattdessen würde man ViewModels nutzen, um eine direkte Abhängigkeit zwischen der Datenstruktur und den für die Frontend-Komponenten verfügbaren Daten zu vermeiden. In unserem Kontext sind die Daten-Templates von Sitecore diese Datenstrukturen und die Ebene der ViewModels gibt es nicht. Man muss also eine zusätzliche Ebene der ViewModels einführen, die Frontend und Backend zusammenbringt. Sie ist das Bindeglied, in dem die gemeinsame Wahrheit über die Datenstrukturen enthalten ist.
Aber solange wir das ViewModel nicht formalisieren können, haben wir nicht viel gewonnen. Natürlich können wir einen mündlichen Vertrag zwischen Backend- und Frontend-Entwicklung schliessen, aber dieser ist schwer zu verwalten, vage und öffnet Tür und Tor für Missverständnisse. Um das zu vermeiden, haben wir einen Contract eingeführt, der technisch genug ist, um präzise zu sein, aber simpel genug ist, dass mehrere Rollen im Projekt dazu beitragen können. Dann haben wir uns eine Lösung ausgedacht, mit der wir aus dem Contract ViewModels als C#-Klassen für das Backend und React-PropTypes für das Frontend generieren können.
Eine gemeinsame Sprache
Der Contract besteht aus den finalen ViewModels sowie aus deren Bausteinen. Letztere können entweder ein Sitecore-Feldtyp sein oder ein Teilmodell, das im ganzen Contract wiederverwendet werden kann. Schauen wir uns zunächst mal die Feldtypen genauer an:
Für uns war eine wichtige Anforderung, weiterhin den Experience Editor nutzen zu können, Sitecores WYSIWYG-Editor. Dafür mussten wir die Feldtypen darstellen können. Die meisten davon sind relativ einfach, wie zum Beispiel ein einfaches Textfeld.
Wie man in diesem Snippet sehen kann, ist der Contract eine Markdown-Datei, die Titel mit optionalen Selektoren für Meta-Informationen über eine Klasse und einen YAML-Block für die technische Spezifikation des Modells verwendet. Die Markdown-Datei ist ein CommonMark-Dokument mit eingebetteten Code-Blöcken für maschinenlesbare Abschnitte. Wenn wir einen Blick auf den Contract für ein ViewModel werfen, sehen wir, wie diese Bausteine weiterverwendet werden können.
AutoRest übersetzt den Contract in Code
Für die Code-Generierung haben wir AutoRest verwendet. Üblicherweise werden damit Client Libraries für den Zugriff auf RESTful Webservices generiert; wir nutzen seine Unterstützung von Literate-Dateiformaten, um unseren Contract in Code zu verwandeln.
AutoRest akzeptiert eine Konfigurationsdatei mit Anweisungen. Diese sahen bei uns zum Schluss wie folgt aus:
Diese Datei definiert mit der Einstellung input-file, welche Datei als Contract verwendet werden soll. Das Ergebnis, wenn man diesen Prozess anstösst, sind die Modelle als C#-Klassen. Es entsteht aber auch unmittelbar ein Artefakt swagger-document.json, was eine OpenAPI-Spezifikation des Contracts ist. Diese Datei kann dann von openapi-proptypes-generator verwendet werden, den wir Open Source zur Verfügung stellen, um die React-PropTypes zu generieren:
Die Datei definiert ausserdem einige Anweisungen, mit denen die Code-Generierung beeinflusst werden kann. Da alle gültigen OpenAPI-Spezifikationen Operationen definieren müssen, haben wir einen Dummy-Eintrag im Contract hinzugefügt. Die erste Anweisung entfernt alle Operationen aus dem Generierungsprozess, weil wir sie im generierten Code nicht brauchen. Die zweite Anweisung führt eine Konvention ein, mit der jedes Modell aus dem Contract das ViewModel-Postfix erhält. So würde zum Beispiel aus dem flightProcess-Contract-Modell die Klasse FlightProcessViewModel in C#. Die dritte Anweisung entfernt alle Klassen, die nicht Modelle sind, da uns nur die Modelle interessieren. Wir identifizieren diese Klassen über den Models-Namespace. Mit der letzten Anweisung vereinfachen wir den Namespace.
Transportdienst Model Builders
Jetzt haben wir alle Datenstrukturen in Sitecore – die natürlich alle brav den Vorgaben von Sitecore Helix folgen – und wir haben den Contract, der definiert, wie die Frontend-Komponenten die Daten erwarten können. Damit bleibt noch die Frage: Wie kommen die Daten von A nach B? Um diese Frage zu beantworten, haben wir uns entschieden, unsere Softwarearchitektur am Konzept von Model Builders auszurichten. Kurz gesagt holen sich Model Builders Daten aus Sitecore-Items und mappen sie auf die ViewModels. Wir nutzen Synthesis, um stark typisiert zu sein, und haben unterschiedliche Typen von Model Builders eingeführt, um der Helix-Architektur gerecht zu werden.
Im Feature Layer haben wir unseren ersten Model Builder, der die Synthesis-Darstellung des Interface-Template als Input bekommt und es auf eine Klasse mappt, die wir Carrier Model nennen. Der Hauptzweck dieses Modells ist es, die Daten in den Project Layer zu transportieren, wo sie in das finale ViewModel umgesetzt werden.
Dieser erste Model Builder nutzt die Model Factory, um seine verschachtelten Modelle zu bauen und eine bessere Wiederverwendbarkeit zu gewährleisten. Mehr dazu können Sie in dieser dreiteiligen Serie nachlesen: Teil 1, Teil 2, Teil 3.
Im Project Layer haben wir den letzten Model Builder, der für die Erstellung des ViewModel zuständig ist. In dem einfachen Beispiel des Flight Process würde nur das Carrier Model erstellt und dem ViewModel zugeordnet. Wenn das ViewModel Daten aus verschiedenen Features brauchen sollte, würde das auf dieser Ebene geschehen.
Contract Driven Development stärkt den Entwicklungsprozess durchgängig
Auch mit ein bisschen Abstand können wir rückblickend sagen, dass Contract Driven Development ein erfolgreicher und nützlicher Ansatz für den Bau einer JSS-Seite war. Nicht nur die Zusammenarbeit zwischen unserer Frontend- und Backend-Entwicklung hat sich dadurch völlig verändert, sondern auch die Art und Weise, wie wir mit unseren Requirement Engineers und Business-Analysten zusammenarbeiten. Der Contract war eine simple Vereinbarung zwischen allen beteiligten Parteien. Mit der Code-Generierung konnten wir Missverständnissen vorbeugen. In unserem stark typisierten Backend-Code würden Änderungen im Contract sofort auffallen, weil die Kompilierung fehlschlagen würde. Das war eine sehr nette Nebenwirkung.
In fast allen Fällen konnten Frontend- und Backend-Entwicklung unabhängig arbeiten und haben sich gegenseitig nicht aufgehalten. Die einzige Anforderung war und ist, dass der Contract mehr oder weniger steht und stabil ist. Änderungen daran mussten wegen möglicher Auswirkungen auf andere Komponenten und Spezifikationen zwischen der Entwicklung und dem Requirement Engineering abgeglichen werden.
Kontakt für Ihre digitale Lösung mit Unic
Termin buchenSie möchten Ihre digitalen Aufgaben mit uns besprechen? Gerne tauschen wir uns mit Ihnen aus.
Wir sind da für Sie!
Termin buchenSie möchten Ihr nächstes Projekt mit uns besprechen? Gerne tauschen wir uns mit Ihnen aus.