私はこの問題についてしばらく考えてきましたが、自分が警告と矛盾を見つけ続けているので、誰かが次のことについて結論を出せることを願っています。
私が知る限り、4年間の業界での作業、本やブログの閲覧などから、エラーを処理するための現在のベストプラクティスは、エラーコード(必ずしもエラーコードではなく、エラーを表す型)。
しかし-私にはこれは矛盾しているようです...
インターフェースまたは抽象化をコーディングして、結合を減らします。インターフェースの特定のタイプと実装がわからない、または知りたい。では、どのような例外をキャッチする必要があるのか、どうしたらわかるでしょうか。実装は、10の異なる例外をスローするか、または何もスローしません。例外をキャッチしたとき、実装について仮定しているのでしょうか?
でなければ-インターフェースは...
一部の言語では、特定のメソッドが特定の例外をスローすることを開発者が述べることができます(たとえば、Javaはthrows
キーワードを使用します)。呼び出しコードの観点から、これは問題がないように見えます。どの例外をキャッチする必要があるかを明示的に知っています。
しかし-これは...を示唆しているようです...
なぜインターフェイスはスローできる例外を指定する必要があるのですか?実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?インターフェイスレベルでは、実装でスローする必要のある例外を知る方法はありません。
そう...
例外が(私の目で)ソフトウェアのベストプラクティスに矛盾しているように見えるのに、なぜ優先されるのですか?また、エラーコードが非常に悪い場合(およびエラーコードの悪質なものを販売する必要がない場合)、別の方法はありますか?上記のベストプラクティスの要件を満たしているが、エラーコードの戻り値をチェックするコードの呼び出しに依存しない、エラー処理の現在(または間もなく)の最先端技術とは何ですか?
まず第一に、私はこの声明に同意しません。
エラーコードよりも例外を優先する
これは常に当てはまるわけではありません。たとえば、(Foundationフレームワークを使用した)Objective-Cを見てください。ここで、NSErrorは、Java開発者が真の例外と呼ぶものがあるにもかかわらず、@ try、@ catch、@ throw、NSExceptionクラスなどが存在するにもかかわらず、エラーを処理するための推奨される方法です。
ただし、例外がスローされると、多くのインターフェイスが抽象化をリークすることは事実です。これは「例外」スタイルのエラーの伝播/処理のせいではない、と私は信じています。一般に、エラー処理に関する最良のアドバイスは次のとおりです。
エラー/例外を可能な限り低いレベルで処理する、期間
その経験則に固執すれば、抽象化による「漏れ」の量は非常に制限されて封じ込められると思います。
メソッドによってスローされた例外がその宣言の一部であるべきかどうかについて、私はそれらがすべきだと信じています:それらはこのインターフェースによって定義されたcontractの一部です:このメソッドAを実行するか、BまたはCで失敗します。
たとえば、クラスがXMLパーサーの場合、その設計の一部は、提供されたXMLファイルがまったく間違っていることを示すことです。 Javaでは、通常、発生することが予想される例外を宣言し、メソッドの宣言のthrows
部分に追加することでそうします。一方、解析アルゴリズムの1つが失敗した場合、その例外を未処理の上で渡す理由はありません。
優れたインターフェース設計インターフェースを十分に設計していれば、例外の数に悩まされることはありません。さもなければ、それはあなたを悩ませることになる単なる例外ではありません。
また、Javaの作成者には、メソッドの宣言/定義への例外を含めるという非常に強力なセキュリティ上の理由があったと思います。
最後にもう1つ:一部の言語(Eiffelなど)には、エラー処理のための他のメカニズムがあり、スロー機能は含まれていません。そこでは、ルーチンの事後条件が満たされない場合、ソートの「例外」が自動的に発生します。
例外とエラーコードは、エラーと代替コードパスを処理する唯一の方法ではないことに注意してください。
頭の中で、Haskellが採用しているようなアプローチをとることができます。エラーは、複数のコンストラクターを持つ抽象データ型を介して通知できます(弁別された列挙型またはnullポインターを考えますが、タイプセーフであり、構文を追加する可能性があります)コードフローを美しくするための砂糖またはヘルパー関数)。
func x = do
a <- operationThatMightFail 10
b <- operationThatMightFail 20
c <- operationThatMightFail 30
return (a + b + c)
operationThatMightfailは、Maybeにラップされた値を返す関数です。これはnull可能ポインターのように機能しますが、do表記は、a、b、またはcのいずれかが失敗した場合に全体がnullと評価されることを保証します。 (そしてコンパイラーは、偶発的なNullPointerExceptionを発生させないように保護します)
別の可能性は、呼び出すすべての関数に追加の引数としてエラーハンドラーオブジェクトを渡すことです。このエラーハンドラーには、渡す可能性のある「例外」ごとにメソッドがあり、渡す関数によって通知できます。また、例外を介してスタックを巻き戻す必要がなく、例外が発生した場所でその関数を使用して例外を処理できます。
Common LISPはこれを行い、シンタティックサポート(暗黙の引数)を備え、組み込み関数がこのプロトコルに従うことで実現可能にします。
はい、例外はリークのある抽象化を引き起こす可能性があります。しかし、この点でエラーコードはさらに悪くないのでしょうか?
この問題に対処する1つの方法は、どのような状況でスローできる例外をインターフェイスで正確に指定し、必要に応じて例外をキャッチ、変換、再スローすることにより、実装が内部例外モデルをこの仕様にマップする必要があることを宣言することです。 「完璧な」インターフェースが必要な場合は、それで十分です。
実際には、通常、論理的にインターフェースの一部であり、クライアントがキャッチして何かしたい例外を指定するだけで十分です。一般に、低レベルのエラーが発生したりバグが発生したりすると、他の例外が発生する可能性があり、クライアントがエラーメッセージを表示したり、アプリケーションをシャットダウンしたりすることによってのみ処理できる例外があります。少なくとも例外には、問題の診断に役立つ情報を含めることができます。
実際、エラーコードを使用すると、ほとんど同じことが、より暗黙的な形で発生し、情報が失われ、アプリが一貫性のない状態になる可能性がはるかに高くなります。
ここにはたくさんの良いものがありますが、通常の制御フローの一部として例外を使用するコードにはすべて注意する必要があることを付け加えておきます。時々人々はその罠に陥り、通常のケースではないものは例外になる。ループ終了条件として使用される例外を見たこともあります。
例外とは、「ここで処理できないことが発生したため、他の誰かのところに行って何をすべきかを理解する必要がある」という意味です。ユーザーが無効な入力を入力した場合も例外ではありません(再度入力するなどして、入力によってローカルで処理する必要があります)。
私が見た例外使用の別の退化したケースは、最初の応答が「例外をスローする」である人々です。これはほとんどの場合、catchを記述せずに行われます(経験則:最初にcatchを記述し、次にthrowステートメントを記述します)。大きなアプリケーションでは、キャッチされない例外がネザー領域からバブルアップしてプログラムを爆破すると、これが問題になります。
私は反例外ではありませんが、数年前のシングルトンのように見えます。使用頻度が高すぎて不適切です。それらは意図された用途に最適ですが、そのケースは一部の人が考えるほど広くはありません。
漏れやすい抽象化
なぜインターフェイスはスローできる例外を指定する必要があるのですか?実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?インターフェイスレベルでは、実装でスローする必要のある例外を知る方法はありません。
いいえ。例外の仕様は、戻り値および引数の型と同じバケットにあります-それらはインターフェースの一部です。その仕様に準拠できない場合は、インターフェースを実装しないでください。あなたが投げたことがないなら、それは大丈夫です。インターフェースで例外を指定することに漏れはありません。
エラーコードはひどいものです。彼らはひどいです。呼び出しごとに、毎回手動で確認して伝達することを忘れないでください。これは、まずDRYに違反し、エラー処理コードを大幅に破壊します。この繰り返しは、例外が直面するどの問題よりもはるかに大きな問題です。例外を黙って無視することは決してできませんが、人々は黙って戻りコードを無視することができますし、無視することは間違いなく悪いことです。
IM-very-HO例外はケースバイケースで判断されます。これは、制御フローを壊すことにより、実際の知覚されるコードの複雑さが増すため、多くの場合、不必要に増加するためです。関数内での例外のスローに関連する説明はさておき、制御フローを実際に改善する可能性があります。コール境界を介して例外をスローすることを検討する場合は、以下を検討してください。
呼び出し先が制御フローを壊すことを許可しても、実際の利点は得られない可能性があり、例外に対処する意味のある方法がない場合があります。直接的な例として、Observableパターンを実装している場合(C#など、あらゆる場所にイベントがあり、定義にthrows
が明示されていない場合)、オブザーバーに制御を破壊させる実際の理由はありません。それがクラッシュした場合のフロー、およびそれらのものに対処する意味のある方法はありません(もちろん、良い隣人は観察時に投げるべきではありませんが、誰も完璧ではありません)。
上記の観察は、(指摘したように)疎結合インターフェースに拡張できます。 3-6個のスタックフレームを這い上がった後、キャッチされない例外が次のいずれかのコードのセクションで終わる可能性が高いのは、実際には標準だと思います。
上記を考慮すると、throws
セマンティクスでインターフェースを装飾することは、機能上のわずかな利益にすぎません。なぜなら、インターフェースコントラクトを介した多くの呼び出し元は、失敗した場合にのみ気にかけ、理由は気にしません。
次に、それは好みと利便性の問題になると思います。主な焦点は、「例外」の後で発信者と着信者の両方の状態を優雅に回復することです。したがって、エラーコードの移動に多くの経験がある場合( Cの背景から)、または例外が悪をもたらす可能性がある環境で作業している場合(C++)、私はものを投げることがNice、clean OOPあなたがそれに慣れていなければ、あなたは古いパターンに頼ることができないこと、特にそれがSoCの破壊につながる場合。
理論的な観点から、例外を処理するSoC-kosherの方法は、直接の呼び出し元がほとんどの場合、失敗したことだけを気にしているという観察から直接導き出せると思います。理由はわかりません。呼び出し先がスローし、非常に近く(2〜3フレーム)の誰かがアップキャストされたバージョンをキャッチし、実際の例外は常に特殊なエラーハンドラーに沈みます(トレースのみであっても)-これらのハンドラーはAOPが役立つ場所です水平になる可能性があります。
例外処理は独自のインターフェース実装を持つことができます。スローされた例外のタイプに応じて、必要な手順を実行します。
設計上の問題の解決策は、2つのインターフェース/抽象化の実装を持つことです。 1つは機能用で、もう1つは例外処理用です。キャッチした例外のタイプに応じて、適切な例外タイプクラスを呼び出します。
エラーコードの実装は、例外を処理する正統な方法です。これは、文字列と文字列ビルダーの使用法に似ています。
エラーコードよりも例外を優先する
両方が共存する必要があります。
特定の動作が予想される場合は、エラーコードを返します。
何らかの動作を予期していなかった場合は、例外を返します。
エラーコードは通常、例外タイプが残っている場合に単一のメッセージに関連付けられますが、メッセージは異なる場合があります
例外にはスタックトレースがありますが、エラーコードにはありません。エラーコードを使用して、壊れたシステムをデバッグしません。
実装ではなくインターフェースへのコーディング
これはJavaに固有の可能性がありますが、インターフェースを宣言するとき、そのインターフェースの実装によってスローされる可能性のある例外を指定していませんが、意味がありません。
例外をキャッチしたとき、実装について仮定しているのでしょうか?
これは完全にあなた次第です。非常に特殊なタイプの例外をキャッチしてから、より一般的なException
をキャッチできます。例外をスタックに伝播させてから処理しませんか?あるいは、例外処理が「プラグイン可能な」アスペクトになるアスペクトプログラミングを見ることができます。
実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?
それがなぜあなたにとって問題なのか、私にはわかりません。はい、失敗しないか例外をスローする実装が1つあり、常に失敗して例外をスローする別の実装がある場合があります。その場合は、インターフェイスで例外を指定しないでください。問題は解決します。
例外ではなく実装が結果オブジェクトを返した場合、何かが変更されますか?このオブジェクトには、エラーや失敗がある場合は、そのアクションの結果が含まれます。その後、そのオブジェクトに問い合わせることができます。
例外により、エラーを報告および処理するためのより構造化された簡潔なコードを記述できることがわかります。エラーコードを使用するには、呼び出しごとに戻り値を確認し、予期しない結果が発生した場合の対処方法を決定する必要があります。
一方、例外により、インターフェイスを呼び出すコードから隠すべき実装の詳細が明らかになることに同意します。どのコードがどの例外をスローできるかをアプリオリに知ることはできないため(Javaのようにメソッドシグネチャで宣言されていない限り)、例外を使用して、コードのさまざまな部分の間に非常に複雑な暗黙の依存関係を導入しています。依存関係を最小限に抑えるという原則に反する。
要約:
漏れやすい抽象化
なぜインターフェイスはスローできる例外を指定する必要があるのですか?実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?インターフェイスレベルでは、実装でスローする必要のある例外を知る方法はありません。
私の経験では、エラーを受け取るコード(例外、エラーコードなど)は、通常、エラーの正確な原因を考慮していません。エラー(エラーダイアログまたは何らかのログ)このレポートは、失敗したプロシージャを呼び出したコードと直交して行われます。たとえば、このコードは、特定のエラーを報告する方法(メッセージ文字列のフォーマットなど)を知っている他のコードにエラーを渡し、コンテキスト情報を添付する可能性があります。
もちろん、特定のセマンティクスをエラーに付加し、発生したエラーに基づいて異なる反応をする必要がある場合もありますis。このような場合は、インターフェース仕様で文書化する必要があります。ただし、インターフェースは、特定の意味を持たない他の例外をスローする権利を予約する場合があります。