Kolumne: Be pragmatic, not dogmatic!

"Modulare Anwendungen sind nur für größere Projekte gut – sonst lohnt sich der Mehraufwand nicht!"

Joachim Arrasz

Im letzten Teil der Kolumne Be pragmatic, not dogmatic! haben wir über typsichere Schnittstellen, deren Vorteile aber auch deren Nachteile gesprochen. Dabei wurde festgestellt, dass die Homogenität der Systeme das wichtigste Entscheidungskriterium ist und es deshalb wohl keinen goldenen Weg geben kann. 

In diesem Teil werde ich ein Dilemma besprechen, welches mir seit Jahren immer wieder begegnet: die Tatsache, dass Systemgrenzen nicht erkannt oder aber nicht eingehalten werden. Wer kennt das nicht: Man schaut sich eine Software an und stellt fest, dass man eigentlich drei unterschiedliche Systeme in einem vor sich hat…

Noch ein wichtiger Punkt, der unbedingt angemerkt werden muss: Alles was ich hier schreibe ist meine persönliche Meinung und spiegelt auch nur diese wider. Diskussionen sind herzlich willkommen! Falls sich der eine oder andere angesprochen fühlt – natürlich ist alles frei erfunden und Ähnlichkeiten sind rein zufällig…

Ich wurde mit der Aufgabe zu einem Kunden geschickt, dessen System(e) zu analysieren und dessen Team in softwaretechnischen Aufgaben zu beraten. Fragen dort waren unter anderem:

• Warum scheint unsere REST-Architektur mehr im Weg zu sein, als sie hilft?

• Warum hat das Team schwankende Leistungen in der Fertigstellung von Features?

• Wie kann ich meine vertikale Architektur absichern?

• Wie können wir die allgemeine Qualität messen?

Nach einer ersten Analyse konnte man sehr gut erkennen, dass man sehr starr an der Idee der vertikalen Servicearchitektur festhielt, und diese auch verteidigt und gepflegt wurde. Soweit, so gut. Weiterhin sah auch das DB-Schema nicht sonderlich überfrachtet aus. Es wunderte nur, dass es relativ wenige Relationen gab, Teile wunderbar isoliert da standen.

Das letzte, was ich mir dann noch intensiver angeschaut habe, waren die Controller der REST-Schnittstellen. Diese wurden mittels Spring-MVC realisiert. Die Controller waren oft enorm überfrachtet. Alles, was an die Tabelle(n) XYZ gesendet werden sollte, wurde mittels einem XYZ-Controller realisiert. Es gab auch keine Unterscheidung zwischen rein technischen Steuerungsservices und den eigentlichen Kundenservices. Daraufhin ließ ich mir eine typische Aufruf-Reihenfolge der Services geben, um die Zusammenhänge zu begreifen.

Es waren zwei ziemlich voneinander unabhängige Arbeitsgebiete innerhalb der Anwendung zu erkennen. Nachdem ich dann im Domain-Modell ebenso keine Zusammenhänge erkennen konnte und auch die Package-Strukturen aufzeigten, dass es sich um zwei verschiedene und auch ziemlich unabhängige Anwendungen innerhalb einer Anwendung handelte, inspizierten wir gemeinsam die Abhängigkeiten, die tatsächlich zwischen den Services bestanden, genauer.

Wir stellten fest, dass es sich eigentlich fast ausschließlich um ungenutzte Abhängigkeiten handelte. Die Code Duplications bewiesen dann auch, dass hier einfach komplette Controller per Copy and Paste erstellt wurden. Als ich meine Meinung dann kund tat, dass hier eine gewisse „Faulheit“ im Umgang mit neuen Features zutage trete, war der Aufschrei im Team natürlich zuerst groß. Das hatte ich aber befürchtet und vorab eine kleine Analyse von Code Duplications und Cyclomatic Dependencies mittels des Werkzeugs Sonar erstellt. Diese bestätigte jede Menge Code Duplications, Unused Imports von Services und auch eine hohe zyklomatische Komplexität innerhalb von bestimmten ablaufbestimmenden Methoden.

Natürlich wurde dann das immer wiederkehrende Argument der falsch verstandenen Wiederverwendbarkeit bemüht. Aber Wiederverwendbarkeit bedeutet nicht, dass ich Methoden so allgemein halte, dass sie n-beliebig viele Dinge tun können, sondern dass sie für einen immer wiederkehrenden Fall nutzbar sind… Single Responsibility Principle und Co. lassen grüßen! Und so erklärt sich aus diesem einen grundlegenden Verständnisfehler das komplette Problem innerhalb der Anwendung. Man erkannte aufgrund von falsch verstandener Wiederverwendbarkeit fachliche Unterschiede nicht und begann damit, einen Monolithen mit vielen vertikal aufgezogenen Services aufzubauen, statt unterschiedliche Anforderungen zuerst einmal mit eigenen Anwendungen zu begegnen.

Das letzte Argument, welches mir noch entgegen geschleudert wurde, war dann der Aufwand des Packaging und des Deployments mit so vielen einzelnen Anwendungen. Hier gibt es natürlich mit verschiedenene Services auch mehr initialen Aufwand. Allerdings verringert sich orthogonal dazu auch der Testaufwand von Änderungen sowie die Gefahr eines kompletten Blackouts. Darüber hinaus könnte man mit einem Overlay-Mechanismus zum Ende hin aus den vielen Einzelanwendungen wieder eine gemeinsame erzeugen 😉

[ header = Seite 2: Meine Meinung zu dieser Kontroverse ]

Meine Meinung zu dieser Kontroverse:

In diesem Fall war es aus meiner Sicht äußert schwer, „mal eben“ zu helfen bzw. sich eine richtige Meinung zu bilden. Es wurde viel richtig gemacht, zumindest zu Beginn des Projekts. Aufgrund von Releasedruck wurde es dann immer schwieriger, auf alles gleichzeitig zu achten, da deutlich zu wenig automatisiert war. Eine Integration war vorhanden, aber automatische Metriken wurden nicht erzeugt. Auch auf die Testergebnisse wurde irgendwann (das Projekt wurde zwei Monate vor Release einfach deaktiviert, keiner weiß mehr warum) nicht mehr geachtet.

Auf der anderen Seite wurden dann aber Lasttests vor dem Livegang durchgeführt. Aufgrund der erkennbaren Lücken und nicht ausbalancierter Automatismen innerhalb der Toolchain sind hier meiner Meinung nach verschiedene Schritte nötig:

Immer auf der grünen Wiese mit einem neuen Service beginnen. Bei neuen fachlichen Services sogar in einem neuen Projekt. Hierüber erkennt man Dependencies direkt, und man bekommt Routine im Umgang mit immer wiederkehrenden Dingen wie dem Erstellen einer Maven POM. Man erkauft sich Abhängigkeiten bewusst, statt sie „geschenkt“ zu bekommen, obwohl gar nicht benötigt. Man erkennt auch die Notwendigkeiten, gewisse APIs für andere Services bereit zu stellen. Die ganze Architektur entkoppelt sich dadurch (fast) automatisch.

Refactoring muss Bestandteil einer jeden Iteration werden, idealerweise innerhalb der zu implementierenden Features. Werkzeuge wie Sonargraph und Sonar müssen in die Continous-Integration-Zyklen integriert werden, um Violations frühzeitig erkennen zu können. Die Ergebnisse der Metriken müssen Bestandteil einer Abnahmeprüfung (wie beispielsweise einer „Definition of Done“) sein. Pro Werkzeug muss es einen Verantwortlichen geben, der für Pflege und Updates Zeit bekommt.

Es muss regelmäßig Schulungen zu Violations und dem angestrebten Architekturmodell geben. Dinge, die zu komplex sind, sollten idealerweise automatisiert werden. Nach jeder Iteration sollte man sich seine technischen Schulden (Technical Debt, Ward Cunningham) gegenüber dem Projekt bewusst machen und versuchen, diese geringer zu halten, als sie in der vorherigen Iteration war. Die Fragestellungen des Management waren somit auch recht einfach zu beantworten:

Die REST-Architektur war im Weg, da das Domain-Modell nicht durch die Use- Cases iterativ modelliert und entwickelt wurde, sondern große Teile auf der grünen Wiese, ohne echte Anforderungen erdacht wurden. Somit waren die REST-Controller vielmehr ein Abbild der fachlichen Domain, anstatt echte Use-Cases zu bearbeiten. Schwankende Leistungen innerhalb der Entwicklung gab es, da das Team weder explizite Refactoring-Zyklen zugestanden bekam, noch die Notwendigkeit dazu sah. Das Team erkannte nicht, dass die Komplexität aufgrund der immer enger werdenden Kopplung zunahm. Man scheute den Aufwand, ein allgemeines API heraus zu refactoren. Stattdessen wurde alles „ineinander hineingekleistert“.

Werkzeuge wie Sonargraph sind in der Lage, die Absicherung zu automatisieren. Somit muss das Team nicht immer selbst ein Auge darauf haben, das Werkzeug weist einen selbstständig darauf hin. Dies ermöglicht es auch, die Aufgabe der Refactorings statisch an die geeignetste Person innerhalb des Teams zu vergeben. Eine Continous Integration war ja bereits vorhanden, somit war es recht einfach, diese um die Erzeugung von Sonar-Metriken zu erweitern. Natürlich müssen die Schulungen, was die Violations eigentlich genau bedeuten, noch folgen. Das eigentliche Problem innerhalb dieses Projekts bestand meiner Meinung nach aber in der falsch verstandenen Wiederverwendbarkeit sowie an der Unkenntnis grundlegender Regeln wie dem Single-Responsible-Principle. Dies sind glücklicherweise einfach zu behebende Dinge 🙂

Meiner Meinung nach sollte es also guter Stil sein, jedes Feature, jeden Service zuerst einmal komplett losgelöst vom bisherigen Projekt zu entwickeln, um von vornherein immer bewusst über jede Kopplung und jede Bibliothek nachzudenken.

Diese beiden Tools werden ohnehin nie unabhängig voneinander gebraucht, wozu sollen wir Module vorsehen?

Geschrieben von
Joachim Arrasz
Joachim Arrasz
Joachim Arrasz ist als Software- und Systemarchitekt in Karlsruhe bei der synyx GmbH & Co. KG als Leiter der CodeClinic tätig. Darüber hinaus twittert (@arrasz) und bloggt er gerne (http://blog.synyx.de/)
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.