try-catch-finallyブロックの無関係なコードは「どれほど悪い」ですか?


11

これは関連しているQ: 返品後の作業を行うためのfinally節の使用は、スタイルが悪い/危険ですか?

参照されるQでは、最終的に使用されるコードは、使用される構造とプリフェッチの必要性に関連しています。私の質問は少し異なりますが、より多くの聴衆にとって密接な関係があると思います。私の特定の例はC#winformアプリですが、これは最終的にC ++ / Javaの使用にも適用されます。

私は、ブロックに例外や例外処理/クリーンアップに関係のないコードがたくさんあるtry-catch-finallyブロックにかなり気付いています。そして、例外と処理に密接に関連するコードで非常にタイトなtry-catch-finallyブロックを作成するという私のバイアスを認めます。ここに私が見ているもののいくつかの例があります。

tryブロックには、スローされる可能性のあるコードに至るまでの多くの予備的な呼び出しと変数が設定されます。ロギング情報は、セットアップを取得し、tryブロックでも実行されます。

最後に、ブロックはform / module / controlのフォーマット呼び出し(catchブロックで表されるように、アプリが終了しようとしているにもかかわらず)を持ち、パネルなどの新しいオブジェクトを作成します。

大体:

    methodName(...)
    {
        試してみる
        {
            //メソッドの多くのコード...
            //スローできるコード...
            //メソッドとリターンのためのより多くのコードを...
        }
        catch(何か)
        {//例外を処理する}
        最後に
        {
            //例外によるいくつかのクリーンアップ、クローズ
            //作成されたもののためのより多くのコード(例外がスローされる可能性があることを無視)...
            //さらにオブジェクトを作成します
        }
    }

コードは機能するため、何らかの価値があります。それはうまくカプセル化されておらず、ロジックは少し複雑です。私は(苦労して)コードを移動したりリファクタリングしたりするリスクに精通しているので、私の質問は、同様に構造化されたコードに関する他の人の経験を知りたいということになります。

悪いスタイルは変更を加えることを正当化しますか?同様の状況で誰かがひどくやけどをしたことがありますか?その悪い経験の詳細を共有してくれませんか?私が過剰に反応していて、スタイルがそれほど悪くないので、それを残してください?物事を片付けることのメンテナンスの利点を得る?


「C ++」タグは、C ++にはないので(ここでは必要ないので)ここには属しませんfinally。すべての適切な使用法は、RAII / RRID / SBRM(好きな頭字語)でカバーされています。
デビッドソーンリー

2
@DavidThornley:「finally」キーワードが使用されている例は別として、残りの質問はC ++に完全に適用されます。私はC ++開発者であり、そのキーワードは私を本当に混乱させませんでした。そして私は半定期的に自分のチームと同様の会話を持って考えると、この質問は、私たちがC ++で何をすべきかに関連する非常に多くある
DXM

@DXM:これを視覚化するのに問題があります(finallyC#で何ができるかは知っています)。C ++と同等のものは何ですか?私が考えているのはcatch、の後のコードで、C#の後のコードにも同じことが当てはまりますfinally
デビッドソーンリー

@DavidThornley:最終的にコードは何に関係なく実行されるコードです。
-orlp

1
@nightcracker、それは本当に正しくありません。実行しても実行されませんEnvironment.FailFast()。キャッチされない例外がある場合、実行されない場合があります。また、finally手動で反復するイテレータブロックがある場合はさらに複雑になります。
svick

回答:


10

私は、開発者が何をしているかを明確に知らない開発者によって書かれたひどいレガシーWindows Formsコードを処理しなければならなかったとき、非常によく似た状況を経験しました。

まず第一に、あなたは過剰に働いていない。これは悪いコードです。あなたが言ったように、catchブロックは中止し、停止する準備をするべきです。オブジェクト(特にパネル)を作成するときではありません。これがなぜ悪いのか説明することすらできません。

それは言われています...

私の最初のアドバイスは、壊れていない場合は触れないでください!

あなたの仕事がコードを維持することであるなら、あなたはそれを壊さないために最善を尽くさなければなりません。痛みを伴うことは知っていますが(私はそこに行ったことがあります)、すでに機能しているものを壊さないように最善を尽くす必要があります。

2番目のアドバイスは、機能を追加する必要がある場合、コードを壊さないように、既存のコード構造を可能な限り維持することです。

例:適切な継承に置き換えることができると思われる恐ろしいswitch-caseステートメントがある場合は、物事を動かし始める前に注意してよく考えなければなりません。

あなたは間違いなくリファクタリングが正しいアプローチである状況を見つけるでしょうが、注意してください:コードのリファクタリングはバグを引き起こす可能性が高いです。開発者の観点からではなく、アプリケーションの所有者の観点から決定する必要があります。そのため、問題を修正するために必要な努力(お金)がリファクタリングの価値があるかどうかを考える必要があります。開発者が「コードがい」と思っているからといって、実際に壊れていないものを修正するのに数日間費やしているのを見てきました。

私の3番目のアドバイスは、コードを壊すとやけどするからです。あなたのせいであるかどうかは関係ありません。

メンテナンスのために雇われた場合、他の誰かが悪い決定を下したためにアプリケーションがバラバラになっているかどうかは本当に問題ではありません。ユーザーの観点から見ると、以前は機能していましたが、今ではそれを破りました。あなたはそれを壊した!

Joelは、レガシーコードを書き換えてはならないいくつかの理由を説明する記事に非常によく載っています。

http://www.joelonsoftware.com/articles/fog0000000069.html

そのため、この種のコードについては本当に気分が悪いはずです(そして、そのようなコードを決して書かないでください)が、それを維持することはまったく別の怪物です。

私の経験について:私は約1年間コードを維持する必要があり、最終的には一からではなく最初から書き直すことができました。

起こったことは、コードが非常に悪かったために、新しい機能を実装することができなかったことです。既存のアプリケーションには、深刻なパフォーマンスと使いやすさの問題がありました。最終的には、3〜4か月かかる変更を行うように依頼されました(主に、そのコードを操作するのにいつもより時間がかかったためです)。約5〜6か月でその部分全体(必要な新しい機能の実装を含む)を書き換えることができると思いました。私はこの提案を利害関係者に持ち込み、彼らはそれを書き直すことに同意します(幸運なことに)。

この作品を書き直した後、彼らは私が彼らがすでに持っていたものよりもはるかに優れたものを提供できることを理解しました。そのため、アプリケーション全体を書き直すことができました。

私のアプローチは、それを少しずつ書き直すことでした。最初にUI全体(Windowsフォーム)を置き換え、次に通信レイヤー(Webサービス呼び出し)の置き換えを開始し、最後にサーバー実装全体(シッククライアント/サーバーの種類のアプリケーション)を置き換えました。

数年後、このアプリケーションは会社全体で使用される美しいキーツールになりました。すべてを書き直さなければ、それは決して不可能だったでしょう。

私はそれを行うことができたが、重要な部分は、利害関係者がそれを承認したことであり、お金の価値があると彼らに確信させることができた。したがって、既存のアプリケーションを維持する必要がありますが、何も壊さないように最善を尽くし、所有者にそれを書き換える利点について納得させることができるなら、切り裂きジャックのようにそれをやろうとしてください:断片的に。


1
+1。しかし、最後の文は連続殺人犯に言及しています。それは本当に必要ですか?
MarkJ

私はこれの一部が好きですが、同時に壊れたデザインをそのままにしておき、ものを追加するとデザインが悪くなることがよくあります。もちろん、慎重なアプローチではありますが、修正方法と修正方法を考えた方が良いでしょう。
リッキークラークソン

@RickyClarkson同意します。あなたがそれをやりすぎているとき(リファクタリングは本当に必要ではない)、そしてあなたが変更を加えることによって本当にアプリケーションを傷つけているのかを知るのは本当に難しい。
アレックス

@RickyClarkson私はそのことに同意しているので、関与したプロジェクトを書き直すことさえできました。しかし、長い間、私は可能な限り最小のダメージを与えようとするものを注意深く追加しなければなりませんでした。リクエストが、主な原因が設計上の不適切な決定(通常はそうではない)である何かを修正することである場合を除き、アプリケーションの設計は変更しないでください。
アレックス

@RickyClarksonそれは私が活用したかったコミュニティの経験の一部でした。すべてのレガシーコードを不良として宣言することは、同様に不良なアンチパターンです。ただし、私が取り組んでいるこのコードには、finallyブロックの一部として実行すべきではないものがあります。アレックスのフィードバックと応答は、私が読みたいと思っていたものです。

2

コードを変更する必要がない場合は、そのままにしておきます。ただし、バグを修正したり機能を変更したりする必要があるためにコードを変更する必要がある場合は、好きかどうかにかかわらず、コードを変更する必要があります。私見では、より小さく理解しやすい部分に最初にリファクタリングしてから機能を追加すると、多くの場合、より安全でエラーが発生しにくくなります。自動リファクタリングツールが手元にある場合、これは比較的安全に実行でき、新しいバグを導入するリスクはごくわずかです。そして、マイケルフェザーズの本を手に入れることをお勧めします

http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

これにより、コードをよりテストしやすくする方法、単体テストを追加する方法、および新しい機能を追加するときにコードが破損しないようにする方法の貴重なヒントが得られます。

正しく実行すれば、メソッドが十分に単純になり、try-catch-finallyに関係のないコードが間違った場所に含まれなくなります。


2

呼び出すメソッドが6つあり、そのうち4つは、最初のメソッドがエラーなしで完了した場合にのみ呼び出す必要があるとします。次の2つの選択肢を検討してください。

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

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

2番目のコードでは、例外をリターンコードのように扱っています。概念的には、これは次と同じです:

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

try / catchブロックで例外をスローする可能性のあるすべてのメソッドをラップする場合、言語機能で例外を使用する意味は何ですか?利点はなく、さらにタイピングがあります。

そもそも言語に例外がある理由は、戻りコードの悪い昔には、エラーを返す可能性のある複数のメソッドがあり、各エラーはすべてのメソッドを完全に中止することを意味していたということです。私のキャリアの初め、昔のC時代、私はあちこちでこのようなコードを見ました:

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

これは非常に一般的なパターンであり、例外は上記の最初の例に戻ることで非常にきれいに解決されました。各メソッドに独自の例外ブロックがあるというスタイルルールは、これを完全にウィンドウからスローします。じゃあ、なぜわざわざ?

tryブロックが概念的にユニットであるコードのセットを囲むように常に努力する必要があります。コードユニットにエラーがある場合、コードユニットで他のコードを実行しても意味がないコードを囲む必要があります。

例外の元の目的に戻ると、実際に発生したコードは簡単にエラーを処理できないことが多いため、例外が作成されたことを思い出してください。例外のポイントは、コードのかなり後の段階、またはスタックのさらに上でエラーに対処できるようにすることです。人々はそれを忘れており、多くの場合、問題のメソッドがこの例外をスローできることを単純に文書化するほうがよい場合に、それらをローカルでキャッチします。世界には次のようなコードが多すぎます。

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

ここでは、レイヤーを追加するだけで、レイヤーは混乱を追加します。これを行うだけです:

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

それに加えて、私のルールでは、そのルールに従って、メソッドに2つ以上のtry / catchブロックがあることに気付いた場合、メソッド自体が複雑すぎて複数のメソッドに分解する必要があるかどうかを質問する必要があります。

要点:tryブロックには、ブロック内のどこかでエラーが発生した場合に中止する必要があるコードのセットを含める必要があります。このコードセットが1行であっても1000行であっても問題ではありません。(メソッドに1000行ある場合、他の問題もあります。)


スティーブン-よく考えられた対応に感謝します。あなたの考えに完全に同意します。単一のtryブロック内にある合理的に関連した、またはシーケンシャルなタイプのコードに問題はありません。実際のコードスニペットを投稿できた場合、最初のスロー可能な呼び出しの前に完全に無関係な呼び出しを5〜10回示していました。1つの特定の最終ブロックがさらに悪化しました-いくつかの新しいスレッドがスピンオフされ、追加の非クリーンアップルーチンが呼び出されました。また、その最終ブロックの呼び出しはすべて、スローされる可能性のあるものとは無関係でした。

このアプローチの1つの大きな問題はmethod0、例外がスローされる可能性があると予想される場合とそうでない場合を区別しないことです。たとえば、をmethod1スローするInvalidArgumentException場合がありますが、スローしない場合、オブジェクトは一時的に無効な状態になり、によって修正されますmethod2。これは、オブジェクトを常に有効な状態に戻すことが期待されます。方法2がスローした場合InvalidArgumentException、から、このような例外をキャッチする予定のコードはmethod1...にもかかわらず、それをキャッチします
supercat

...システムの状態はコードの期待に一致します。method1からスローされた例外をmethod2によってスローされた例外とは異なる方法で処理する必要がある場合、それらをラップせずに賢明に達成するにはどうすればよいですか?
supercat

理想的には、例外が十分にきめ細かいので、これはまれな状況です。
ロボット
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.