私の同僚と私は、APIの正しい設計について議論しています。関数void deleteBlogPost(int postId)
があるとします。 postId
で索引付けされたブログ投稿が存在しない場合、この関数は何をすべきですか?
関数は1つのことを実行するように設計する必要があるため、例外をスローするのが適切だと思います。ユーザーがdeleteBlogPost
という関数を呼び出すとき、ユーザーは常にID postId
の投稿が削除されることを期待しています。 postId
が無効な投稿を削除しようとしても意味がないため、例外をスローする必要があります。
同僚は、発信者が特定の投稿を実際にdeleteするつもりはなく、通話後の投稿は存在しません。存在しない投稿IDでdeleteBlogPost
を呼び出すと、目標状態はすでに達成されているため、何も起こりません。彼はまた、この設計はdeleteBlogPost
への呼び出しがべき等であることを保証することにも言及しましたが、これが良いことだとは確信していません。
いくつかのAPIで両方のパターンの例を見つけました。たとえば、辞書/マップエントリの削除を、PythonとJavaの間で存在しないキーと比較します。
Python:
my_dict = {}
del my_dict['test'] # KeyError: 'test'
Java:
Map<String, Object> map = new HashMap<>();
map.remove("test"); // no exception thrown
関数は、予想される動作または目標状態に基づいて例外をスローする必要がありますか?
関数は、予想される動作または目標状態に基づいて例外をスローする必要がありますか?
どちらでもない。これは正反対です。
例外をスローするかどうかの適切な基準は、それが例外的な状況によるものかどうかです。
Java :
例外は例外的な状況でのみ使用してください。例外的な状況とは、通常とは異なる予期しない状況です。他の状況よりも一般的ではないという理由だけで、状況は例外的ではありません。例外ではない状況では、例外の代わりに戻り値を使用する必要があります。
c# :
例外を使用して、通常の実行の一部としてプログラムのフローを変更しないでください。例外は、エラー状態の報告と処理にのみ使用してください。
したがって、実際の問題は、ファイルが欠落していると予想されるかどうか、それがコンテキストと要件を考慮して通常のビジネスケースであるかどうか、または通常は予期しない例外的な動作であるかどうかです。
どちらにせよ、理由を考えることができます。たとえば、高度に並列化されたシステムでは、複数のスレッドが同時にファイルを削除していることがよくある場合、deleteBlogPost
をサイレントに失敗させることができます。一方、ブログの投稿がデリケートなアイテムであり、1回限りの削除が必要で、削除イベントがログに記録され、監査可能である場合、deleteBlogPost
が例外をスローした方がデータの一貫性が確保されます。 。
現時点で、削除が何かを何にもしないことに成功したことを絶対に知っている必要がある場合でも、別の方法で実行できます。
一つは投げることです。例外のスローは(他の方法と比べて)遅くなります。スタックトレースは無料で連結されません。このため、サルをコード化することは、投げることを非常にまれにするという伝統があります。ですから、人々に何かを強制することに注意してください。それは伝統に慣れている人々を混乱させるかもしれません。しかし、この方法では、イベントを無視するのが難しくなります。
別のデザインは戻ることです。これにより、何も連結されなくなります。変に見えるだけです。人々は何も考えていない。なにもしないときに何かを取得するのは奇妙に思えます。しかし、何も使用されていなかったことを原子的に確認する必要がある場合は、何かを返します。ユーザーが安全な場所に置いて、あまり気にしていないかテストしてみましょう。このようにして、例外は必要なく、すべて同じユースケースで機能します。それはただ奇妙に奇妙に見えます。
APIがどのように使用されるかを正確に知ることはできないので、何をサポートするかを決める必要があります。それをすべてサポートしたい場合は、名前を変更することで物事をおかしくないようにすることができます。
Dict.pop()を使用してキーを辞書から削除します
dict.pop(key[, default])
辞書にキーが存在する場合、dict.pop()は指定されたキーを持つ要素を辞書から削除し、その値を返します。
指定されたキーが辞書に存在しない場合、指定されたデフォルト値を返します。
指定されたキーが辞書に存在せず、デフォルト値がpop()に渡されない場合、KeyErrorがスローされます
これで、その動作はそれほど奇妙ではなくなり、APIユーザーは自分で決めることができます。
削除が何かを変更したかどうかをまったく気にしない場合は、何も残っていない限り、べき等性を探しています。
pop
(デフォルト)を使用すると、APIユーザーは戻り値を無視するだけでべき等性を実現できます。
べき等性(繰り返し呼び出して同じ結果を期待できる能力)の関連性は、APIユーザーが決定する必要があります。 APIの作成者として判断できるのは、それをサポートする問題に値するかどうかだけです。
どちらの方法にも長所と短所があり、それぞれに適用できる状況があります。これはクリーンアップ中の処理が簡単なため、「スローなし」バージョンを好みます。何かを削除しようとした場合、オブジェクトがコンテナー内にあるかどうかは通常気にしません。もう存在しないことを確認したいだけです。見つからない場合にリムーバーがスローする場合、一部の種類のクリーンアップコードでは、try/catchブロックを実行するか、オブジェクトが最初にコンテナーにあるかどうかを確認する必要があります。また、これらのハンドラーまたはチェックを追加することを忘れがちであり、この条件が処理されない場合、一連の問題が発生します。
3番目の可能性は、オブジェクトが削除されたかどうかにかかわらず、remove関数がインジケーターを返すようにすることです。単純なブール値、または削除されたオブジェクトの数を指定できます。これにより、remove関数の呼び出し元が、オブジェクトが見つからない場合の処理方法を決定できます。オブジェクトが存在しないことを確認するだけの場合は、追加のオーバーヘッドは必要ありません。
どちらのアプローチも有効であり、正しいと見なすことができると私は考えます。問題は、どのアプローチがニーズに合うかです。
エラーとして扱うと、アプリケーションのバグをより迅速に特定するのに役立つ場合があります。ただし、プログラムの通常の実行が原因で状況が発生する可能性がある場合は、独自の問題が発生します。
すでに閉じられているDBへの接続を閉じようとすると、例外をスローするOracleドライバーの非常に迷惑な「機能」を思い出しました。これは、クローズ手順の間にたまにランダムに発生するものでした。しかし、実際の問題はありませんでした。目的は接続を閉じることでしたが、閉じられました。何もする必要はありません。
一方、この状況が発生する唯一の方法が何らかのプログラミングまたは実行エラーによるものである場合は、例外をスローすることに傾倒します。デバッグにかかる時間を短縮できます。
関数void deleteBlogPost(int postId)
の結果を定義する方法はいくつかあります。
残念ながら、両方の定義はあなたに嘘をついています。考えられる結果は2つしかなく、開発者はそれらをsuccessおよびfailureと解釈することがよくあります。あなたとあなたの同僚は同じ罠に陥りましたが、成功とは何か、失敗とは何であるかについては同意しません。真実は、関数にはより多くの結果があるということです:
これらをsuccessとfailureに分類しようとすると、常にどのカテゴリ投稿が存在しないため削除されなかったが属するかについて意見の不一致が発生します。に。残念ながら、シグネチャvoid deleteBlogPost(int postId)
は2つのカテゴリの余地しか残していません。成功することも、失敗することもあります(例外を介して)。シグネチャは、これ以上発生する可能性があることを伝えていません。
正しく通信したい場合は、関数のシグネチャを変更する必要があります。たとえば、戻り値をPOST_DELETED
、POST_DID_NOT_EXIST
、PERMISSION_ERROR
、CORRUPT_DATABASE
の値を持つ列挙型に変更して、例外を削除できます。このようにして、すべての結果が明確に伝えられます。ただし、デメリットは、関数がサイレントに失敗する可能性があることです(例外はもうないため)。
例外の動作を制御する引数を追加することで、例外を保持できます。たとえば、値THROW_IF_POST_NOT_EXISTS
およびDONT_THROW_IF_POST_NOT_EXISTS
(またはより読みやすいもの)を持つ列挙型eExceptionBehavior
を追加できます。このような関数を呼び出すと
deleteBlogPost(42, THROW_IF_POST_NOT_EXISTS);
誰もが何が起こっているか知っています。
例外は、その名前が既に示しているように、例外的な状況を伝えることを目的としています。 「予期しない状況」と言うこともできます。だから問題は:
存在しない投稿を削除するリクエストが実行されるのは予想外ですか?
例外は予想される失敗を伝えることを意図したものではありませんが、予想される失敗とそうでないものを決定するのはユーザー次第です。
例えば。 HTTPフレームワークを作成する場合、リソースをフェッチする前にリソースが存在するかどうかを無理に知ることができるため、リソースをフェッチしようとすると、404ページが見つかりませんというエラーが予期される失敗になります。例外的な失敗は、TCP接続がリソースの転送の途中で停止した場合です。
"存在しない投稿を削除してはいけません"と言った場合、これはプログラミングエラーになるため、削除しようとすると例外がスローされます。
「まだ存在するかどうか事前にわからないため、存在しない投稿を削除しようとしても大丈夫です」と言った場合、その投稿は例外ではありません。存在しませんが、データベースを削除しようとしたときにデータベースへの接続が失われた場合、それは予期されていないため、例外になります。
投稿が削除されたかどうかを知る必要がある場合は、投稿を削除できなかったという事実に特別な処理が必要であるため、成功/失敗を伝えます。
bool deleteBlogPost ( int postId )
関数は、投稿が削除された場合はtrue
を返し、それ以外の場合はfalse
を返し、何かがうまくいかなかった場合は例外をスローします。
目標状態です。だけでなく...
概念的には、混乱を解消できない場合、例外をスローする必要があると考えることができます。混乱をクリーンアップすることは、それが聞こえるほどあいまいです。しかし、それを明確かつ単純にしましょう:
プログラミング構造としての例外の残念な採用は、それが解決するよりも多くの問題を引き起こす可能性があります。だからあなたの質問への答えはそれが問題ではないということです。 何を設計/モデル化したいかに基づいて、あなたが決定する何を処理し、何を処理しないかを決定します。何も処理しないことにした場合は、例外をスローする必要があります。それだけでなく、通知 APIを使用する他のすべての人にも、 do 例外をスローするように知らせる必要があります。
コードを記述するときは常に、メソッドと動作に署名を使用します。これらの署名は、誰かがあなたに期待できることを保証します。例外とは、あなたが purport できること に基づいて、どのような状況でも 合理的な方法 で処理できないことを意味します。メソッド/関数は、完全に無生物のブラックボックスではありません。彼らは行動の形でドメイン知識をencodeします。通常、彼らは行動をモデル化するので、最良のアドバイスはモデル化されたドメイン/行動からのみ得ることができます。例外は私たちの生活を楽にするために作成されました。
しかし結局、それは問題ではありません。さまざまな言語で使用されているさまざまなパターンで見たように、メソッドを設計しても、APIを使用する人々はそれに慣れ、使用します。いつ、または例外をスローするかは関係ありません。
throws
キーワード )を適切に伝え、全員が準備できるようにします。try
{
double x = double.Parse(str);
}
catch (Exception ex) //Or any other exception
{
//...
}
または
if (dict.ContainsKey(key))
{
var value = dict[key];
//...
}
aPIを利用するコードベース全体の数十の場所.