Komplexe Refactorings an Legacy Code durchführen
Wenn es um Legacy Code geht, wird vielen Entwicklern unwohl. „Auf der grünen Wiese“ mit einem Projekt zu beginnen macht viel mehr Freude, als im Sumpf des „brownfield“ unterwegs zu sein. Die fehlenden automatisierten Tests sind das eine Problem. Doch viel größer ist die Herausforderung, dass selbst mit Tests keine klare Vorstellung davon existiert, wie man komplexe Refactorings denn überhaupt angehen könnte.
Zu oft haben Entwickler schon die Erfahrung gemacht, dass nach tage- oder wochenlangen Versuchen, den Code wieder unter ihre Kontrolle zu bringen, am Ende nichts besser wurde. Die Strukturen sind zu komplex, Methoden und Klassen zu lang, niemand blickt durch. Im Zweifel wird das große Refactoring, mit dem doch endlich alles besser werden sollte, abgebrochen aus Furcht, dass am Ende nichts mehr funktioniert wie es soll. Leider wird dabei häufig übersehen, dass auch einfache Refactorings dazu beitragen, den Code verständlicher und damit wandelbarer zu machen. Lesen Sie dazu meine Beiträge zu einfachen Refactorings.
Gründe für Refactorings
Ein komplexes Refactoring an einem Legacy Projekt muss immer einen guten Grund haben. Clean Code ist kein Selbstzweck. Als Gründe kommen nur zwei Möglichkeiten in Frage:
- Entweder es soll ein neues Feature ergänzt bzw. ein vorhandenes modifiziert werden
- oder es muss ein Fehler behoben werden.
Ihr Geldgeber wird wenig Bereitschaft zeigen, ohne einen Gegenwert in komplexe Refactorings zu investieren. Es ist sogar noch dramatischer: der Auftraggeber geht davon aus, dass Sie von vornherein wandelbaren Code produziert haben. So wie Sie beim Kauf von Margarine davon ausgehen, dass diese Sie nicht umbringt. Oder weisen Sie den Kassierer im Supermarkt explizit daraufhin, dass Sie eine Margarine wünschen, die Sie nicht tötet? Sicher nicht. Genau sowenig weisen uns unsere Auftraggeber explizit darauf hin, dass sie automatisiert getesten und wandelbaren Code erwarten. Sie erwarten es implizit, weil das dem Stand der Technik entspricht. Wenn Ihr Code also derzeit nicht wandelbar ist, dann haben Sie Ihre Hausaufgaben nicht gemacht. Leider muss ich es so drastisch bezeichnen. Sie sind dafür verantwortlich, die aufgenommenen Schulden zurückzuzahlen. Wirtschaftlich ist das nur im Kontext von ohnehin anstehenden Änderungen.
Chaos
Liegt ein wirklicher Grund für ein Refactoring vor, kann die Änderung des Code beginnen. Das läuft dann typischerweise so ab, wie in der folgenden Bilderserie zu sehen.
(1) Die Entwickler beginnen mit einer Änderung am Code.
(2) Sogleich zeigen sich einige Probleme.
(3) Für diese Probleme werden Lösungen gefunden, doch…
(4) sogleich zeigen sich neue Probleme.
(5) Auch für diese lassen sich Lösungen finden.
(6) Doch erneut zeigen sich Probleme…
(7 – 9) Und so geht es weiter, bis keine neuen Probleme mehr auftreten.
Das mag nicht so tragisch aussehen. Doch zieht sich ein solcher Ablauf häufig über Wochen hin. Daher werden diese Änderungen meist auf einem Branch der Versionskontrolle durchgeführt. So stören die Refactoring Maßnahmen nicht die Weiterentwicklung, die gleichzeitig auf dem Trunk oder einem weiteren Branch durchgeführt wird. Und als wäre das Refactoring selbst nicht schon aufwendig genug, entstehen beim späteren Merge der ganzen Änderungen neue Herausforderungen: Zwischenzeitlich wurde am Trunk ebenfalls weiter entwickelt, so dass das Zusammenfügen aller Änderungen sehr aufwendig ist. Oft werden dabei einzelne Refactoring Schritte erneut ausgeführt, weil dies einfacher erscheint, als ein Merge. Kein Wunder, dass Entwickler von solchen Refactorings lieber die Finger lassen.
Die Mikado Methode
[amazon template=thumbnail&asin=B00LXK8O8Q]
Die beiden Autoren Ola Ellnestam und Daniel Brolund haben in ihrem Buch „The Mikado Method“ eine einfache und dennoch brillante Idee für den Umgang mit komplexen Refactorings vorgestellt. Mithilfe der Mikado Methode verlieren komplexe Refactorings ihren Schrecken. Sehen Sie die Vorgehensweise am gleichen Beispiel.
(1) Zu Beginn steht eine notwendige Änderung.
(2) Diese wird als Ziel im sogenannten Mikado Graph notiert. Dazu später mehr. Nun wird versucht, die Änderung naiv durchzuführen.
(3) Das Ergebnis kennen wir bereits, es tauchen einige Probleme auf.
(4) Statt diese Probleme nun direkt zu beheben, wird das Problem als Erkenntnisgewinn betrachtet und im Mikado Graph notiert.
(5) Anschließend, und das ist der brillante Trick an der Sache, wird die Codebasis mithilfe der Versionskontrolle zurückgesetzt. Somit steht nun wieder ein definierter Zustand zur Verfügung.
(6) Auf diesem definierten Zustand wird erneut versucht, das Ziel naiv umzusetzen, um auf diese Weise zu den nächsten Problemen zu kommen.
(7) Auch hier wird das Problem wieder im Mikado Graph notiert und…
(8) …die Codebasis wird zurückgesetzt.
(9) Nun wird versucht, eines der Probleme, dessen Lösung eine Vorbedingung für die Durchführung der eigentlichen Änderung darstellt, zu lösen.
(10) Erneut zeigen sich Herausforderungen, die wieder im Mikado Graph notiert werden.
(11 – 20) Am Ende dieser Schritte steht mit dem Mikado Graph eine Visualisierung der Vorgehensweise zur Verfügung.
Die folgenden Abbildung zeigen den Mikado Graph zur Bilderserie.
Die Vorbedingungen für die Änderung erhalten eine Bezeichnung.
Der fertige Mikado Graph.
Das Ergebnis der Experimente
Während versucht wurde, die einzelnen Änderungen naiv umzusetzen, ist ein Mikado Graph entstanden, der das komplexe Refactoring in kleine Schritte zerlegt. Diese Erkenntnisse können in der Regel nicht durch eine Analyse gewonnen werden. Bei einer Analyse, die darin besteht sich den Code anzuschauen, werden viele Details übersehen. Diese Details zeigen sich nur, wenn tatsächlich versucht wird, die Änderungen durchzuführen.
Es handelt sich beim Refactoring von Legacy Code um ein komplexes Problem. Es gibt auch komplizierte Dinge. Über komplizierte Probleme denke ich länger nach und finde irgendwann eine Lösung. Durch Nachdenken kann ich sicher feststellen, dass es sich tatsächlich um eine Lösung handelt. Mathematikaufgaben können kompliziert sein. Durch langes Nachdenken und Tüfteln finde ich eine Lösung und bin dann sicher, dass es sich tatsächlich um eine Lösung handelt.
Dagegen gibt es auch komplexe Probleme. Diese zeichnen sich dadurch aus, dass selbst noch so langes Nachdenken Ihnen keine Sicherheit bringt, ob es sich bei der Lösungsidee tatsächlich um eine Lösung handelt. Probleme in der Partnerschaft sind häufig komplex. Sie denken über eine Lösung nach. Doch am Ende können Sie nur sicher sein, dass es sich tatsächlich um eine Lösung handelt, indem Sie ein Experiment machen. Sie probieren die Lösung einfach aus und kriegen so heraus, ob Ihre Idee funktioniert.
Ähnlich ist es mit komplexen Refactorings. Sie können durch Analyse und Nachdenken meist nicht herausfinden, ob Ihre Idee tatsächlich eine Lösung darstellt. Erst ein Experiment führt Sie zu allen Details oder zeigt, dass Sie eine Lösung gefunden haben. Die Mikado Methode setzt auf viele kleine Experimente. Erst durch das konkrete Ausprobieren am Code zeigen sich die vielen kleinen hässlichen Details.
Ein weiterer Vorteil des Mikado Graph liegt in der Visualisierung. Mit einem solchen Graph lässt sich im Team viel besser besprechen, wer welche Aufgaben übernimmt, wann die einzelnen Schritte durchgeführt werden, etc. Ferner kann das ehemals große Refactoring, das nur als „ganz oder garnicht“ betrachtet werden konnte, nun zeitlich gestreckt durchgeführt werden. Der Mikado Graph schafft die Möglichkeit, Schritt für Schritt die Blätter des Graph umzusetzen. Somit ist es möglich, die einzelnen Refactoringschritte konkret einzuplanen und mit der Weiterentwicklung des Systems zu verzahnen.
Umsetzung des komplexen Refactorings
Nachdem der Mikado Graph erstellt ist, werden die einzelnen Herausforderungen angegangen. Wir arbeiten uns nun in der anderen Richtung, im Bild von außen nach innen, vor. Die einzelnen Herausforderungen werden gelöst und jeweils in die Versionskontrolle übertragen. Beachten Sie, dass diese Änderungen auf dem Trunk durchgeführt werden, da sie von kurzer Dauer sind. Ein Branch ist nicht erforderlich, weil die Änderungen nicht lange dauern und das Ergebnis potentiell auslieferbar sein soll.
Der Mikado Graph ist ein gerichteter Graph. An der Wurzel steht das Mikado Ziel, das durch die einzelnen Refactoring Maßnahmen erreicht werden soll. Dies kann so etwas sein wie „Speichern der Daten in der Cloud statt lokal“ oder „Fehler bei der Neuanlage eines Produkts beheben“. Durch die einzelnen naiven Implementationsversuche ist der Mikado Graph als Abhängigkeitsgraph entstanden. Die Umsetzung erfolgt nun in der Gegenrichtung der Abhängigkeiten, von den Blättern zur Wurzel.
Der nächste Beitrag erläutert die Vorgehensweise der Mikado Methode im Detail.
3 Gedanken zu „Komplexe Refactorings an Legacy Code durchführen – Teil 1“