Dies ist eine verwandte Frage: Ist die Verwendung der finally-Klausel für die Ausführung von Arbeiten nach der Rückkehr ein schlechter Stil / gefährlich?
In dem referenzierten Q bezieht sich der endgültige Code auf die verwendete Struktur und die Notwendigkeit des Vorabrufens. Meine Frage ist etwas anders und ich glaube, dass sie für das breitere Publikum von Bedeutung ist. Mein spezielles Beispiel ist eine C # Winform-App, aber dies würde auch für die endgültige Verwendung von C ++ / Java gelten.
Ich bemerke einige Try-Catch-finally-Blöcke, in denen viel Code enthalten ist, der nichts mit Ausnahmen und Ausnahmebehandlung / Bereinigung zu tun hat. Und ich gebe meine Neigung zu sehr engen Try-Catch-End-Blöcken mit dem Code zu, der eng mit der Ausnahme und der Behandlung zusammenhängt. Hier sind einige Beispiele von dem, was ich sehe.
In Try-Blöcken werden viele vorläufige Aufrufe und Variablen festgelegt, die zu dem Code führen, der ausgelöst werden könnte. Die Protokollinformationen werden eingerichtet und auch im try-Block ausgeführt.
Schließlich erhalten Blöcke Formatierungsaufrufe für Formulare / Module / Steuerelemente (obwohl die App kurz vor dem Beenden steht, wie im catch-Block angegeben) sowie neue Objekte wie Bedienfelder.
Grob:
methodName (...) { Versuchen { // Viel Code für die Methode ... // Code, der werfen könnte ... // Viel mehr Code für die Methode und eine Rückgabe ... }} fangen (etwas) {// Ausnahme behandeln} schließlich { // einige Aufräumarbeiten aufgrund von Ausnahmen, Dinge schließen // mehr Code für das erstellte Material (ignoriert, dass Ausnahmen ausgelöst haben könnten) ... // vielleicht noch ein paar Objekte erstellen }} }}
Der Code funktioniert, daher hat er einen gewissen Wert. Es ist nicht gut gekapselt und die Logik ist etwas verschlungen. Ich bin (schmerzlich) mit den Risiken beim Verschieben von Code sowie beim Refactoring vertraut, daher läuft meine Frage darauf hinaus, die Erfahrungen anderer mit ähnlich strukturiertem Code kennen zu wollen.
Rechtfertigt der schlechte Stil die Änderungen? Wurde jemand aus einer ähnlichen Situation schwer verbrannt? Möchten Sie die Details dieser schlechten Erfahrung mitteilen? Lass es sein, weil ich überreagiere und es nicht so schlecht im Stil ist? Erhalten Sie die Wartungsvorteile beim Aufräumen?
quelle
finally
. Alle guten Verwendungen werden unter RAII / RRID / SBRM (welches Akronym Sie möchten) abgedeckt.finally
in C # funktioniert). Was ist das C ++ - Äquivalent? Was ich denke, ist Code nach demcatch
, und das gilt auch für Code nach dem C #finally
.Environment.FailFast()
. Es kann möglicherweise nicht ausgeführt werden, wenn Sie eine nicht erfasste Ausnahme haben. Und es wird noch komplizierter, wenn Sie einen Iteratorblock haben, mitfinally
dem Sie manuell iterieren.Antworten:
Ich hatte eine sehr ähnliche Situation, als ich mich mit einem schrecklichen alten Windows Forms-Code befassen musste, der von Entwicklern geschrieben wurde, die offensichtlich nicht wussten, was sie taten.
Zuallererst übertreibst du nicht. Das ist schlechter Code. Wie Sie sagten, sollte es beim Fangblock darum gehen, abzubrechen und sich auf den Stopp vorzubereiten. Es ist nicht an der Zeit, Objekte (insbesondere Panels) zu erstellen. Ich kann nicht einmal erklären, warum das schlecht ist.
Davon abgesehen ...
Mein erster Rat ist: Wenn es nicht kaputt ist, fass es nicht an!
Wenn Sie den Code pflegen möchten, müssen Sie Ihr Bestes tun, um ihn nicht zu beschädigen. Ich weiß, dass es schmerzhaft ist (ich war dort), aber Sie müssen Ihr Bestes geben, um nicht zu brechen, was bereits funktioniert.
Mein zweiter Rat ist: Wenn Sie weitere Funktionen hinzufügen müssen, versuchen Sie, die vorhandene Codestruktur so weit wie möglich beizubehalten, damit Sie den Code nicht beschädigen.
Beispiel: Wenn es eine abscheuliche Switch-Case-Anweisung gibt, die Ihrer Meinung nach durch eine ordnungsgemäße Vererbung ersetzt werden könnte, müssen Sie vorsichtig sein und zweimal überlegen, bevor Sie sich entscheiden, Dinge zu verschieben.
Sie werden auf jeden Fall Situationen finden, in denen ein Refactoring der richtige Ansatz ist, aber Vorsicht: Refactoring-Code führt eher zu Fehlern . Sie müssen diese Entscheidung aus Sicht der Anwendungsinhaber treffen, nicht aus Sicht der Entwickler. Sie müssen sich also überlegen, ob der Aufwand (Geld), der zur Behebung des Problems erforderlich ist, ein Refactoring wert ist oder nicht. Ich habe oft gesehen, wie ein Entwickler mehrere Tage damit verbracht hat, etwas zu reparieren, das nicht wirklich kaputt ist, nur weil er "den Code für hässlich hält".
Mein dritter Rat ist: Sie werden sich verbrennen, wenn Sie den Code brechen. Es spielt keine Rolle, ob Sie schuld sind oder nicht.
Wenn Sie mit der Wartung beauftragt wurden, spielt es keine Rolle, ob die Anwendung auseinanderfällt, weil jemand anderes schlechte Entscheidungen getroffen hat. Aus Anwendersicht hat es vorher funktioniert und jetzt haben Sie es kaputt gemacht. Du hast es kaputt gemacht!
Joel erklärt in seinem Artikel sehr gut, warum Sie Legacy-Code nicht neu schreiben sollten.
http://www.joelonsoftware.com/articles/fog0000000069.html
Du solltest dich also wirklich schlecht fühlen über diese Art von Code (und du solltest niemals so etwas schreiben), aber es zu pflegen ist ein ganz anderes Monster.
Über meine Erfahrung: Ich musste den Code ungefähr 1 Jahr lang warten und konnte ihn schließlich von Grund auf neu schreiben, aber nicht auf einmal.
Was passiert ist, ist, dass der Code so schlecht war, dass neue Funktionen nicht implementiert werden konnten. Die vorhandene Anwendung hatte schwerwiegende Leistungs- und Usability-Probleme. Schließlich wurde ich gebeten, eine Änderung vorzunehmen, die 3-4 Monate dauern würde (hauptsächlich, weil die Arbeit mit diesem Code viel mehr Zeit in Anspruch nahm als gewöhnlich). Ich dachte, ich könnte das ganze Stück (einschließlich der Implementierung der gewünschten neuen Funktion) in ungefähr 5-6 Monaten umschreiben. Ich habe diesen Vorschlag den Stakeholdern vorgelegt und sie erklären sich damit einverstanden, ihn neu zu schreiben (zum Glück für mich).
Nachdem ich dieses Stück umgeschrieben hatte, verstanden sie, dass ich viel bessere Sachen liefern konnte als das, was sie bereits hatten. So konnte ich die gesamte Anwendung neu schreiben.
Mein Ansatz war es, es Stück für Stück neu zu schreiben. Zuerst habe ich die gesamte Benutzeroberfläche (Windows Forms) ersetzt, dann die Kommunikationsschicht (Webdienstaufrufe) und zuletzt die gesamte Serverimplementierung (es war eine dicke Client / Server-Anwendung).
Ein paar Jahre später hat sich diese Anwendung zu einem schönen Schlüsselwerkzeug entwickelt, das vom gesamten Unternehmen verwendet wird. Ich bin mir zu 100% sicher, dass das niemals möglich gewesen wäre, wenn ich das Ganze nicht umgeschrieben hätte.
Obwohl ich es geschafft habe, ist es wichtig, dass die Stakeholder es genehmigten und ich sie davon überzeugen konnte, dass es das Geld wert war. Während Sie also die vorhandene Anwendung warten müssen, tun Sie einfach Ihr Bestes, um nichts zu beschädigen. Wenn Sie die Eigentümer von den Vorteilen des Umschreibens überzeugen können, versuchen Sie es wie Jack the Ripper: Stück für Stück.
quelle
Wenn der Code nicht geändert werden muss, lassen Sie ihn unverändert. Wenn Sie jedoch den Code ändern müssen, weil Sie einen Fehler beheben oder einige Funktionen ändern müssen, müssen Sie ihn ändern, ob es Ihnen gefällt oder nicht. IMHO ist es oft sicherer und weniger fehleranfällig, wenn Sie es zuerst in kleinere, besser verständliche Teile umgestalten und dann die Funktionalität hinzufügen. Wenn Sie über automatische Refactoring-Tools verfügen, kann dies relativ sicher durchgeführt werden, wobei nur ein sehr geringes Risiko besteht, dass neue Fehler auftreten. Und ich empfehle Ihnen, ein Exemplar von Michael Feathers Buch zu bekommen
http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052
Dies gibt Ihnen wertvolle Hinweise, wie Sie den Code testbarer machen, Komponententests hinzufügen und vermeiden können, dass der Code beim Hinzufügen neuer Funktionen beschädigt wird.
Wenn Sie es richtig machen, wird Ihre Methode hoffentlich so einfach, dass try-catch-finally keinen nicht mehr verwandten Code an den falschen Stellen mehr enthält.
quelle
Angenommen, Sie müssen sechs Methoden aufrufen, von denen vier nur aufgerufen werden sollten, wenn die erste fehlerfrei abgeschlossen wird. Betrachten Sie zwei Alternativen:
vs.
Im zweiten Code behandeln Sie Ausnahmen wie Rückkehrcodes. Konzeptionell ist dies identisch mit:
Wenn wir jede Methode, die eine Ausnahme auslösen kann, in einen try / catch-Block einschließen, was bringt es dann, überhaupt Ausnahmen in einer Sprachfunktion zu haben? Es gibt keinen Vorteil und es gibt mehr Tippen.
Der Grund, warum wir in erster Linie Ausnahmen in Sprachen haben, ist, dass wir in den schlechten alten Tagen der Rückkehrcodes häufig Situationen hatten, in denen wir mehrere Methoden hatten, die Fehler zurückgeben konnten, und jeder Fehler bedeutete, alle Methoden vollständig abzubrechen. Zu Beginn meiner Karriere, in den alten C-Tagen, sah ich überall solchen Code:
Dies war ein unglaublich häufiges Muster, das Ausnahmen sehr gut gelöst hat, indem Sie zum ersten Beispiel oben zurückkehren konnten. Eine Stilregel, die besagt, dass jede Methode einen eigenen Ausnahmeblock hat, wirft dies vollständig aus dem Fenster. Warum dann überhaupt die Mühe machen?
Sie sollten sich immer bemühen, den try-Block über den Codesatz hinaus zu bewegen, der konzeptionell eine Einheit darstellt. Es sollte Surround-Code enthalten, für den es keinen Sinn macht, einen anderen Code in der Codeeinheit auszuführen, wenn ein Fehler in der Codeeinheit vorliegt.
Zurück zum ursprünglichen Zweck von Ausnahmen: Denken Sie daran, dass sie erstellt wurden, da Code häufig Fehler nicht einfach sofort behandeln kann, wenn sie tatsächlich auftreten. Der Ausnahmepunkt besteht darin, dass Sie Fehler wesentlich später im Code oder weiter oben im Stapel behandeln können. Die Leute vergessen das und fangen sie in vielen Fällen lokal, wenn sie besser dran sind, einfach zu dokumentieren, dass die fragliche Methode diese Ausnahme auslösen kann. Es gibt zu viel Code auf der Welt, der so aussieht:
Hier fügen Sie nur Ebenen hinzu, und Ebenen sorgen für Verwirrung. Mach das einfach:
Darüber hinaus habe ich das Gefühl, dass Sie sich fragen sollten, ob die Methode selbst zu komplex ist und in mehrere Methoden unterteilt werden muss, wenn Sie dieser Regel folgen und mehr als ein paar Try / Catch-Blöcke in der Methode haben .
Das Mitnehmen: Ein Try-Block sollte den Code enthalten, der abgebrochen werden sollte, wenn irgendwo im Block ein Fehler auftritt. Ob dieser Codesatz aus einer oder tausend Zeilen besteht, spielt keine Rolle. (Wenn Sie tausend Zeilen in einer Methode haben, haben Sie andere Probleme.)
quelle
method0
quasi zu erwarten ist, dass eine Ausnahme ausgelöst wird, und solchen, in denen dies nicht der Fall ist. Angenommen , es wirdmethod1
manchmal ein geworfenInvalidArgumentException
, aber wenn dies nicht der Fall ist, wird ein Objekt in einen vorübergehend ungültigen Zustand versetzt, der von korrigiert wirdmethod2
, wodurch erwartet wird, dass das Objekt immer wieder in einen gültigen Zustand versetzt wird. Wenn Methode2 einenInvalidArgumentException
Code auslöst , der erwartet, dass eine solche Ausnahmemethod1