web-dev-qa-db-ja.com

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

中央の場所またはプロセスの境界で例外を処理することの重要性が、try/catchの周りのすべてのコードブロックを散らかすのではなく、良い方法として強調されているいくつかの投稿を見ることができます。私はほとんどの人がその重要性を理解していると強く信じていますが、例外の発生時のトラブルシューティングを簡単にするために、コンテキスト固有の情報をログに記録したいという理由で、catch-log-rethrowアンチパターンが依然として発生しているようです(例:メソッドパラメーター渡された)そして方法は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フレームワークに関連するパフォーマンスコストのマイナス面または大きな面があるかどうか知りたいのですが。

ありがとう!

21
rahulaga_dev

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

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

例#1:処理する

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

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

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

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

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句は、メソッドに必要なすべてのリソースを確実にクリーンアップするために使用されますが、これは例外がスローされたことをログに記録する場所ではありません。

21
Berin Loritsch

ローカルキャッチがアンチパターンであるとは思いません。実際、私が正しく覚えていれば、それは実際に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 [〜#〜]

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

7
Liath

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

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

私は個人的に、すべてをログに記録することは役に立たないと信じています。したがって、私は自動化されたソリューションに懐疑的です。優れたロギングフレームワークを考えると、ログに記録する情報の種類とこの情報のフォーマット方法について説明する、合意済みのコーディングガイドラインがあれば十分かもしれません。次に、重要な場所にロギングを追加できます。

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

4
amon

私が見るとき try/catch/logすべてのメソッドで、開発者がアプリケーションで何が発生するかどうかわからない、最悪の場合を想定して、予期していたすべてのバグのためにあらゆる場所ですべてを予防的にログに記録するという懸念を引き起こします。

これは、単体テストと統合テストが不十分であり、開発者がデバッガーで多くのコードをステップ実行することに慣れており、何とかして多くのログがテスト環境でバグのあるコードをデプロイして問題を見つけることを可能にすることを望んでいる症状ですログ。

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

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

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

4
Scott Hannen

まだ例外に含まれていないコンテキスト情報を記録する必要がある場合は、それを新しい例外にラップし、元の例外を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で問題ありません。

2
JacquesB

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

DatabaseConnectionException、InvalidQueryException、およびInvalidSQLParameterExceptionをキャッチし、DatabaseExceptionを再スローするなど、いくつかの例外のパターンが共通の例外としてキャッチおよび再スローされることがよくあります。これに対して、これらすべての特定の例外はそもそもDatabaseExceptionから派生しているはずなので、再スローする必要はありません。

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

とはいえ、通常は全体的に若干の速度低下を伴いますが、AOPは迅速な解決策になる可能性があります。代わりに、付加価値のない不要なtry catch句を完全に削除することをお勧めします。

1
Neil