例外を設計する方法


11

私は非常に簡単な質問に苦労しています:

現在、サーバーアプリケーションで作業しています。例外の階層を作成する必要があります(一部の例外は既に存在しますが、一般的なフレームワークが必要です)。どうすればこれを開始できますか?

私はこの戦略に従うことを考えています:

1)何が問題なのでしょうか?

  • 何かが求められますが、これは許可されていません。
  • 何らかの質問があり、許可されていますが、パラメーターが間違っているため機能しません。
  • 何らかの質問があり、許可されていますが、内部エラーのために機能しません。

2)リクエストを開始しているのは誰ですか?

  • クライアントアプリケーション
  • 別のサーバーアプリケーション

3)メッセージ処理:サーバーアプリケーションを扱っているので、メッセージの送受信がすべてです。それでは、メッセージの送信がうまくいかない場合はどうでしょうか?

そのため、次の例外タイプが発生する場合があります。

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException(サーバーがリクエストの送信元を知らない場合)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

これは例外階層を定義するための非常に一般的なアプローチですが、いくつかの明らかなケースが欠けているのではないかと心配しています。私がカバーしていない分野についてのアイデアがありますか、この方法の欠点を知っていますか、この種の質問に対するより一般的なアプローチがありますか(後者の場合、どこで見つけることができますか)?

前もって感謝します


5
例外クラス階層で何を達成したいかは言及していませんでした(それはまったく明らかではありません)。有意義なロギング?クライアントがさまざまな例外に合理的に対応できるようにしますか?または何?
ラルフクレーバーホフ

2
最初にいくつかのストーリーとユースケースを調べて、何が出てくるかを確認しておくと便利でしょう。たとえば、クライアントがXを要求することは許可されていません。クライアントはXを要求しますが、要求は無効です。誰が例外を処理する必要があるか、それを使って何ができるか(プロンプト、再試行など)、およびそれを適切に実行するために必要な情報を考えて作業します。次に、具体的な例外が何であり、ハンドラーが例外を処理するために必要な情報を把握したら、それらを適切な階層に形成できます。
役に立たない

1
関連性の中で、私は推測するだろう:softwareengineering.stackexchange.com/questions/278949/...
マーティンBaの

1
これほど多くの異なる例外タイプを使用したいという願望を本当に理解したことはありません。通常catch、私が使用するほとんどのブロックでは、例外に含まれるエラーメッセージほど多くの例外を使用しません。ファイルの読み取り中にメモリの割り当てに失敗したため、ファイルの読み取りに失敗したことに関連する例外に対してできることは何もありません。そのためstd::exception、含まれているエラーメッセージをキャッチして報告する傾向があります。それを"Failed to open file: %s", ex.what()印刷する前にスタックバッファに追加します。

3
それに、そもそもスローされるすべての例外タイプを予想することはできません。私は今それらを予測することができるかもしれませんが、同僚は将来新しいものを導入するかもしれません。例えば、私は、現在そして永遠に、操作。そのため、1つのcatchブロックで一般的にスーパーをキャッチします。私は、多くの異なる使用している人々の例を見てきましたcatch...単一復旧サイト内のブロックを、しばしばそれだけ例外内のメッセージを無視して、より多くのローカライズされたメッセージを印刷します

回答:


5

総論

(少し意見が偏っている)

通常、詳細な例外階層には行きません。

最も重要なこと:例外は、メソッドがジョブを完了できなかったことを呼び出し元に伝えます。そして、あなたの呼び出し元それについて通知を受け取らなければならないので、彼は単に続行しません。どの例外クラスを選択しても、それはどの例外でも機能します。

2番目の側面はロギングです。何か問題が発生した場合は、意味のあるログエントリを検索する必要があります。また、異なる例外クラスは必要なく、適切に設計されたテキストメッセージのみが必要です(エラーログを読み取るための自動化は必要ないと思います...)。

3番目の側面は、発信者の反応です。呼び出し元が例外を受け取ったときに何ができますか?ここで、異なる例外クラスを持つことは理にかなっているので、呼び出し元は同じ呼び出しを再試行するか、別のソリューションを使用するか(代わりに代替ソースを使用する)、または放棄するかを決定できます。

そして、問題についてエンドユーザーに通知するためのベースとして、例外を使用することもできます。つまり、ログファイルの管理テキストに加えてユーザーフレンドリーなメッセージを作成することを意味しますが、異なる例外クラスは必要ありません(ただし、テキストの生成が容易になる可能性があります)。

ロギング(およびユーザーエラーメッセージ)の重要な側面は、一部のレイヤーで例外をキャッチし、メソッドパラメーターなどのコンテキスト情報を追加して再スローすることにより、コンテキスト情報で例外を修正できることです。

あなたの階層

誰がリクエストを開始しますか?リクエストを開始した人の情報は必要ないと思います。コールスタックの奥深くでそれをどのように知っているか想像することさえできません。

メッセージ処理:それは別の側面ではありませんが、「何が間違っているのですか?」の追加のケースです。

コメントでは、例外を作成するときに「ログなし」フラグについて説明します。例外を作成してスローする場所では、その例外をログに記録するかどうかの信頼できる決定を下せるとは思いません。

私が想像できる唯一の状況は、いくつかの上位レイヤーが時々例外を生成する方法であなたのAPIを使用することです。しかし、それはコードのにおいです。予期される例外はそれ自体矛盾であり、APIを変更するためのヒントです。そして、例外を生成するコードではなく、決定すべき上位層です。


私の経験では、ユーザーフレンドリーなテキストと組み合わせたエラーコードは非常にうまく機能します。管理者はエラーコードを使用して追加情報を見つけることができます。
シェード

1
私は一般的にこの答えの背後にあるアイデアが好きです。私の経験から、例外が獣の複雑すぎる状態になることはありません。例外の主な目的は、呼び出し元のコードが特定の問題に対処し、関数の応答を乱すことなく関連するデバッグ/再試行情報を取得できるようにすることです。
greggle138

2

エラー応答パターンを設計する際に留意すべき主なことは、呼び出し側にとって有用であることを確認することです。これは、例外を使用している場合でも、定義済みのエラーコードを使用している場合でも適用されますが、例外の説明に限定されます。

  • 言語またはフレームワークがすでに一般的な例外クラスを提供している場合それらが適切であり、合理的に予想される場所でそれらを使用します。独自のクラスArgumentNullExceptionまたはArgumentOutOfRange例外クラスを定義しないでください。発信者はそれらをキャッチすることを期待しません。

  • MyClientServerAppExceptionアプリケーションのコンテキスト内で一意のエラーを含むように基本クラスを定義します。基本クラスのインスタンスをスローしないでください。あいまいなエラー応答は史上最悪です。「内部エラー」がある場合は、そのエラーが何であるかを説明します。

  • ほとんどの場合、基本クラスの下の階層は深くなく、広くする必要があります。呼び出し側にとって有用な状況でのみ深める必要があります。たとえば、クライアントからサーバーへのメッセージが失敗する可能性がある5つの理由がある場合、ServerMessageFault例外を定義し、その下にある5つのエラーごとに例外クラスを定義できます。そうすれば、呼び出し元は必要に応じて、または必要に応じてスーパークラスをキャッチできます。これを特定の合理的なケースに制限してください。

  • 実際に使用する前に、すべての例外クラスを定義しようとしないでください。そのほとんどをやり直すことになります。コードの作成中にエラーが発生した場合、そのエラーを説明する最善の方法を決定します。理想的には、発信者がやろうとしていることのコンテキストで表現する必要があります。

  • 前の点に関連して、エラーに応答するために例外を使用するからといって、エラー状態に例外のみを使用する必要があるわけではないことに注意してください。例外のスローは通常高価であり、パフォーマンスコストは言語ごとに異なる場合があることに注意してください。一部の言語では、コールスタックの深さによってコストが高くなるため、エラーがコールスタック内にある場合は、プッシュに単純なプリミティブ型(整数エラーコードまたはブールフラグ)を使用できないかどうかを確認します。エラーはスタックをバックアップするため、呼び出し元の呼び出しの近くにスローできます。

  • エラー応答の一部としてロギングを含める場合、呼び出し元がコンテキスト情報を例外オブジェクトに簡単に追加できます。ログコードで情報が使用されている場所から開始します。ログが有用であるために必要な情報量を決定します(テキストの巨大な壁ではありません)。次に、逆方向に作業して、例外クラスにその情報を簡単に提供できるようにします。

最後に、アプリケーションができない限り、絶対にメモリ不足エラーで優雅に扱う、それらの、または他の致命的な実行時例外に対処しようとしないでください。OSにそれを処理させてください。実際には、それがあなたにできるすべてだからです。


2
最後から2番目の箇条書き(例外のコストについて)を除き、答えは良いです。例外のコストに関する箇条書きは誤解を招く可能性があります。低レベルで例外を実装するすべての一般的な方法では、例外をスローするコストは呼び出しスタックの深さに完全に依存しないためです。エラーが即時の呼び出し元によって処理されることを知っている場合、例外をスローする前に呼び出しスタックからいくつかの関数を取得するのではなく、代替のエラー報告方法の方が良い場合があります。
バートヴァンインゲンシェ

@BartvanIngenSchenau:これを特定の言語に結び付けないようにしました。一部の言語(Javaなど)では、呼び出しスタックの深さがインスタンス化のコストに影響します。私はそれがあまりにもカットされていないことを反映するように編集します
マークベニングフィールド

0

さて、まず最初にException、アプリケーションによってスローされる可能性のあるすべてのチェック済み例外の基本クラスを作成することをお勧めします。アプリケーションが呼び出された場合DuckTypeDuckTypeException基本クラスを作成します。

これによりDuckTypeException、処理のために基本クラスの例外をキャッチできます。ここから、例外は、問題のタイプをより強調できる説明的な名前で分岐するはずです。たとえば、「DatabaseConnectionException」。

明確にしましょう。これらはすべて、プログラムで適切に処理したい状況で発生する可能性のある例外をすべてチェックする必要があります。言い換えれば、データベースに接続できないため、a DatabaseConnectionExceptionがスローされ、それをキャッチして、一定時間後に待機して再試行することができます。

あなたは考えていない、そのような不正なSQLクエリまたはnullポインタ例外として、非常に予想外の問題のチェック例外を参照してください、と私は、これらの例外は、ほとんどのcatch節を超越(またはキャッチし、必要に応じて再スロー)あなたがメインに到着するまでできるようにすることをお勧めしますコントローラー自体は、RuntimeExceptionログを記録する目的でのみをキャッチできます。

私の個人的な好みはRuntimeException、未チェックの例外の性質上、あなたはそれを期待しないので、別の例外として未チェックを再スローしないことです。それはあなたの好みである場合は、あなたはまだキャッチできるRuntimeExceptionと投げDuckTypeInternalExceptionているとは違っDuckTypeExceptionから派生しRuntimeException、したがって、未チェックです。

必要に応じて、DatabaseExceptionデータベース関連など、組織上の目的で例外をサブカテゴリに分類できますが、そのようなサブ例外はベース例外から派生し、DuckTypeException抽象的であるため、明示的な記述名で派生することをお勧めします。

一般的なルールとして、例外を処理するために呼び出し元の呼び出しスタックを上に移動するにつれて、try catchはますます一般的になり、メインコントローラーでは、チェックされた例外のすべてが派生するDatabaseConnectionException単純なものDuckTypeExceptionを処理します。


2
質問には「C ++」というタグが付いていることに注意してください。
マーティンBa

0

それを単純化してみてください。

別の戦略で考えるのに役立つ最初のことは、多くの例外をキャッチすることは、Javaからのチェック例外の使用に非常に似ています(申し訳ありませんが、私は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;
    }
}

そしてあなたのユニークな例外:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

このリンクで見つけこの戦略と、ここでJava実装を見つけることができます。上記のコードは簡略化されているため、詳細を確認できます。

「クライアント」アプリケーションと「別のサーバー」アプリケーション間で異なる例外を分離する必要があるため、ErrorCodeインターフェースを実装する複数のエラーコードクラスを持つことができます。


2
質問には「C ++」というタグが付いていることに注意してください。
シェード

アイデアに合わせて編集しました。
デリック

0

例外は無制限のgotoであり、注意して使用する必要があります。それらに対する最善の戦略は、それらを制限することです。呼び出し側の関数は、呼び出し側の関数によってスローされたすべての例外を処理するか、プログラムが終了する必要があります。例外を処理するための正しいコンテキストを持っているのは、呼び出し元の関数のみです。呼び出しツリーのさらに上位の関数にそれらを処理させることは、無制限のgotoです。

例外はエラーではありません。状況によって、プログラムがコードの1つのブランチを完了できず、別のブランチが続くことを示している場合に発生します。

例外は、呼び出された関数のコンテキスト内にある必要があります。例:二次方程式を解く関数。division_by_zeroとsquare_root_of_negative_numberの2つの例外を処理する必要があります。しかし、これらの例外は、この二次方程式ソルバーを呼び出す関数には意味がありません。方程式を解くために使用される方法が原因で発生し、単純に再スローすると内部が露出し、カプセル化が解除されます。代わりに、division_by_zeroはnot_quadraticとsquare_root_of_negative_numberとno_real_rootsとして再スローする必要があります。

例外の階層は必要ありません。呼び出し元の関数は例外を処理する必要があるため、関数によってスローされる例外の列挙(それらを識別する)で十分です。呼び出しツリーを処理できるようにすることは、コンテキスト外(無制限)のgotoです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.