Ich habe ein Projekt. In diesem Projekt wollte ich es umgestalten, um ein Feature hinzuzufügen, und ich habe das Projekt umgestaltet, um das Feature hinzuzufügen.
Das Problem ist, dass es sich herausstellte, als ich fertig war, dass ich eine geringfügige Änderung an der Benutzeroberfläche vornehmen musste, um sie aufzunehmen. Also habe ich die Änderung vorgenommen. Und dann kann die konsumierende Klasse nicht mit ihrer aktuellen Schnittstelle in Bezug auf die neue implementiert werden, weshalb sie auch eine neue Schnittstelle benötigt. Jetzt ist es drei Monate später, und ich musste unzählige Probleme beheben, die praktisch nichts miteinander zu tun haben, und ich bin auf der Suche nach Lösungen für Probleme, die in einem Jahr geplant wurden oder die aufgrund von Schwierigkeiten nicht behoben werden können, bevor die Sache kompiliert werden kann nochmal.
Wie kann ich diese Art von kaskadierenden Refactorings in Zukunft vermeiden? Ist es nur ein Symptom für meine früheren Klassen, die zu eng voneinander abhängen?
Kurze edit: In diesem Fall ist das Refactoring war das Merkmal, da das Umgestalten der Dehnbarkeit eines bestimmten Stückes Code erhöht und eine Kopplung verringert. Dies bedeutete, dass externe Entwickler mehr tun konnten, was die Funktion war, die ich liefern wollte. Der ursprüngliche Refactor selbst sollte also keine funktionale Änderung sein.
Größere Änderung, die ich vor fünf Tagen versprochen habe:
Bevor ich mit diesem Refactor anfing, hatte ich ein System mit einer Schnittstelle, aber bei der Implementierung habe ich einfach dynamic_cast
alle möglichen Implementierungen durchgearbeitet, die ich ausgeliefert habe. Dies bedeutete offensichtlich, dass Sie zum einen nicht einfach von der Schnittstelle erben konnten und zum anderen, dass es für niemanden ohne Implementierungszugriff möglich wäre, diese Schnittstelle zu implementieren. Deshalb habe ich beschlossen, dieses Problem zu beheben und die Schnittstelle für den öffentlichen Verbrauch zu öffnen, damit jeder sie implementieren kann, und die Implementierung der Schnittstelle war der gesamte erforderliche Vertrag - offensichtlich eine Verbesserung.
Als ich alle Orte gefunden und mit Feuer getötet hatte, an denen ich dies getan hatte, fand ich einen Ort, der sich als besonderes Problem herausstellte. Es hing von den Implementierungsdetails aller verschiedenen Ableitungsklassen und der duplizierten Funktionalität ab, die bereits implementiert waren, aber an einer anderen Stelle besser. Es hätte stattdessen in Form der öffentlichen Schnittstelle implementiert und die vorhandene Implementierung dieser Funktionalität wiederverwendet werden können. Ich entdeckte, dass es eines bestimmten Kontextes bedurfte, um richtig zu funktionieren. Grob gesagt sah die aufrufende vorherige Implementierung irgendwie so aus
for(auto&& a : as) {
f(a);
}
Um diesen Kontext zu erhalten, musste ich ihn jedoch in etwas Ähnliches ändern
std::vector<Context> contexts;
for(auto&& a : as)
contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
f(con);
Dies bedeutet, dass für alle Vorgänge, zu denen früher ein Teil gehörte f
, einige von ihnen zu einem Teil der neuen Funktion gemacht werden müssen g
, die ohne Kontext ausgeführt wird, und einige von ihnen zu einem Teil der jetzt zurückgestellten f
. Aber nicht alle Methoden f
benötigen oder wollen diesen Kontext - einige von ihnen benötigen einen bestimmten Kontext, den sie auf unterschiedliche Weise erhalten. Also musste ich für alles, was f
letztendlich aufruft (was grob gesagt so ziemlich alles ist ), bestimmen, welchen Kontext sie brauchten, woher sie ihn nehmen sollten und wie sie von alt f
nach neu f
und von neu zu trennen waren g
.
Und so bin ich dort gelandet, wo ich jetzt bin. Der einzige Grund, warum ich so weitermachte, war, dass ich dieses Refactoring sowieso aus anderen Gründen brauchte.
Antworten:
Als ich das letzte Mal versuchte, ein Refactoring mit unvorhergesehenen Konsequenzen zu starten und den Build und / oder die Tests nach einem Tag nicht mehr stabilisieren konnte , gab ich auf und stellte die Codebasis auf den Punkt vor dem Refactoring zurück.
Dann begann ich zu analysieren, was schief gelaufen war, und entwickelte einen besseren Plan, um das Refactoring in kleineren Schritten durchzuführen. Mein Ratschlag zur Vermeidung von kaskadierenden Refactorings lautet daher: Weiß, wann aufzuhören ist , und lass die Dinge nicht außer Kontrolle geraten!
Manchmal muss man in die Kugel beißen und einen ganzen Arbeitstag wegwerfen - auf jeden Fall einfacher als drei Monate Arbeit wegzuwerfen. Der Tag, an dem Sie verlieren, ist nicht ganz umsonst, zumindest haben Sie gelernt , das Problem nicht anzugehen. Und meiner Erfahrung nach gibt es immer Möglichkeiten, kleinere Schritte beim Refactoring zu machen.
Randnotiz : Sie scheinen sich in einer Situation zu befinden, in der Sie entscheiden müssen, ob Sie bereit sind, volle drei Monate Arbeit zu opfern und mit einem neuen (und hoffentlich erfolgreicheren) Refactoring-Plan von vorne zu beginnen. Ich kann mir vorstellen, dass dies keine einfache Entscheidung ist, aber fragen Sie sich, wie hoch das Risiko ist, dass Sie weitere drei Monate benötigen, um nicht nur den Build zu stabilisieren, sondern auch um alle unvorhergesehenen Fehler zu beheben, die Sie wahrscheinlich während Ihres Umschreibens in den letzten drei Monaten verursacht haben ? Ich habe "rewrite" geschrieben, weil ich denke, das ist, was Sie wirklich getan haben, kein "Refactoring". Es ist nicht unwahrscheinlich, dass Sie Ihr aktuelles Problem schneller lösen können, indem Sie zu der letzten Revision zurückkehren, in der Ihr Projekt kompiliert wurde, und mit einem echten Refactoring beginnen (im Gegensatz zu "Rewrite").
quelle
Sicher. Eine Änderung, die unzählige andere Änderungen hervorruft, ist so ziemlich die Definition der Kopplung.
In der schlimmsten Art von Codebasen wird eine einzelne Änderung weiterhin kaskadiert, was schließlich dazu führt, dass Sie (fast) alles ändern. Ein Teil eines Refaktors, bei dem eine weit verbreitete Kopplung besteht, besteht darin, den Teil zu isolieren, an dem Sie arbeiten. Sie müssen nicht nur umgestalten, wo Ihre neue Funktion diesen Code berührt, sondern wo alles andere diesen Code berührt.
Normalerweise bedeutet dies, dass einige Adapter verwendet werden, damit der alte Code mit etwas funktioniert, das wie der alte Code aussieht und funktioniert, aber die neue Implementierung / Schnittstelle verwendet. Wenn Sie nur die Schnittstelle / Implementierung ändern, aber die Kopplung verlassen, erhalten Sie schließlich nichts. Es ist Lippenstift auf einem Schwein.
quelle
Es hörte sich so an, als wäre Ihr Refactoring zu ehrgeizig. Ein Refactoring sollte in kleinen Schritten durchgeführt werden, von denen jeder in (etwa) 30 Minuten - oder im schlimmsten Fall höchstens einem Tag - abgeschlossen werden kann. Das Projekt kann dann noch erstellt werden, und alle Tests sind noch nicht abgeschlossen.
Wenn Sie jede einzelne Änderung auf ein Minimum beschränken, sollte es für ein Refactoring nicht möglich sein, Ihren Build für längere Zeit zu brechen. Der schlimmste Fall ist wahrscheinlich das Ändern der Parameter in eine Methode in einer weit verbreiteten Schnittstelle, z. B. um einen neuen Parameter hinzuzufügen. Die daraus resultierenden Änderungen sind jedoch mechanisch: Hinzufügen (und Ignorieren) des Parameters in jeder Implementierung und Hinzufügen eines Standardwerts in jedem Aufruf. Selbst wenn es Hunderte von Referenzen gibt, sollte es nicht einmal einen Tag dauern, um ein solches Refactoring durchzuführen.
quelle
Wunschdenken Design
Das Ziel ist ein exzellentes OO-Design und die Implementierung des neuen Features. Das Vermeiden von Refactoring ist ebenfalls ein Ziel.
Beginnen Sie bei Null und entwerfen Sie ein Design für die neue Funktion , die Sie sich gewünscht haben. Nehmen Sie sich Zeit, es gut zu machen.
Beachten Sie jedoch, dass der Schlüssel hier "eine Funktion hinzufügen" ist. Neue Sachen neigen dazu, die aktuelle Struktur der Codebasis weitgehend zu ignorieren. Unser Wunschdenken Design ist unabhängig. Dann brauchen wir aber noch zwei Dinge:
Heuristiken, Lektionen gelernt, etc.
Refactoring war so einfach wie das Hinzufügen eines Standardparameters zu einem vorhandenen Methodenaufruf. oder ein einzelner Aufruf einer statischen Klassenmethode.
Erweiterungsmethoden für vorhandene Klassen können dazu beitragen, die Qualität des neuen Designs bei einem absolut minimalen Risiko zu erhalten.
"Struktur" ist alles. Struktur ist die Verwirklichung des Einzelverantwortungsprinzips; Design, das Funktionalität erleichtert. Code bleibt in der gesamten Klassenhierarchie kurz und einfach. Zeit für neues Design wird beim Testen, Überarbeiten und Vermeiden von Hackerangriffen durch den alten Code-Dschungel aufgewendet.
Wunschdenken Klassen konzentrieren sich auf die jeweilige Aufgabe. Vergessen Sie im Allgemeinen, eine vorhandene Klasse zu erweitern - Sie lösen nur die Refactorkaskade erneut aus und müssen sich mit dem Overhead der "schwereren" Klasse befassen.
Entfernen Sie alle Reste dieser neuen Funktionalität aus dem vorhandenen Code. Hier ist eine vollständige und gut gekapselte Funktionalität neuer Features wichtiger als die Vermeidung von Refactoring.
quelle
Aus dem (wunderbaren) Buch Working Effectively with Legacy Code von Michael Feathers :
quelle
Es hört sich so an, als hätten Sie sich (insbesondere aufgrund der Diskussionen in Kommentaren) selbst mit selbst auferlegten Regeln konfrontiert, was bedeutet, dass diese "geringfügige" Änderung den gleichen Arbeitsaufwand bedeutet wie ein vollständiges Umschreiben der Software.
Die Lösung muss lauten: "Dann tu das nicht" . Das passiert in echten Projekten. Viele alte APIs haben hässliche Schnittstellen oder aufgegebene (immer null) Parameter oder Funktionen mit dem Namen DoThisThing2 (), die dasselbe tun wie DoThisThing () mit einer völlig anderen Parameterliste. Andere häufige Tricks sind das Speichern von Informationen in Globals oder das Markieren von Zeigern, um sie an einem großen Teil des Frameworks vorbeizuschleusen. (Zum Beispiel habe ich ein Projekt, in dem die Hälfte der Audiopuffer nur einen magischen 4-Byte-Wert enthält, da dies viel einfacher war, als zu ändern, wie eine Bibliothek ihre Audiocodecs aufrief.)
Ohne spezifischen Code ist es schwierig, spezifische Ratschläge zu geben.
quelle
Automatisierte Tests. Sie müssen kein TDD-Fanatiker sein und benötigen keine 100-prozentige Abdeckung. Mit automatisierten Tests können Sie jedoch sicher Änderungen vornehmen. Außerdem klingt es so, als hätten Sie ein Design mit sehr hoher Kopplung. Lesen Sie mehr über die SOLID-Prinzipien, die speziell für diese Art von Problemen im Software-Design formuliert wurden.
Ich würde diese Bücher auch empfehlen.
quelle
Höchstwahrscheinlich ja. Obwohl Sie ähnliche Effekte mit einer ziemlich netten und sauberen Codebasis erzielen können, wenn sich die Anforderungen genug ändern
Abgesehen davon, dass Sie aufhören, an Legacy-Code zu arbeiten, haben Sie keine Angst. Sie können jedoch eine Methode verwenden, die den Effekt vermeidet, dass für Tage, Wochen oder sogar Monate keine Arbeitscodebasis vorhanden ist.
Diese Methode heißt "Mikado-Methode" und funktioniert folgendermaßen:
Schreiben Sie das gewünschte Ziel auf ein Blatt Papier
Nehmen Sie die einfachste Änderung vor, die Sie in diese Richtung führt.
Überprüfen Sie, ob es mit dem Compiler und Ihrer Testsuite funktioniert. Fahren Sie andernfalls mit Schritt 4 fort.
Notieren Sie auf Ihrem Papier die Dinge, die geändert werden müssen, damit Ihre aktuelle Änderung funktioniert. Zeichne Pfeile von deiner aktuellen Aufgabe zu den neuen.
Machen Sie Ihre Änderungen rückgängig Dies ist der wichtige Schritt. Es ist kontraintuitiv und tut am Anfang körperlich weh, aber da Sie nur eine einfache Sache ausprobiert haben, ist es eigentlich nicht so schlimm.
Wählen Sie eine der Aufgaben aus, die keine ausgehenden Fehler aufweist (keine bekannten Abhängigkeiten), und kehren Sie zu 2 zurück.
Übernehmen Sie die Änderung, streichen Sie die Aufgabe auf dem Papier durch, wählen Sie eine Aufgabe aus, die keine ausgehenden Fehler enthält (keine bekannten Abhängigkeiten), und kehren Sie zu 2 zurück.
Auf diese Weise erhalten Sie in kurzen Abständen eine funktionierende Codebasis. Hier können Sie auch Änderungen aus dem Rest des Teams zusammenführen. Und Sie haben eine visuelle Darstellung dessen, was Sie wissen, was Sie noch zu tun haben. Dies hilft bei der Entscheidung, ob Sie mit dem Endevour fortfahren möchten oder ob Sie es beenden sollten.
quelle
Refactoring ist eine strukturierte Disziplin, die sich von der Bereinigung von Code nach Belieben unterscheidet. Vor dem Start müssen Unit-Tests geschrieben werden, und jeder Schritt sollte aus einer bestimmten Transformation bestehen, von der Sie wissen, dass sie keine Änderungen an der Funktionalität vornehmen sollte. Die Komponententests sollten nach jeder Änderung bestanden werden.
Natürlich werden Sie während des Umgestaltungsprozesses feststellen, dass Änderungen vorgenommen werden müssen, die zu Beschädigungen führen können. Versuchen Sie in diesem Fall, ein Kompatibilitäts-Shim für die alte Schnittstelle zu implementieren, die das neue Framework verwendet. Theoretisch sollte das System weiterhin wie bisher funktionieren und die Komponententests sollten bestanden werden. Sie können das Kompatibilitäts-Shim als veraltete Schnittstelle markieren und zu einem geeigneteren Zeitpunkt bereinigen.
quelle
Wie @Jules sagte, sind Refactoring und das Hinzufügen von Funktionen zwei sehr unterschiedliche Dinge.
... aber in der Tat müssen Sie manchmal das Innenleben ändern, um Ihre Inhalte hinzuzufügen, aber ich würde es eher als Modifizieren als als Umgestalten bezeichnen.
Dort wird es chaotisch. Schnittstellen sind als Grenzen gedacht, um die Implementierung von ihrer Verwendung zu isolieren. Sobald Sie Schnittstellen berühren, muss auch alles auf beiden Seiten (Implementierung oder Verwendung) geändert werden. Dies kann sich so weit ausbreiten, wie Sie es erlebt haben.
Dass eine Schnittstelle eine Änderung erfordert, hört sich gut an ... dass sie sich auf eine andere Schnittstelle ausbreitet, bedeutet, dass sich die Änderungen noch weiter ausbreiten. Es hört sich so an, als ob irgendeine Form von Eingabe / Daten erforderlich wäre, um die Kette entlang zu fließen. Ist das der Fall?
Ihr Vortrag ist sehr abstrakt, daher ist es schwierig, dies herauszufinden. Ein Beispiel wäre sehr hilfreich. Normalerweise sollten die Schnittstellen ziemlich stabil und unabhängig voneinander sein, so dass es möglich ist, einen Teil des Systems zu ändern, ohne den Rest zu beschädigen ... dank der Schnittstellen.
... eigentlich sind gerade gute Schnittstellen der beste Weg, um kaskadierende Code-Änderungen zu vermeiden . ;)
quelle
Ich denke, Sie können es normalerweise nicht, wenn Sie nicht bereit sind, die Dinge so zu belassen, wie sie sind. In Situationen wie Ihrer ist es jedoch meiner Meinung nach besser, das Team zu informieren und ihnen mitzuteilen, warum einige Umgestaltungen vorgenommen werden sollten, um eine gesunde Entwicklung fortzusetzen. Ich würde nicht einfach alles selbst reparieren. Ich würde darüber in Scrum-Meetings sprechen (vorausgesetzt, ihr habt sie) und mich systematisch mit anderen Entwicklern zusammensetzen.
quelle