試行/キャッチ/ログ/再スロー-アンチパターンはありますか?


18

try / catchの周りにすべてのコードブロックを散らかすのではなく、中央の場所またはプロセスの境界で例外を処理することの重要性が良いプラクティスとして強調されているいくつかの投稿を見ることができます。私たちのほとんどはそれの重要性を理解していると強く信じていますが、主に例外中のトラブルシューティングを容易にするために、より多くのコンテキスト固有の情報(例:メソッドパラメータ合格)、方法はtry / catch / log / rethrowの周りにメソッドをラップすることです。

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

例外処理の優れた実践を維持しながら、これを達成する正しい方法はありますか?このためにPostSharpのようなAOPフレームワークについて聞いたことがありますが、これらのAOPフレームワークに関連するマイナスまたは主要なパフォーマンスコストがあるかどうかを知りたいです。

ありがとう!


6
try / catchで各メソッドをラップすること、例外をログに記録すること、コードを一緒に移動させることの間には大きな違いがあります。そして、追加情報を使用して例外をキャッチし、再スローします。最初はひどい練習です。2番目は、デバッグエクスペリエンスを改善するための完璧な方法です。
陶酔

私は各メソッドをtry / catchと言って、単純にcatchブロックにログインして再スローします-これは大丈夫ですか?
rahulaga_dev

2
アモンが指摘したように。言語にスタックトレースがある場合、各catchにログインしても意味がありません。ただし、例外をラップして追加情報を追加することをお勧めします。
陶酔

1
@Liathの答えをご覧ください。私が出した答えはほとんど彼のことを反映しています:できるだけ早く例外をキャッチし、その段階でできることはいくつかの有用な情報を記録するだけで、それからそれをやり直してください。私の見解では、これをアンチパターンとして見ることは無意味です。
デビッドアルノ

1
Liath:小さなコードスニペットを追加。私はc#を使用しています
rahulaga_dev

回答:


18

問題はローカルのcatchブロックではなく、問題はlogとrethrowです。例外を処理するか、追加のコンテキストを追加する新しい例外でラップしてスローします。そうしないと、同じ例外に対して複数の重複したログエントリが発生します。

ここでのアイデアは、アプリケーションをデバッグする機能を強化することです。

例#1:処理する

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

例外を処理する場合、例外ログエントリの重要性を簡単に下げることができ、その例外をチェーンに浸透させる理由はありません。すでに対処済みです。

例外の処理には、問題が発生したことをユーザーに通知すること、イベントを記録すること、または単に無視することが含まれます。

注:例外を意図的に無視する場合は、理由を明確に示す空のcatch句でコメントを提供することをお勧めします。これにより、将来のメンテナーはそれが間違いや怠laなプログラミングではなかったことを知ることができます。例:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

例#2:追加のコンテキストを追加してスローする

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

追加のコンテキスト(解析コードの行番号やファイル名など)を追加すると、入力ファイルをデバッグする機能を強化するのに役立ちます(問題があると仮定)。これは特殊なケースなので、例外をブランド変更するためだけに「ApplicationException」に再ラップしても、デバッグには役立ちません。必ず追加情報を追加してください。

例#3:例外なしで何もしない

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

この最後のケースでは、例外を変更せずにそのままにしておきます。最外層の例外ハンドラーがロギングを処理できます。このfinally句は、メソッドに必要なリソースがクリーンアップされるようにするために使用されますが、例外がスローされたことをログに記録する場所ではありません。


問題はローカルのcatchブロックではなく、ログと再スローである」が好きでした しかし、最終的には、try / catchがすべてのメソッドに散らばっていても問題ないということですね。すべての方法を実行するのではなく、このプラクティスを慎重に実行するためのガイドラインが必要だと思います。
rahulaga_dev

私は答えにガイドラインを提供しました。これはあなたの質問に答えませんか?
ベリンロリチュ

@rahulaga_devガイドライン/銀の弾丸があるとは思わないので、コンテキストに大きく依存するため、この問題を解決してください。例外を処理する場所または例外を再スローするタイミングを示す一般的なガイドラインはありません。IMO、私が見る唯一のガイドラインは、ロギング/処理を可能な限り最新の時間に延期し、再利用可能なコードでのロギングを避けて、不必要な依存関係を作成しないようにすることです。独自の方法で処理する機会を与えずに物事を記録した場合(例外を処理した場合)、コードのユーザーはあまり面白くないでしょう。ちょうど私の2セント:)
andreee

7

地元の漁獲がアンチパターンであるとは思わない。実際、私が正しく覚えていれば、それは実際にJavaで実施されている!

エラー処理を実装するときに私にとって重要なのは、全体的な戦略です。サービス境界ですべての例外をキャッチするフィルターが必要な場合、手動でインターセプトしたい場合があります。全体的な戦略があり、チームのコーディング標準に該当する場合は両方とも問題ありません。

個人的には、次のいずれかを実行できるときに、関数内のエラーをキャッチするのが好きです。

  • コンテキスト情報(オブジェクトの状態や何が起こっているかなど)を追加します
  • 例外を安全に処理する(TryXメソッドなど)
  • システムがサービスの境界を越えて、外部ライブラリまたはAPIを呼び出しています
  • 異なるタイプの例外をキャッチして再スローしたい(おそらく、元の例外を内部例外として)
  • いくつかの低価値のバックグラウンド機能の一部として例外がスローされました

これらのケースのいずれでもない場合、ローカルのtry / catchを追加しません。そうである場合、シナリオに応じて、例外を処理する場合(たとえば、falseを返すTryXメソッド)または再スローして、例外がグローバル戦略によって処理されるようにします。

例えば:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

または再スローの例:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

次に、スタックの上部でエラーをキャッチし、ユーザーにわかりやすいメッセージを表示します。

どちらのアプローチを採用するにしても、このシナリオの単体テストを作成する価値は常にあるため、機能が変更されないことを確認し、後日プロジェクトのフローを中断することができます。

どの言語で作業しているのかは言及していませんが、.NET開発者であり、これを見ないほど何度も見たことがあるでしょう。

書かないで:

catch(Exception ex)
{
  throw ex;
}

使用する:

catch(Exception ex)
{
  throw;
}

前者はスタックトレースをリセットし、トップレベルのキャッチをまったく役に立たなくします!

TLDR

ローカルでキャッチすることはアンチパターンではありません。多くの場合、デザインの一部であり、エラーにコンテキストを追加するのに役立ちます。


3
同じロガーがトップレベルの例外ハンドラーで使用される場合、catchでのロギングのポイントは何ですか?
陶酔

スタックの最上部にはアクセスできない追加情報(ローカル変数など)がある場合があります。説明のために例を更新します。
リース

2
その場合、追加のデータと内部例外を含む新しい例外をスローします。
陶酔

2
@Euphoricうん、私もそれを見たことがありますが、個人的に私はファンではありませんが、それはあなたが私が多くのオーバーヘッドであると感じるほぼすべての単一のメソッド/シナリオに対して新しいタイプの例外を作成する必要があるためです。ここにログ行を追加すると(おそらく最上部に別の行も追加される)、問題を診断する際のコードの流れを説明するのに役立ちます
-Liath

4
Javaは例外の処理を強制するのではなく、例外を認識するように強制します。あなたはそれをキャッチして何でもするか、関数が投げることができ、関数でそれを使って何もしないと宣言することができます....それ以外の場合はかなり良い答えを少し選ぶ!
ニュートピア

4

これは言語に大きく依存します。たとえば、C ++は例外エラーメッセージでスタックトレースを提供しないため、頻繁なcatch-log-rethrowを通じて例外をトレースすると役立ちます。対照的に、Javaおよび同様の言語は非常に優れたスタックトレースを提供しますが、これらのスタックトレースの形式はあまり構成できない場合があります。これらの言語で例外をキャッチして再スローすることは、実際に重要なコンテキストを追加できない限り、まったく意味がありません(たとえば、低レベルのSQL例外をビジネスロジック操作のコンテキストに接続する)。

リフレクションによって実装されるエラー処理戦略は、ほとんどの場合、言語に組み込まれている機能よりも効率が劣ります。また、パーベイシブロギングにはパフォーマンスのオーバーヘッドが避けられません。そのため、取得する情報のストリームとこのソフトウェアの他の要件とのバランスを取る必要があります。とはいえ、コンパイラレベルのインスツルメンテーションに基づいて構築されたPostSharpのようなソリューションは、一般にランタイムリフレクションよりもはるかに優れています。

私は個人的に、すべてを記録することは役に立たないと思います。なぜなら、それは無関係な情報をたくさん含んでいるからです。したがって、自動化されたソリューションには懐疑的です。優れたロギングフレームワークを考えると、ログに記録する情報の種類と、この情報をどのようにフォーマットするかを説明する合意済みのコーディングガイドラインを作成すれば十分です。その後、必要に応じてロギングを追加できます。

ビジネスロジックへのログオンは、ユーティリティ関数へのログオンよりもはるかに重要です。また、実際のクラッシュレポートのスタックトレース(プロセスの最上位レベルでのログ記録のみを必要とする)を収集することにより、ログが最も価値のあるコード領域を見つけることができます。


4

try/catch/logすべての方法で見ると、開発者がアプリケーションで何が起こるか、または起こらないかがわからず、最悪の事態を想定し、予想されるすべてのバグのためにすべてを先取りしてログに記録するという懸念が生じます。

これは、単体テストと統合テストが不十分であり、開発者がデバッガーで大量のコードをステップ実行することに慣れていることを示す症状であり、何らかの方法で大量のログを記録することで、テスト環境にバグのあるコードを展開し、問題を見つけることを望んでいますログ。

例外をスローして記録する冗長コードよりも、例外をスローするコードの方が便利です。メソッドが予期しない引数を受け取ったときに意味のあるメッセージで例外をスローする(およびサービス境界でログに記録する)場合、無効な引数の副作用としてスローされた例外をすぐに記録し、原因を推測するよりもはるかに役立ちます。

ヌルは一例です。引数またはメソッド呼び出しの結果として値を取得し、それがnullであってはならない場合、例外をスローします。NullReferenceExceptionnull値のために、5行後にスローされた結果を単にログに記録しないでください。どちらの場合でも例外が発生しますが、1つは何かを伝え、もう1つはあなたに何かを探させます。

他の人が言ったように、サービスの境界で例外をログに記録するか、例外が正常に処理されたために例外が再スローされない場合に最適です。最も重要な違いは、あるものとないものの違いです。例外が簡単にアクセスできる1つの場所に記録されている場合、必要なときに必要な情報を見つけることができます。


スコットありがとう。「メソッドが予期しない引数を受け取ったときに意味のあるメッセージで例外をスローした場合(およびサービス境界でログに記録した場合)」という点は、メソッド引数のコンテキストで自分の周りに浮かぶ状況を視覚化するのに役立ちました。安全なガード句を持ち、引数の詳細をキャッチしてログに記録するのではなく、その場合にArgumentExceptionをスローすることは理にかなっていると思います
-rahulaga_dev

スコット、私は同じ気持ちを持っています。コンテキストを記録するためだけにログとリトウを見ると、開発者はクラスの不変式を制御できないか、メソッド呼び出しを保護できないと感じます。代わりに、すべてのメソッドは、同様のtry / catch / log / throwにラップされます。そしてそれはただひどいです。
マックス

2

まだ例外に含まれていないコンテキスト情報を記録する必要がある場合は、それを新しい例外にラップし、元の例外をとして提供しInnerExceptionます。そうすれば、元のスタックトレースが保持されます。そう:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Exceptionコンストラクターの2番目のパラメーターは、内部例外を提供します。その後、すべての例外を1か所で記録できますが、同じログエントリで完全なスタックトレースコンテキスト情報を取得できます。

カスタム例外クラスを使用することもできますが、ポイントは同じです。

try / catch / log / rethrowは混乱を招くため、混乱を招きます。たとえば、コンテキスト情報のロギングとトップレベルハンドラーでの実際の例外のロギングの間に別のスレッドで別の例外が発生した場合はどうでしょうか。ただし、新しい例外が元の情報に情報を追加する場合、try / catch / throwは問題ありません。


元の例外タイプはどうですか?包むと消えてしまいます。それって問題ですか?たとえば、誰かがSqlTimeoutExceptionに依存していました。
マックス

@Max:元の例外タイプは内部例外として引き続き使用可能です。
ジャックB

それが私の言いたいことです!これで、SqlExceptionをキャッチしていた呼び出しスタックのすべてのユーザーが、それを取得することはありません。
マックス

1

例外自体は、メッセージ、エラーコードなどを含む、適切なロギングに必要なすべての情報を提供する必要があります。したがって、例外をキャッチする必要はありません。例外を再スローするか、別の例外をスローするためだけです。

DatabaseConnectionException、InvalidQueryException、InvalidSQLParameterExceptionをキャッチしてDatabaseExceptionを再スローするなど、一般的な例外としてキャッチおよび再スローされるいくつかの例外のパターンがよく見られます。ただし、これらの特定の例外はすべて、最初はDatabaseExceptionから派生しているため、再スローする必要はないと主張します。

不要なtry catch句(純粋にログを記録するためのものであっても)を削除すると、実際には仕事が簡単になり、難しくなることはありません。例外を処理するプログラム内の場所のみが例外をログに記録する必要があり、他のすべてが失敗する場合は、プログラムを正常に終了する前に例外をログに記録する最後の試行のためのプログラム全体の例外ハンドラーが必要です。例外には、例外がスローされた正確なポイントを示す完全なスタックトレースが必要であるため、多くの場合、「コンテキスト」ロギングを提供する必要はありません。

それはAOPがあなたのためのクイックフィックスソリューションであるかもしれないと言ったけれども、それは通常全体的にわずかな減速を必要とします。代わりに、付加価値のない不要なtry catch句を完全に削除することをお勧めします。


1
例外自体は、メッセージ、エラーコード、およびその他の適切なログ記録に必要なすべての情報を提供する必要があります。」しかし、実際には、古典的なケースであるNull参照例外はありません。例えば、複雑な式の中でそれを引き起こした変数を教えてくれる言語は知りません。
デビッドアルノ

1
@DavidArno確かに、しかしあなたが提供できるコンテキストはその特定のものでもありえない。それ以外の場合はtry { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }。ほとんどの場合、スタックトレースで十分ですが、それがなければ、通常、エラーのタイプで十分です。
ニール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.