Was würde helfen, wenn eine große Methode überarbeitet wird, um sicherzustellen, dass ich nichts kaputt mache?

10

Ich überarbeite derzeit einen Teil einer großen Codebasis ohne Unit-Tests. Ich habe versucht, Code auf brutale Weise umzugestalten, dh indem ich versucht habe zu erraten, was der Code tut und welche Änderungen seine Bedeutung nicht ändern würden, aber ohne Erfolg: Es werden zufällig Funktionen rund um die Codebasis unterbrochen.

Beachten Sie, dass das Refactoring das Verschieben von Legacy-C # -Code in einen funktionaleren Stil umfasst (der Legacy-Code verwendet keine der Funktionen von .NET Framework 3 und höher, einschließlich LINQ), das Hinzufügen von Generika, bei denen der Code davon profitieren kann usw.

Ich kann keine formalen Methoden anwenden, wenn man bedenkt, wie viel sie kosten würden.

Andererseits gehe ich davon aus, dass mindestens die Regel "Jeder überarbeitete Legacy-Code muss mit Komponententests geliefert werden" strikt befolgt werden sollte, unabhängig davon, wie viel es kosten würde. Das Problem ist, dass das Hinzufügen von Komponententests eine schwierige Aufgabe ist, wenn ich einen winzigen Teil einer privaten 500-LOC-Methode umgestalte.

Was kann mir helfen zu wissen, welche Komponententests für einen bestimmten Code relevant sind? Ich vermute, dass eine statische Analyse des Codes irgendwie hilfreich wäre, aber mit welchen Tools und Techniken kann ich Folgendes tun:

  • Wissen Sie genau, welche Unit-Tests ich erstellen soll,

  • Und / oder wissen Sie, ob die von mir vorgenommene Änderung den ursprünglichen Code so beeinflusst hat, dass er anders ausgeführt wird als jetzt?

Arseni Mourzenko
quelle
Was ist Ihre Argumentation, dass das Schreiben von Unit-Tests die Zeit für dieses Projekt verlängern wird? Viele Befürworter würden dem nicht zustimmen, aber es hängt auch von Ihrer Fähigkeit ab, sie zu schreiben.
JeffO
Ich sage nicht, dass dies die Gesamtzeit für das Projekt verlängern wird. Was ich sagen wollte, ist, dass es die Zeit kurzfristig verlängern wird (dh die unmittelbare Zeit, die ich gerade beim Umgestalten des Codes verbringe ).
Arseni Mourzenko
1
Sie würden es formal methods in software developmentsowieso nicht verwenden wollen, da es verwendet wird, um die Richtigkeit eines Programms mithilfe von Prädikatenlogik zu beweisen, und nicht für die Umgestaltung einer großen Codebasis anwendbar wäre. Formale Methoden, die normalerweise zum Nachweis von Code verwendet werden, funktionieren in Bereichen wie medizinischen Anwendungen korrekt. Sie haben Recht, es ist teuer zu tun, weshalb es nicht oft verwendet wird.
Mushy
Ein gutes Tool wie die Refactor-Optionen in ReSharper erleichtern eine solche Aufgabe erheblich. In solchen Situationen ist es das Geld wert .
Billy.Bob
1
Keine vollständige Antwort, aber eine blöde Technik, die ich überraschend effektiv finde, wenn alle anderen Refactoring-Techniken versagen: Erstellen Sie eine neue Klasse, teilen Sie die Funktion in separate Funktionen mit genau dem Code auf, der bereits vorhanden ist, nur alle 50 Zeilen unterbrochen, fördern Sie eine beliebige Einheimische, die funktionsübergreifend mit Mitgliedern geteilt werden, dann passen die einzelnen Funktionen besser in meinen Kopf und geben mir die Möglichkeit, in Mitgliedern zu sehen, welche Teile durch die gesamte Logik gezogen werden. Dies ist kein Endziel, sondern nur ein sicherer Weg, um ein altes Chaos für ein sicheres Refactoring vorzubereiten.
Jimmy Hoffa

Antworten:

12

Ich hatte ähnliche Herausforderungen. Das Buch Arbeiten mit Legacy-Code ist eine großartige Ressource, aber es wird davon ausgegangen, dass Sie in Unit-Tests Schuhhupen verwenden können, um Ihre Arbeit zu unterstützen. Manchmal ist das einfach nicht möglich.

In meiner archäologischen Arbeit (mein Begriff für die Wartung von Legacy-Code wie diesem) verfolge ich einen ähnlichen Ansatz wie den von Ihnen beschriebenen.

  • Beginnen Sie mit einem soliden Verständnis dessen, was die Routine gerade tut.
  • Zur gleichen Zeit, zu identifizieren , was die Routine sollte , zu tun. Viele denken, dass diese und die vorherige Kugel gleich sind, aber es gibt einen subtilen Unterschied. Wenn die Routine das tat, was sie eigentlich tun sollte, würden Sie häufig keine Wartungsänderungen vornehmen.
  • Führen Sie einige Beispiele durch die Routine und stellen Sie sicher, dass Sie die Grenzfälle, relevanten Fehlerpfade und den Hauptlinienpfad treffen. Ich habe die Erfahrung gemacht, dass der Kollateralschaden (Feature Break) durch Randbedingungen verursacht wird, die nicht genau auf die gleiche Weise implementiert werden.
  • Identifizieren Sie nach diesen Beispielfällen, was beibehalten wird, was nicht unbedingt beibehalten werden muss. Wieder habe ich festgestellt, dass es solche Nebenwirkungen sind, die anderswo zu Kollateralschäden führen.

Zu diesem Zeitpunkt sollten Sie eine Kandidatenliste darüber haben, was von dieser Routine freigelegt und / oder manipuliert wurde. Einige dieser Manipulationen sind wahrscheinlich unbeabsichtigt. Jetzt benutze ich findstrund die IDE, um zu verstehen, welche anderen Bereiche auf die Elemente in der Kandidatenliste verweisen könnten. Ich werde einige Zeit damit verbringen zu verstehen, wie diese Referenzen funktionieren und wie sie beschaffen sind.

Sobald ich mich getäuscht habe zu glauben, die Auswirkungen der ursprünglichen Routine zu verstehen, werde ich meine Änderungen einzeln vornehmen und die oben beschriebenen Analyseschritte erneut ausführen, um zu überprüfen, ob die Änderung wie erwartet funktioniert es zu arbeiten. Ich versuche ausdrücklich zu vermeiden, dass mehrere Dinge gleichzeitig geändert werden, da ich festgestellt habe, dass dies mich in die Luft jagt, wenn ich versuche, die Auswirkungen zu überprüfen. Manchmal kann man mit mehreren Änderungen davonkommen, aber wenn ich einer einzelnen Route folgen kann, ist das meine Präferenz.

Kurz gesagt, mein Ansatz ähnelt dem, was Sie dargelegt haben. Es ist viel Vorarbeit; dann nehmen Sie umsichtige, individuelle Änderungen vor; und dann überprüfen, überprüfen, überprüfen.


quelle
2
+1 nur für die Verwendung von "Archäologie". Das ist der gleiche Begriff, den ich benutze, um diese Aktivität zu beschreiben, und ich denke, das ist eine großartige Möglichkeit, es auszudrücken (dachte auch, die Antwort war gut - ich bin nicht wirklich so flach)
Erik Dietrich
10

Was würde helfen, wenn eine große Methode überarbeitet wird, um sicherzustellen, dass ich nichts kaputt mache?

Kurze Antwort: kleine Schritte.

Das Problem ist, dass das Hinzufügen von Komponententests eine schwierige Aufgabe zu sein scheint, wenn ich einen winzigen Teil einer privaten 500-LOC-Methode umgestalte.

Betrachten Sie diese Schritte:

  1. Verschieben Sie die Implementierung in eine andere (private) Funktion und delegieren Sie den Aufruf.

    // old:
    private int ugly500loc(int parameters) {
        // 500 LOC here
    }
    
    // new:    
    private int ugly500loc_old(int parameters) {
        // 500 LOC here
    }
    
    private void ugly500loc(int parameters) {
        return ugly500loc_old(parameters);
    }
    
  2. Fügen Sie in Ihrer ursprünglichen Funktion für alle Ein- und Ausgänge Protokollierungscode hinzu (stellen Sie sicher, dass die Protokollierung nicht fehlschlägt).

    private void ugly500loc(int parameters) {
        static int call_count = 0;
        int current = ++call_count;
        save_to_file(current, parameters);
        int result = ugly500loc_old(parameters);
        save_to_file(current, result); // result, any exceptions, etc.
        return result;
    }
    

    Führen Sie Ihre Anwendung aus und tun Sie alles, was Sie können (gültige Verwendung, ungültige Verwendung, typische Verwendung, atypische Verwendung usw.).

  3. Sie haben jetzt eine max(call_count)Reihe von Ein- und Ausgängen, mit denen Sie Ihre Tests schreiben können. Sie können einen einzelnen Test schreiben , der alle Ihre vorhandenen Parameter / Ergebnismengen durchläuft und diese in einer Schleife ausführt. Sie können auch einen zusätzlichen Test schreiben, der eine bestimmte Kombination ausführt (um schnell zu überprüfen, ob ein bestimmter E / A-Satz übergeben wird).

  4. Kehren Sie // 500 LOC herezu Ihrer ugly500locFunktion zurück (und entfernen Sie die Protokollierungsfunktion).

  5. Beginnen Sie mit dem Extrahieren von Funktionen aus der großen Funktion (tun Sie nichts anderes, extrahieren Sie nur Funktionen) und führen Sie Tests aus. Danach sollten Sie mehr kleine Funktionen zum Refactor haben als die 500LOC.

  6. Lebe glücklich bis ans Ende.

utnapistim
quelle
3

Normalerweise sind Unit Tests der richtige Weg.

Führen Sie die erforderlichen Tests durch, um zu beweisen, dass der Strom wie erwartet funktioniert. Nehmen Sie sich Zeit und der letzte Test muss Sie von der Ausgabe überzeugen.

Was kann mir helfen zu wissen, welche Komponententests für einen bestimmten Code relevant sind?

Sie sind dabei, einen Code zu überarbeiten. Sie müssen genau wissen, was er tut und welche Auswirkungen er hat. Grundsätzlich müssen Sie also alle betroffenen Zonen testen. Dies wird viel Zeit in Anspruch nehmen ... aber das ist das erwartete Ergebnis eines jeden Refactoring-Prozesses.

Dann können Sie alles problemlos auseinander reißen.

AFAIK, dafür gibt es keine kugelsichere Technik ... Sie müssen nur methodisch sein (bei welcher Methode auch immer Sie sich wohl fühlen), viel Zeit und viel Geduld! :) :)

Prost und viel Glück!

Alex

AlexCode
quelle
Tools zur Codeabdeckung sind hier unerlässlich. Es ist schwierig zu bestätigen, dass Sie jeden Pfad durch eine große komplexe Methode durch Inspektion zurückgelegt haben. Ein Tool, das zeigt, dass KitchenSinkMethodTest01 () ... KitchenSinkMethodTest17 () gemeinsam die Zeilen 1-45, 48-220, 245-399 und 488-500 abdeckt, aber den Code dazwischen nicht berührt. Dadurch wird es viel einfacher, herauszufinden, welche zusätzlichen Tests Sie zum Schreiben benötigen.
Dan spielt am Feuer