"Wie schlecht" ist nicht verwandter Code im Try-Catch-finally-Block?

11

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?

Gemeinschaft
quelle
Das "C ++" - Tag gehört nicht hierher, da C ++ nicht hat (und nicht braucht) finally. Alle guten Verwendungen werden unter RAII / RRID / SBRM (welches Akronym Sie möchten) abgedeckt.
David Thornley
2
@DavidThornley: Abgesehen von dem Beispiel, in dem das Schlüsselwort 'finally' verwendet wird, gilt der Rest der Frage für C ++. Ich bin ein C ++ - Entwickler und dieses Schlüsselwort hat mich nicht wirklich verwirrt. Und wenn man bedenkt, dass ich halbjährlich ähnliche Gespräche mit meinem eigenen Team habe, ist diese Frage sehr relevant für das, was wir mit C ++
DXM
@DXM: Ich habe Probleme, dies zu visualisieren (und ich weiß, was finallyin C # funktioniert). Was ist das C ++ - Äquivalent? Was ich denke, ist Code nach dem catch, und das gilt auch für Code nach dem C # finally.
David Thornley
@ DavidThornley: Code in schließlich ist Code, der ausgeführt wird, egal was passiert .
Orlp
1
@nightcracker, das stimmt nicht wirklich. Es wird nicht ausgeführt, wenn Sie dies tun 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, mit finallydem Sie manuell iterieren.
Svick

Antworten:

10

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.

Alex
quelle
1
+1. Aber der letzte Satz bezieht sich auf einen Serienmörder. Ist das wirklich notwendig?
MarkJ
Ich mag einen Teil davon, aber gleichzeitig führt das Belassen defekter Designs und das Hinzufügen von Dingen oft zu einem schlechteren Design. Es ist besser herauszufinden, wie man es repariert und repariert, obwohl natürlich in einem gemessenen Ansatz.
Ricky Clarkson
@ RickyClarkson Ich stimme zu. Es ist wirklich schwer zu wissen, wann Sie es übertreiben (ein Refactoring ist nicht wirklich notwendig) und wann Sie die Anwendung durch Hinzufügen von Änderungen wirklich verletzen.
Alex
@ RickyClarkson Ich stimme dem so sehr zu, dass ich sogar das Projekt, an dem ich beteiligt war, umschreiben konnte. Aber lange Zeit musste ich vorsichtig Dinge hinzufügen, um den geringstmöglichen Schaden zu verursachen. Sofern die Anforderung nicht darin besteht, etwas zu beheben, dessen Hauptursache eine schlechte Entwurfsentscheidung ist (was normalerweise nicht der Fall ist), würde ich sagen, dass der Entwurf der Anwendung nicht berührt werden sollte.
Alex
@ RickyClarkson Das war Teil der Community-Erfahrung, die ich nutzen wollte. Alle Legacy-Codes als schlecht zu deklarieren, ist ein ebenso schlechtes Anti-Pattern. Es gibt jedoch Dinge in diesem Code, an denen ich arbeite, die eigentlich nicht als Teil eines finally-Blocks ausgeführt werden sollten. Alex 'Feedback und Antwort waren das, worüber ich lesen wollte.
2

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.

Doc Brown
quelle
2

Angenommen, Sie müssen sechs Methoden aufrufen, von denen vier nur aufgerufen werden sollten, wenn die erste fehlerfrei abgeschlossen wird. Betrachten Sie zwei Alternativen:

try {
     method1();  // throws
     method2();
     method3();
     method4();
     method5();
 } catch(e) {
     // handle error
 }
 method6();

vs.

 try {
      method1();  // throws
 }
 catch(e) {
      // handle error
 }
 if(! error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

Im zweiten Code behandeln Sie Ausnahmen wie Rückkehrcodes. Konzeptionell ist dies identisch mit:

rc = method1();
if( rc != error ) {
     method2();
     method3();
     method4();
     method5();
 }
 method6();

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:

rc = method1();
if( rc != error ) {
     rc = method2();
     if( rc != error ) {
         rc = method3();
         if( rc != error ) {
             rc = method4();
             if(rc != error ) {
                 method5();
             }
         }
     }
 }
 method6();

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:

void method0() : throws MyNewException 
{
    try {
        method1();  // throws MyOtherException
    }
    catch(e) {
        if(e == MyOtherException)
            throw MyNewException();
    }
    method2();
}

Hier fügen Sie nur Ebenen hinzu, und Ebenen sorgen für Verwirrung. Mach das einfach:

void method0() : throws MyOtherException
{
    method1();
    method2();
}

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.)

Gort den Roboter
quelle
Steven - danke für die übersichtliche Antwort, und ich stimme Ihren Gedanken voll und ganz zu. Ich habe kein Problem mit vernünftig verwandtem oder sequentiellem Typcode, der in einem einzelnen Try-Block lebt. Wäre es mir gelungen, ein tatsächliches Code-Snippet zu veröffentlichen, hätte es vor dem ersten Wurfanruf 5 bis 10 völlig unabhängige Anrufe angezeigt. Ein bestimmter Endblock war viel schlimmer - mehrere neue Threads wurden abgespalten und zusätzliche, nicht bereinigende Routinen wurden aufgerufen. Und im Übrigen hatten alle Anrufe in diesem endgültigen Block nichts mit allem zu tun, was hätte geworfen werden können.
Ein Hauptproblem bei diesem Ansatz besteht darin, dass nicht zwischen Fällen unterschieden wird, in denen method0quasi zu erwarten ist, dass eine Ausnahme ausgelöst wird, und solchen, in denen dies nicht der Fall ist. Angenommen , es wird method1manchmal ein geworfen InvalidArgumentException, aber wenn dies nicht der Fall ist, wird ein Objekt in einen vorübergehend ungültigen Zustand versetzt, der von korrigiert wird method2, wodurch erwartet wird, dass das Objekt immer wieder in einen gültigen Zustand versetzt wird. Wenn Methode2 einen InvalidArgumentExceptionCode auslöst , der erwartet, dass eine solche Ausnahme method1
abgefangen
... der Systemstatus entspricht den Erwartungen des Codes. Wenn eine von Methode1 ausgelöste Ausnahme anders behandelt werden sollte als eine von Methode2 ausgelöste, wie kann dies sinnvoll erreicht werden, ohne sie zu verpacken?
Supercat
Im Idealfall sind Ihre Ausnahmen so detailliert, dass dies eine seltene Situation ist.
Gort the Robot