私は非常に単純な質問に苦労しています:
私は現在サーバーアプリケーションに取り組んでおり、例外の階層を作成する必要があります(一部の例外は既に存在しますが、一般的なフレームワークが必要です)。どうすればこれを始められますか?
私はこの戦略に従うことを考えています:
1)何が問題になっていますか?
2)誰がリクエストを開始しますか?
3)メッセージ処理:サーバーアプリケーションを扱っているため、メッセージの送受信がすべてです。では、メッセージの送信がうまくいかない場合はどうでしょうか?
そのため、次の例外タイプが発生する可能性があります。
これは例外階層を定義するための非常に一般的なアプローチですが、いくつかの明らかなケースが欠けているのではないかと心配しています。私がカバーしていない領域についてのアイデアはありますか、この方法の欠点を知っていますか、またはこの種の質問に対するより一般的なアプローチがありますか(後者の場合、どこで見つけることができますか)?
前もって感謝します
総論
(少し意見が偏る)
私は通常、詳細な例外階層には行きません。
最も重要なこと:例外は、メソッドがジョブを完了できなかったことを呼び出し元に通知します。そして、あなたの呼び出し元mustそのことに気づくので、彼は単に続行しません。これは、どの例外クラスを選択しても、どの例外でも機能します。
2番目の側面はロギングです。何か問題が発生したときに、意味のあるログエントリを見つける必要があります。これも、さまざまな例外クラスを必要とせず、適切に設計されたテキストメッセージのみを必要とします(エラーログを読み取るためのオートマトンは必要ないと思います...)。
3番目の側面は、発信者の反応です。呼び出し元が例外を受け取ったときに何ができますか?ここで、異なる例外クラスを持つことは理にかなっているため、呼び出し元は、同じ呼び出しを再試行するか、別のソリューションを使用するか(代わりにフォールバックソースを使用するなど)、中止するかを決定できます。
また、例外をエンドユーザーに問題について通知するためのベースとして使用することもできます。つまり、ログファイルのadmin-textのほかにユーザーフレンドリーなメッセージを作成しますが、別の例外クラスは必要ありません(ただし、テキスト生成が容易になる可能性があります...)。
ロギング(およびユーザーエラーメッセージ)の重要な側面は、あるレイヤーで例外をキャッチし、コンテキスト情報を追加することにより、例外をコンテキスト情報で修正できることです。メソッドパラメータ、およびそれを再スローします。
あなたの階層
リクエストを起動しているのは誰ですか?リクエストを起動した人物の情報は必要ないと思います。コールスタックの奥深くにあることをどのようにして知っているのか、想像もできません。
メッセージ処理:これは別の側面ではありませんが、「何が問題なのですか?」.
コメントでは、例外を作成するときに "no logging"フラグについて話します。例外を作成してスローする場所で、その例外をログに記録するかどうかを確実に決定できるとは思いません。
私が想像できる唯一の状況は、一部の上位層がAPIを時々例外を生成する方法で使用し、この層がその例外を管理者に迷惑をかける必要がないことを知っているため、例外を静かに飲み込むことです。しかし、それはコードのにおいです。予期される例外はそれ自体が矛盾であり、APIを変更するためのヒントです。そして、決定する必要があるのは上位層であり、例外生成コードではありません。
エラー応答パターンを設計する際に留意すべき主なことは、呼び出し元にとって有用であることを確認することです。これは、例外を使用している場合でも、定義されたエラーコードを使用している場合でも当てはまりますが、例外のあるものに限定して説明します。
言語またはフレームワークがすでに一般的な例外クラスを提供している場合は、それらを適切な場所および合理的に期待される場所に使用します。独自のArgumentNullException
またはArgumentOutOfRange
例外クラスを定義しないでください。発信者はそれらをキャッチすることを期待していません。
アプリケーションのコンテキスト内で一意のエラーを含むようにMyClientServerAppException
基本クラスを定義します。 決して基本クラスのインスタンスをスローしません。あいまいなエラー応答はTHE WORST THING EVERです。 「内部エラー」がある場合は、そのエラーについて説明します。
ほとんどの場合、基本クラスの下の階層は、深いものではなく広いものにする必要があります。発信者にとって役立つ状況でのみ、それを深くする必要があります。たとえば、クライアントからサーバーへのメッセージが失敗する可能性がある5つの理由がある場合、ServerMessageFault
例外を定義し、その下にある5つのエラーごとに例外クラスを定義できます。このようにして、呼び出し側はスーパークラスを必要に応じて、または必要に応じてキャッチできます。これを特定の合理的なケースに制限するようにしてください。
実際に使用される前に、すべての例外クラスを定義しようとしないでください。そのほとんどをやり直すことになります。コードの記述中にエラーが発生した場合は、そのエラーをどのように説明するのが最善かを判断します。理想的には、発信者が実行しようとしていることのコンテキストで表現する必要があります。
前のポイントに関連して、エラーに応答するために例外を使用するからといって、エラーにonly例外を使用する必要があることを意味しないことに注意してください状態。例外のスローは通常高価であり、パフォーマンスコストは言語によって異なる可能性があることに注意してください。一部の言語では、コールスタックの深さに応じてコストが高くなるため、エラーがコールスタック内の深い場合は、単純なプリミティブ型(整数エラーコードまたはブールフラグ)をプッシュに使用できないかどうかを確認してくださいエラーはスタックをバックアップするため、呼び出し元の呼び出しの近くでスローできます。
エラー応答の一部としてロギングを含める場合、呼び出し元が例外オブジェクトにコンテキスト情報を追加するのは簡単にフォールダウンできるはずです。ロギングコードで情報が使用されているところから始めます。 (巨大なテキストの壁にならずに)ログが役立つために必要な情報量を決定します。次に、逆方向に作業して、例外クラスにその情報を簡単に提供できることを確認します。
最後に、アプリケーションがメモリ不足エラーを適切に絶対に処理できない限り、それらまたは他の致命的なランタイム例外を処理しようとしないでください。 OSに処理を任せてください。実際にはそれだけでできます。
それを簡素化してみてください。
別の戦略で考えるのに役立つ最初のことは、多くの例外をキャッチすることは、Javaからのchecked exceptionsの使用と非常に似ています(申し訳ありませんが、私はC++開発者ではありません)。これは 多くの理由 のために良くないので、私は常にそれらを使用しないようにします、そしてあなたの階層の例外戦略は私にその多くを覚えています。
したがって、別の柔軟な戦略をお勧めします:未チェックの例外を使用とコードエラー。
例として、次のJavaコードを参照してください。
public class SystemErrorCode implements ErrorCode {
INVALID_NAME(101),
ORDER_NOT_FOUND(102),
PARAMETER_NOT_FOUND(103),
VALUE_TOO_SHORT(104);
private final int number;
private ErrorCode(int number) {
this.number = number;
}
@Override
public int getNumber() {
return number;
}
}
そして、あなたのnique例外:
public class SystemException extends RuntimeException {
private ErrorCode errorCode;
public SystemException(ErrorCode errorCode) {
this.errorCode = errorCode;
}
}
この戦略は私がこのリンクで見つけたであり、Java実装 ここ で見つけることができ、詳細を見ることができます上記のコードが簡略化されているためです。
「クライアント」アプリケーションと「別のサーバー」アプリケーション間で異なる例外を分離する必要があるので、ErrorCodeインターフェースを実装する複数のエラーコードクラスを持つことができます。
さて、まず第一に、アプリケーションによってスローされる可能性があるすべてのチェック済み例外のベースException
クラスを作成することをお勧めします。アプリケーションがDuckType
と呼ばれていた場合は、DuckTypeException
基本クラスを作成します。
これにより、基本DuckTypeException
クラスの例外を処理のためにキャッチできます。ここから、例外は説明的な名前で分岐し、問題のタイプをより明確に示すことができます。たとえば、「DatabaseConnectionException」です。
はっきりさせておきますが、これらはすべて、プログラムで適切に処理したいと思われる可能性のある状況の例外をチェックする必要があります。つまり、データベースに接続できないため、DatabaseConnectionException
がスローされます。これをキャッチして、しばらく待ってから再試行できます。
あなたはnot無効なSQLクエリやnullポインタ例外などの非常に予期しない問題についてチェックされた例外を確認します。これらの例外にほとんどのcatch句を超えさせる(または必要に応じてキャッチして再スローする)ことをお勧めします)メインコントローラ自体に到達するまで、これはRuntimeException
を純粋にロギング目的でキャッチできます。
私の個人的な好みは、チェックされていないRuntimeException
を別の例外として再スローしないことです。チェックされていない例外の性質上、それは予期されないため、別の例外で再スローすると情報が非表示になります。ただし、それが好みの場合でも、RuntimeException
をキャッチしてDuckTypeInternalException
をスローすることができます。これは、DuckTypeException
とは異なり、RuntimeException
から派生するため、チェックされません。
必要に応じて、DatabaseException
などの組織的な目的で、データベース関連の例外をサブカテゴリ化できますが、そのようなサブ例外は、ベース例外DuckTypeException
から派生し、抽象的であるため、明示的な説明的な名前で導出されます。
原則として、例外を処理するために呼び出し元の呼び出しスタックを上に移動するにつれて、試行キャッチはより一般的になるはずです。また、メインコントローラでは、DatabaseConnectionException
ではなく単純なDuckTypeException
のうち、すべてのチェック済み例外が派生します。
例外は無制限のgotoであり、注意して使用する必要があります。それらのための最もよい戦略はそれらを制限することです。呼び出し元の関数は、呼び出す関数によってスローされたすべての例外を処理する必要があります。そうしないと、プログラムが終了します。呼び出し関数のみが、例外を処理するための正しいコンテキストを持っています。コールツリーの上位にある関数にそれらを処理させることは、無制限のゴトです。
例外はエラーではありません。これらは、プログラムがコードの1つのブランチを完了できず、別のブランチが続くことを示す場合に発生します。
例外は、呼び出された関数のコンテキスト内にある必要があります。例: 二次方程式 を解く関数。 2つの例外を処理する必要があります:Division_by_zeroとsquare_root_of_negative_number。ただし、これらの例外は、この2次方程式ソルバーを呼び出す関数には意味がありません。これらは、方程式を解くために使用される方法が原因で発生し、単純にそれらを再スローすると、内部が明らかになり、カプセル化が解除されます。代わりに、division_by_zeroはnot_quadraticおよびsquare_root_of_negative_numberおよびno_real_rootsとして再スローする必要があります。
例外の階層構造は必要ありません。呼び出し側の関数は例外を処理する必要があるため、関数によってスローされた例外(それらを識別する)の列挙で十分です。呼び出しツリーを処理できるようにすることは、コンテキスト外(制限なし)のgotoです。