Follow my new blog

Samstag, 22. August 2009

Code cleaning – aber wann? [endlich-clean.net]

image Neulich habe ich ein hübsches Scrum-Buch gelesen: Scrum mit User Stories. Darin gibt es eine “Definition of Done”. Die beschreibt, wann ein User Story (Anforderung) eigentlich vom Team als fertig anzusehen ist. Denn erst wenn sie fertig ist, kann das Team mit der nächsten in einem Sprint weitermachen.

Ein Kritierium für “Done” ist darin: “Die User Story führt zu keinem Anstieg der ‘Technischen Schuld’”. Das hört sich gut an. Da hab ich bei der Lektüre sofort zustimmend genickt. An anderer Stelle ist es so formuliert: “[Nicht] refaktorisierte User Stories [sind] nicht fertig.”

Dass zu fertig die Refaktorisierung gehört, steckt auch im TDD-Vorgehen red-green-refactor drin.

Also ist alles klar, oder? Fertig bedeutet refaktorisiert. Ein Entwickler fühlt sich nicht wohl, bevor der Code nicht sauber gemacht ist. Erst dann gibt er ihn guten Gewissens an die nächste Phase im Softwareproduktionsprozess weiter. Das ist Clean Code Development.

Oder vielleicht doch nicht? Heute sind mir nämlich Zweifel gekommen.

Nein, ich zweifle nicht am Wert von Clean Code. Refaktorisierung ist ne gute Sache. Aber wann? Sollten sie am Ende einer Implementierungsphase stehen? Nein!

Es ist ein Missverständnis, mit Refactoring eine Implementation abschließen zu wollen. Ich glaube, wenn man einen kleinen Moment drüber nachdenkt, dann ist das auch ganz einsichtig. Refaktorisierter Code ist ein Feature von Software. Er steht im Grunde auf derselben Stufe mit Funktionalität oder Performance. Nur fordert dieses Feature nicht der Anwender, sondern das Entwicklerteam.

Refaktorisierung unterliegt damit genauso den Prinzipien YAGNI, KISS und Beware of Premature Optimization!

Wenn ein Entwickler auf dem Zettel hat, die Multiplikation für einen Taschenrechner zu implementieren, dann ist seine Arbeit fertig, wenn er die Multiplikation korrekt aus Sicht des Kunden implementiert hat. Wenn er darüber hinaus jedoch auch noch nach dieser getanen Arbeit refaktorisiert, dann halte ich das für eine vorzeitige Optimierung. Niemand weiß, ob der refaktorisierte Zustand der Anwendung irgendwann mal nützlich wird. Vielleicht fällt dem Kunden ein, dass er die Multiplikation nicht braucht und alles per Addition rechnet. Dann ist der Refaktorisierungsaufwand vergebens gewesen.

Ein refaktorisierter Zustand ist daher genau wie jedes andere Feature erst dann herzustellen, wenn wirklich klar ist, dass es gebraucht wird. Das bedeutet, die Arbeit an der inneren Qualität findet vor (!) der Arbeit an der äußeren Qualität statt. Das wird auch klar, wenn wir den TDD-Prozess verlängern und etwas anderes notieren:

  • red-green
  • refactor-red-green
  • refactor-red-green
  • refactor-read-green

Refactor steht in der kurzen Phase “red-green-refactor” nur am Ende, weil impliziert wird, dass es danach weitergeht. Insofern finden während der Implementation eines äußeren Features natürlich immer auch Refaktorisierungen statt.

Ich halte es jedoch für sehr bedenkenswert, am Ende (!) nicht ruhen zu wollen, bevor nicht “alles” so richtig sauber ist. Stattdessen sollte der Entwicklungsprozess vorsehen, dass zu Beginn (!) der Arbeit an äußerer Qualität zuerst die nötige innere Qualität hergestellt wird. Und zwar nur die wirklich nötige innere Qualität!

Ohne ein Maß kann Refaktorisierung genauso zur Sucht werden wie Performanceoptimierung. Wenn also Performanceoptimierung nur stattfinden soll mit konkreter Zielvorgabe (“Die Suche nach einem Kunden darf höchstens 1 Sekunde dauern.”), dann soll auch Refaktorisierung nur mit konkreter Zielvorgabe stattfinden.

In Ermangelung quantifizierbarer Refaktorisierungsziele ist deshalb die Zielvorgabe einer Refaktorisierung eine innere Qualität, die die Implementation des nächsten Kundenfeatures leicht macht. Was dafür nötig ist, ist allerdings erst klar, wenn die Arbeit an diesem Kundenfeature beginnt.

Ich denke daher, die Arbeit an einer User Story sollte so definiert sein:

  1. Plane Implementation der User Story bzw. einer Task innerhalb der User Story
  2. Refaktorisiere vorhanden Code vor Beginn der Implementation nach Bedarf
  3. Implementiere mittels refactor-red-green
  4. Liefere User Story bzw. Task aus

Am Ende des letzten kleinen refactor-red-green-Schrittes bleibt dann zwar etwas technical debt übrigen, aber das macht nichts. Solange keine weitere Anforderung existiert (oder genauer: keine weitere User Story/Task begonnen wurde), wüsste ja niemand, welchem Zweck eine weitere Refaktorisierug dienen sollte.

Die ursprüngliche “Defintion of Done” ist damit sogar fast erfüllt. Zwar ist der Code nicht ohne “Technische Schuld”, aber die ist verschmerzbar klein, nein, sogar unvermeidbar, wenn Sie nicht in Bezug auf CCD in die YAGNI-Falle tappen wollen.

Nur so ist sichergestellt, dass die CCD-Bausteine konsequent, d.h. selbstbezüglich angewandt werden. CCD-Bausteine sind kein Selbstzweck und müssen mit Augenmaß angewandt werden.

Kommentare:

Andreas Heil hat gesagt…

Das Problem, dass Du hier ansprichst ist ein typisches Problem, dass in der Natur der Software-Entwickler liegt. Entwickler lieben es Code zu schreiben, denn das ist es was sie beherrschen, das ist das was ihnen Spaß macht. Dadurch wird auch allzu gerne das KISS Prinzip verletzt. Der Software-Entwickler schreibt komplizierten Code weil er es beherrscht und weil es ihm Spaß macht. Und wer erst einmal Spaß am Refactoring gefunden hat wird es einsetzen, weil er es beherrscht und weil es ihm Spaß macht. Augenmaß ist hier sicherlich ein wichtiger Aspekt, aber auch die Fähigkeit zuzugeben wann „genug“ ist.

nsteenbock hat gesagt…

Hm,
also mit diesem Standpunkt "Refactoring vor Implementierung" kann ich irgendwie nicht recht warm werden.

Dazu müsste man ja schon ein paar Annahmen machen:
1. Das Refactoring des bestehenden Codes dauert auch nach x Monaten genauso lange wie direkt nach dessen Implementierung.
2. Bestenfalls ist immer derselbe Entwickler am gleichen Code beschäftigt(fremden Code zu refaktorieren ist sicherlich weniger einfach als den eigenen).
3. Beim Kunden treten keine Fehler auf. Spätestens dann profitiere ich direkt vom "verfrühten Refactoring".

Klar, wenn man mit einer Codebasis arbeitet, die schon 90% clean ist und die Stories mit TDD entwickelt, mag das verschieben des Refactorings funktionieren bzw. nicht ganz so tragisch sein. Es aber pauschal vor die Implementierung zu setzen halte ich für keine gute Idee.

Sicherlich muss man nicht direkt arichtektionische Refactorings vornehmen, um erweiterbar zu machen, was nie erweitert wird. Aber ein mindestmass an innerer Qualität sollte m.E. nach beim Verlassen der Baustelle schon hergestellt werden.

Ralf Westphal - One Man Think Tank hat gesagt…

@nsteenbock: Dass du beim "Pre-factoring" ;-) ein schlechtes Gefühl hast, verstehe ich. Es ist ein ungewohnter Gedanke. Aber ich glaube, am Ende bist du gar nicht weit davon entfernt.

Zu deinen Punkten:

1. Refactoring dauert natürlich nicht immer gleich lang. Aber gerade deshalb sollte eben nur soviel refaktorisiert werden, wie nötig. Wieviel das ist, ist nur unmittelbar vor (!) einer Implementation klar. Nach einer Impl. fehlt der Maßstab und es droht Refaktorisierung bis zur Perfektion. Denn wann ist man denn wirklich fertig?

2. Refaktorisierungen profitieren davon, wenn sie eben nicht vom Urheber des riechenden Codes durchgeführt werden. Denn wenn der Mühe hat, sich einzuarbeiten, dann ist es gerade angezeigt zu refaktorisieren. Auch hier weiß dieser Entwickler aber erst bei anstehender Aufgabe, wieviel da wirklich nötig ist.

3. Eine Refaktorisierung behebt keine Fehler. Insofern profitiert man "beim Kunden" davon nicht.

Unit Tests einbauen während der Implementation, ist kein Refactoring.

Insbesondere bei Brownfield-Anwendungen ist es doch sonnenklar, dass eine Refaktorisierung vor (!) weiterer Entwicklung von Features stattfinden muss.

Auch bei der von mir vorgeschlagenen Schrittfolge mit Refaktorisierungen vor (!) der Implementierung steht am Ende kein verquarztes System. Bitte schau nochmal genau hin: Vor der Impl. wird refaktorisiert, was nötig ist, um sie leicht beginnen zu können. Dann wird aber auch während (!) der Impl immer wieder refaktorisiert: refactor-red-green...

Am Ende kommt also ein System raus, bei dem quasi nur ein relativ kleines Abschlussrefactoring fehlt. Aber das halte ich gerade für wichtig, weil diese kleine Refaktorisierung nicht durch eine folgende Impl. ausbalanciert ist. Sie droht daher nicht klein zu bleiben, sondern sich zu einem Rundumschlag auszuweiten.

Deshalb Refaktorisierung vor die Implementation stellen.

-Ralf

Thomas Vidic hat gesagt…

Hallo,

zur Zeit versuche ich das folgende:
... - refactor red green refactor - refactor red green refactor - ..., wobei das "refactor" vor dem "red" den nächsten Test bzw den Einbau des nächsten Features erleichern soll, wärend das "refactor" nach dem "green" die eben verursachte Unordnung aufräumen soll.

Dabei ist aber zu beachten, dass ich ziemlich viel Fake-it, Hacks usw. benutze, um möglichst schnell auf "green" zu kommen und dabei eine ganze Menge duplizierter Code entsteht... Die beiden "refactor"-Phasen scheinen also eine unterschiedliche Qualität zu haben. Aber richtig zu Ende reflektiert habe ich mein Vorgehen noch nicht, werde daher dein YAGNI für Refactorings mal im Kopf behalten... :-)

Grüße,
Thomas

nsteenbock hat gesagt…

@Ralf

3. Eine Refaktorisierung behebt keine Fehler. Insofern profitiert man "beim Kunden" davon nicht.

Natürlich behebt man durch Refaktorieren keine Fehler. Man kann jedoch durch gut strukturierten Code die Zeit zum Auffinden und Beheben der Fehler teilweise drastisch reduzieren.

Insbesondere bei Brownfield-Anwendungen ist es doch sonnenklar, dass eine Refaktorisierung vor (!) weiterer Entwicklung von Features stattfinden muss.

Richtig. Wenn ich ein neues Feature implementiere, sehe ich mir zuerst den bestehenden Code an. Bei einer Methode mit beispielsweise 200 Zeilen versuche ich erst einmal einzelne Blöcke zu bilden und daraus Methoden zu extrahieren.
An dem Punkt habe ich durch ganz einfache Refaktorierung meist eine Methode, die nahzu 1:1 einem Struktogramm oder PAP gleicht und aus 4 oder 5 Methodenaufrufen besteht.
Mit etwas Glück greift mein neueres Feature schon in dieser obersten Methode und ich muss nicht gleich jede der neuen Methode bis ins kleinste weiter zu refaktorieren - oder würdest Du bzw. man das an diesem Punkt sowieso machen?

Jetzt stehe ich da und habe vor meiner Implementierung ein Methode quasi "struktogrammisiert", fehlende Funktionalitäten ergänzt und lasse u.U. etwas zurück, was sich wieder nur halb zu einem Struktogramm umsetzen lässt.

Vielleicht verdeutlicht das ein wenig mein Problem mit "kein Refactoring nach dem letzten Grün".

Man muss denke ich auch wieder ein wenig zwischen den verschiedenen Arten von Refactorings unterscheiden.

Grüsse,

Nils

P.S.: Thomas Ansatz geht da schon mehr in eine mir angenehme Richtung :)