web-dev-qa-db-ja.com

n層アプリケーションでの例外処理?

階層型アプリケーションで例外を処理するための推奨されるアプローチまたはベストプラクティスは何ですか?

  • どこに配置すればよいですかtry/catchブロック?
  • どこにロギングを実装する必要がありますか?
  • N層アプリケーションで例外を管理するための推奨パターンはありますか?

簡単な例を考えてみましょう。ビジネスレイヤーを呼び出し、データレイヤーを呼び出すUIがあるとします。

//UI
protected void ButtonClick_GetObject(object sender, EventArgs e) 
{
    try {
        MyObj obj = Business.GetObj();
    }
    catch (Exception ex) {
        Logger.Log(ex); //should the logging happen here, or at source?
        MessageBox.Show("An error occurred");
    }
}

//Business
public MyObj GetObj()
{
    //is this try/catch block redundant?  
    try {
        MyObj obj = DAL.GetObj();
    }
    catch (Exception ex) {
        throw new Exception("A DAL Exception occurred", ex);
    }
}

//DAL
public MyObj GetObj()
{
    //Or is this try/catch block redundant? 
    try {
        //connect to database, get object
    }
    catch (SqlException ex) {
        throw new Exception("A SQLException occurred", ex);
    }
}

上記の例外処理についてどのような批判をしますか?

ありがとう

32
flesh

私の経験則では、通常、トップレベルで例外をキャッチし、そこでログに記録(またはその他の方法で報告)します。これは、エラーに関する最も多くの情報、最も重要なのは完全なスタックトレースがあるためです。

ただし、他の層で例外をキャッチする理由はいくつかあります。

  1. 例外は実際に処理されます。例:接続に失敗しましたが、層は接続を再試行します。
  2. 例外はより多くの情報で再スローされます(スタックを見るとまだ利用できません)。例えば。 DALは、接続しようとしたDBを報告する場合がありますが、SqlExceptionは通知しません。
  3. 例外はより一般的な例外に変換されますこれはその層のインターフェースの一部であり、上位で処理される場合とされない場合があります。例えば。 DALは接続エラーをキャッチし、DatabaseUnavailableExceptionをスローする場合があります。 BLは、重要ではない操作についてはこれを無視するか、重要な操作に対してはそれを伝播させる可能性があります。 BLが代わりにSqlExceptionをキャッチした場合、DALの実装の詳細に公開されます。代わりに、DatabaseUnavailableExceptionをスローする可能性はDALのインターフェースの一部です。

複数の層で同じエラーをログに記録することは一般的には役に立ちませんが、1つの例外を考えることができます。下位層が問題が重大であるかどうかわからない場合、警告としてログに記録できます。上位層がisクリティカルであると判断した場合、同じ問題をエラーとしてログに記録できます。

19
EMP

例外処理に関連して私が従ういくつかのルールは次のとおりです。

  • 例外は、それらを処理するためのロジックを実装できるレイヤーでのみキャッチする必要があります。ほとんどの場合、これは最上層で発生します。経験則として、レイヤー内にキャッチを実装する前に、上位レイヤーのロジックが例外の存在に何らかの形で依存しているかどうかを自問してください。たとえば、ビジネスレイヤーでは、次のルールがあります。サービスが利用可能な場合は外部サービスからデータを読み込み、そうでない場合はローカルキャッシュからデータを読み込みます。サービスを呼び出すとexptionがスローされた場合は、BLレベルでそれをキャッチしてログに記録し、キャッシュからデータを返すことができます。この場合、上位UIレイヤーはアクションを実行する必要はありません。ただし、サービスとキャッシュ呼び出しの両方が失敗した場合、エラーメッセージをユーザーに表示できるように、例外はUIレベルに移動する必要があります。
  • アプリケーション内のすべての例外をキャッチする必要があり、それらを処理するための特別なロジックがない場合は、少なくともログに記録する必要があります。もちろん、これは、すべてのメソッドのコードをtry/catchブロックでラップする必要があるという意味ではありません。代わりに、どのアプリケーションタイプにも、キャッチされない例外のグローバルハンドラがあります。たとえば、Windowsアプリケーションでは、Application.ThreadExceptionイベントを実装する必要があります。 ASP .Netアプリケーションでは、global.asaxのApplication_Errorイベントハンドラーを実装する必要があります。これらの場所は、コードで例外をキャッチできる最上位の場所です。多くのアプリケーションでは、は、ほとんどの例外をキャッチする場所であり、ロギングに加えて、ここでは、ユーザーに表示される一般的でわかりやすいエラーメッセージウィンドウも実装する可能性があります。
  • catchブロックを実装せずに、必要な場所にtry/finally機能を実装できます。例外処理ロジックを実装する必要がない場合は、catchブロックを実装しないでください。例えば:
SqlConnection conn = null;  
try
{
    conn = new SqlConnection(connString);
    ...
}
// do not implement any catch in here. db exceptions logic should be implemented at upper levels
finally
{
    // finalization code that should always be executed.
    if(conn != null) conn.Dispose();
}
  • キャッチブロックから例外を再スローする必要がある場合は、throw;を使用するだけです。これにより、スタックトレースが確実に保持されます。 throw ex;を使用すると、スタックトレースがリセットされます。
  • 上位レベルにとってより意味のある別のタイプで例外を再スローする必要がある場合は、新しく作成された例外のInnerExceptionプロパティに元の例外を含めます。
  • コードから例外をスローする必要がある場合は、意味のある例外タイプを使用してください。
11

最初に修正することは、一般的なExceptionを決してスローしないことです。

2つ目は、例外をラップする正当な理由がない限り、throw; の代わりに throw new...catch句で。

3番目(これは厳格なルールではありません)は、UIレイヤーの下のどのポイントでも一般的な例外をキャッチしません。 UIレイヤーは、一般的な例外をキャッチして、爆発したものの技術的な詳細ではなく、ユーザーフレンドリーなメッセージをエンドユーザーに表示できるようにする必要があります。レイヤーのより深いところで一般的な例外をキャッチすると、意図せずに飲み込まれてしまう可能性があり、追跡が非常に難しいバグが発生します。

3
Agent_9191

例外処理はどのアプリケーションでも困難です。あなたはすべての例外について考え、すぐにあなたのために働くパターンに入る必要があります。例外を次のカテゴリのいずれかにグループ化しようとしています...

  • コードが間違っている(または理解していない、またはそれらを制御できない)場合にのみ発生するはずの例外:

例:永続性フレームワークでは、不正な形式のSQLから発生する可能性のあるSQL例外をキャッチする必要があるかもしれませんが、ハードコードされたクエリを実行しています。

処理:私の経験では、ほとんどの例外はこのカテゴリに分類されます。少なくとも、それらをログに記録します。さらに良いことに、それらをログに記録する例外処理サービスに送信します。その後、それらを別の方法でログに記録したり、別の方法で何かを実行したりする場合は、1か所で変更できます。また、UIレイヤーに何らかのエラーが発生したことを示すフラグを送信して、操作を再試行する必要がある場合もあります。たぶんあなたは管理者にメールを送ります。

また、サーバーでの寿命が続くように、何かを上位層に戻す必要があります。これがデフォルト値であるか、nullである可能性があります。たぶん、操作全体をキャンセルする方法があります。

もう1つの選択肢は、例外処理サービスに2つの処理方法を与えることです。 handleUnexpectedException()メソッドはユーザーに通知しますが、例外を再スローしません。スタックを自分で巻き戻すか、何らかの方法で続行できる場合は、デフォルト値を返すことができます。 handleFatalException()メソッドは、ユーザーに通知し、ある種の例外を再スローします。これにより、例外のスローによってスタックが巻き戻されるようになります。

  • 実際にユーザーによって引き起こされる例外:

例:ユーザーがfoobarウィジェットを更新して新しい名前を付けようとしていますが、必要な名前のfoobarウィジェットが既に存在します。

Handling:この場合、例外をユーザーに戻す必要があります。この場合、UIレイヤーに到達するまで、例外をスローし続けることができます(または、キャッチすらしないでください)。その後、UIレイヤーは例外の処理方法を認識している必要があります。これらの例外を文書化して、あなた(またはUIを作成している人)がそれらが存在することを認識し、それらを予期することを知っていることを確認してください。

  • 実際に処理できる例外:

例:リモートサービス呼び出しを行い、リモートサービスがタイムアウトしましたが、リモートサービスに呼び出しの履歴があることがわかっているので、呼び出しをやり直す必要があります。

処理:これらの例外は最初のカテゴリから始まる場合があります。アプリケーションがしばらくの間使用された後、実際にそれを処理するための良い方法があることに気づきます。楽観的ロックや中断された例外からの例外のように、例外をキャッチしてそれを使って何かをすることは、ビジネスの一部にすぎない場合があります。このような場合は、例外を処理してください。言語(Javaだと思います)がチェックされた例外とチェックされていない例外を区別する場合は、これらを常にチェックされた例外にすることをお勧めします。

上記の質問に対処するために、ユーザーに通知するサービスに最初の例外を委任します。MyObjのオブジェクトの種類(設定など)によっては、これを致命的ではない例外にしてデフォルト値を返す場合があります。それができない場合(ユーザーアカウントなど)、これを致命的な例外にする可能性があるため、心配する必要はありません。

2
Pace

ユーザーには明確で理解しやすいエラーメッセージのみが表示されるため、レイヤー境界(これは管理者用)で例外をログに記録する(ファイル内など)ために、レイヤーごとに個別の例外クラス(DALException、BLException、...)を使用しています。これらの例外は、すべてのデータアクセス層によって継承されたDAlBaseで処理する必要があります。これにより、いくつかのクラスで例外処理を一元化でき、開発者はlayerexception(DALExceptionなど)のみをスローします。詳細を参照してください 多層例外処理

0
shanon