„Ich habe schlaflose Nächte gehabt, als ich versucht habe, Funktionen in den Code einzufügen, den wir von einem anderen Unternehmen erworben haben. Ich habe es mit der reinsten Form von Legacy Code zu tun.“
„Es fällt mir sehr schwer, mit verworrenem, unstrukturiertem Code umzugehen, mit dem ich arbeiten muss, den ich aber kein bisschen verstehe. Legacy Code !“
Legacy Code ist ein Begriff, für den es wahrscheinlich viele verschiedene Definitionen gibt, wie z.B. Code, der von jemand anderem erworben wurde, Code, der von jemand anderem geschrieben wurde, Code, der schwer zu verstehen ist oder Code, der mit veralteten Technologien geschrieben wurde. Was auch immer die Definition sein mag, die meisten von uns glauben, dass Legacy Code beängstigend ist.
Frage> Wie würden Sie Legacy Code definieren?
Defining Legacy Code
Michael Feathers definiert in seinem Buch „Working Effectively with Legacy Code“ Legacy Code als Code ohne Tests.
Code ohne Tests ist ein schlechter Code. Es spielt keine Rolle, wie gut er geschrieben ist, wie gut er strukturiert ist, wie gut er gekapselt ist.
Ohne Tests gibt es keine Möglichkeit festzustellen, ob unser Code besser oder schlechter wird.
Nun, eine leicht abgewandelte Version dieser Definition lautet: „Code ohne Unit-Tests wird Legacy-Code genannt“. Es ist immer besser, Tests so nah am Code wie möglich zu haben (Unit-Tests > Integrationstests > UI-Tests). Es wäre also nicht unfair, einen Code ohne Unit-Tests als Legacy-Code zu bezeichnen.
Arbeiten mit Legacy-Code
Frage> Welchen Ansatz würden Sie wählen, wenn Sie eine Änderung am Legacy-Code vornehmen würden?
Die meisten von uns würden vielleicht sagen: „Ich nehme die Änderung vor und lasse es gut sein, warum sich um die Verbesserung des Codes kümmern“. Die Begründung für diesen Gedankengang könnte sein:
- Ich habe nicht genug Zeit, um den Code zu überarbeiten, Ich würde es vorziehen, eine Änderung vorzunehmen und meine Geschichte abzuschließen
- Warum sollte ich riskieren, die Struktur des Codes zu ändern, der schon seit langem in der Produktion läuft
- Was ist der Gesamtnutzen des Refactoring von Legacy-Code
Michael Feathers nennt diese Art, eine Änderung vorzunehmen, Edit and Pray. Man plant und macht seine Änderungen und wenn man fertig ist, betet man noch mehr, um die Änderungen richtig hinzubekommen.
Mit diesem Stil kann man nur dazu beitragen, den Legacy-Code zu vergrößern.
Es gibt einen anderen Stil, Änderungen vorzunehmen, nämlich Cover and Modify. Bauen Sie ein Sicherheitsnetz auf, nehmen Sie Änderungen am System vor, lassen Sie das Sicherheitsnetz Feedback geben und arbeiten Sie an diesen Rückmeldungen.
Es kann mit Sicherheit davon ausgegangen werden, dass „Cover and Modify“ der richtige Weg ist, um mit Legacy-Code umzugehen.
Frage> Aber sollte man überhaupt Zeit damit verbringen, Tests in Legacy-Code zu schreiben oder überhaupt darüber nachzudenken, einen Legacy-Code zu refaktorisieren?
Die Pfadfinderregel
Die Idee hinter der Pfadfinderregel, wie sie von Onkel Bob formuliert wurde, ist ziemlich einfach: Hinterlasse den Code sauberer, als du ihn vorgefunden hast! Wann immer Sie einen alten Code anfassen, sollten Sie ihn ordentlich reinigen. Wenden Sie nicht einfach eine Abkürzungslösung an, die den Code schwerer verständlich macht, sondern behandeln Sie ihn mit Sorgfalt. Es reicht nicht aus, den Code gut zu schreiben, er muss auch im Laufe der Zeit sauber gehalten werden.
Wenn die Pfadfinderregel auf alten Code angewandt wird, erhalten wir eine sehr starke Botschaft: „Hinterlasse eine Spur des Verständnisses, damit andere ihr folgen können“, was bedeutet, dass wir den Code refaktorisieren werden, um ihn verständlicher zu machen. Und um zu refaktorisieren, werden wir ein Sicherheitsnetz um ihn herum aufbauen.
Nun, da wir verstehen, dass wir keine Abkürzungen nehmen können, ist die einzige Option, die uns bleibt, einige Tests zu schreiben, Code zu refaktorisieren und mit der Entwicklung fortzufahren. Fragen>
- Welche Tests sollen wir schreiben?
- Wie viel sollen wir refaktorisieren?
Welche Tests sollen wir schreiben
In fast jedem Altsystem ist das, was das System tut, wichtiger als das, was es tun soll.
Charakterisierungstests, die Tests, die wir brauchen, wenn wir das Verhalten erhalten wollen, werden als Charakterisierungstests bezeichnet. Ein Charakterisierungstest ist ein Test, der das tatsächliche Verhalten eines Codestücks charakterisiert. Es gibt kein „Nun, es sollte dies tun“ oder „Ich denke, es tut dies“. Die Tests dokumentieren das tatsächliche aktuelle Verhalten des Systems.
Schreiben von Charakterisierungstests
Ein Charakterisierungstest dokumentiert per Definition das tatsächliche aktuelle Verhalten des Systems genau so, wie es in der Produktionsumgebung läuft.
Lassen Sie uns einen Charakterisierungstest für ein Kundenobjekt schreiben, das Textanweisungen für einige von einem Kunden ausgeliehene Filme generiert.
import static com.code.legacy.movie.MovieType.CHILDREN;
import static org.junit.Assert.assertEquals;public void shouldGenerateTextStatement(){ Customer john = new Customer("John");
Movie childrenMovie = new Movie("Toy Story", CHILDREN);
int daysRented = 3;
Rental rental = new Rental(childrenMovie, daysRented); john.addRental(rental); String statement = john.generateTextStatement();
assertEquals("", statement);
}
Dieser Test versucht, die Generierung von „Textanweisungen“ für einen Kunden zu verstehen (oder zu charakterisieren), der einen Kinderfilm für 3 Tage ausgeliehen hat. Da wir das System nicht verstehen (zumindest bis jetzt), erwarten wir, dass die Anweisung leer ist oder einen beliebigen Dummy-Wert enthält.
Lassen wir den Test laufen und lassen ihn fehlschlagen. Wenn er fehlschlägt, haben wir herausgefunden, was der Code unter dieser Bedingung tatsächlich tut.
java.lang.AssertionError:
Expected :""
Actual :Rental Record for John, Total amount owed = 12.5. You earned 4 frequent renter points.
Nun, da wir das Verhalten des Codes kennen, können wir weitermachen und den Test ändern.
import static com.code.legacy.movie.MovieType.CHILDREN;
import static org.junit.Assert.assertEquals;public void shouldGenerateTextStatement(){
String expectedStatement = "Rental Record for John, Total amount owed = 12.5. You earned 4 frequent renter points"; Customer john = new Customer("John");
Movie childrenMovie = new Movie("Toy Story", CHILDREN);
int daysRented = 3;
Rental rental = new Rental(childrenMovie, daysRented);
john.addRental(rental); Sting statement = john.generateTextStatement();
assertEquals(expectedStatement, statement);
}
Warten Sie, haben wir gerade die vom Code erzeugte Ausgabe kopiert und in unseren Test eingefügt. Ja, das ist genau das, was wir getan haben.
Wir versuchen jetzt nicht, Bugs zu finden. Wir versuchen, einen Mechanismus einzubauen, um später Fehler zu finden, Fehler, die sich als Unterschiede zum aktuellen Verhalten des Systems zeigen. Wenn wir diese Perspektive einnehmen, haben wir einen anderen Blick auf Tests: Sie haben keine moralische Autorität; sie sitzen nur da und dokumentieren, was das System wirklich tut. In diesem Stadium ist es sehr wichtig, das Wissen darüber, was das System tatsächlich tut, irgendwo zu haben.
Frage> Wie viele Tests können wir insgesamt schreiben, um ein System zu charakterisieren?
Antwort> Es sind unendlich viele. Wir könnten einen guten Teil unseres Lebens damit verbringen, Fall für Fall für jede Klasse in einem Legacy-Code zu schreiben.
Frage> Wann hören wir dann auf? Gibt es eine Möglichkeit zu wissen, welche Fälle wichtiger sind als andere?
Antwort> Sehen Sie sich den Code an, den wir charakterisieren. Der Code selbst kann uns Ideen darüber geben, was er tut, und wenn wir Fragen haben, sind Tests eine ideale Möglichkeit, sie zu stellen. An diesem Punkt schreiben wir einen oder mehrere Tests, die einen ausreichenden Teil des Codes abdecken.
Frage> Deckt das alles im Code ab?
Antwort> Vielleicht nicht. Aber dann machen wir den nächsten Schritt. Wir denken über die Änderungen nach, die wir am Code vornehmen wollen, und versuchen herauszufinden, ob die Tests, die wir haben, alle Probleme erkennen, die wir verursachen können. Wenn das nicht der Fall ist, fügen wir weitere Tests hinzu, bis wir sicher sind, dass sie es tun.
Wie viel ist zu refaktorisieren?
Es gibt so viel zu refaktorisieren in altem Code und wir können nicht alles refaktorisieren.
Wir wollen den Legacy-Code so umgestalten, dass er sauberer ist als das, was er war, als er zu uns kam, und dass er für andere verständlich ist.
Damit wollen wir das System verbessern und uns auf die Aufgabe konzentrieren. Wir wollen uns nicht mit Refactoring verrückt machen und versuchen, das ganze System in ein paar Tagen neu zu schreiben. Was wir tun wollen, ist „den Code, der uns bei der Implementierung neuer Änderungen im Weg steht, zu refaktorisieren“. Wir werden versuchen, dies im nächsten Artikel anhand eines Beispiels besser zu verstehen.