12

質問:

コンストラクターでのエラー処理の"ベスト プラクティス" と見なされるものとその理由は何ですか?

「ベスト プラクティス」は、Schwartz からの引用、または CPAN モジュールの 50% がそれを使用しているなどです。しかし、一般的なベストプラクティスが実際には最善のアプローチではない理由を説明しているとしても、誰からの十分に合理的な意見があればうれしいです.

このトピックに関する私自身の見解 (何年にもわたる Perl でのソフトウェア開発から得た情報) では、perl モジュールでのエラー処理に対する 3 つの主なアプローチを見てきました (私の意見では、最良のものから最悪のものへとリストされています)。

  1. オブジェクトを構築し、無効なフラグを設定します (通常は " is_valid" メソッド)。多くの場合、クラスのエラー処理を介してエラー メッセージを設定します。

    長所

    • $obj->errors()他のメソッド呼び出しの後と同じように、不正なコンストラクターの後に型呼び出しを使用できるため、標準の (他のメソッド呼び出しと比較して) エラー処理が可能になります。

    • 追加情報を渡すことができます (例: >1 エラー、警告など...)

    • 軽量の「やり直し」/「修正」機能を可能にします。つまり、構築されたオブジェクトが非常に重く、多くの複雑な属性があり、100% 常に問題なく、有効でない唯一の理由が誰かが入力したためである場合日付が正しくない場合は$obj->setDate()、コンストラクター全体を再度実行するオーバーヘッドの代わりに、単純に " " を実行できます。このパターンは必ずしも必要ではありませんが、適切な設計では非常に役立ちます。

    短所:私が知っているものはありません。

  2. " " を返しundefます。

    短所: 最初のソリューションの長所 (グローバル変数以外のオブジェクトごとのエラー メッセージと、重いオブジェクトに対する軽量の "fixme" 機能) のいずれも実現できません。

  3. コンストラクター内で死ぬ。いくつかの非常に狭いエッジケースを除いて、個人的には、この質問の余白にリストするには理由が多すぎるため、これはひどい選択だと考えています。

  4. 更新:明確にするために、まったく失敗しない非常に単純なコンストラクターと、すべてのエラーチェックが発生する重いイニシャライザーメソッドを持つという(それ以外の場合は非常に価値があり、優れた設計の)ソリューションは、どちらかの単なるサブセットであると考えていますこの質問では、ケース #1 (イニシャライザがエラー フラグを設定する場合) またはケース #3 (イニシャライザが停止する場合) を使用します。明らかに、そのようなデザインを選択すると、オプション 2 は自動的に拒否されます。

4

4 に答える 4

8

コンストラクターをどのように動作させたいかによって異なります。

この回答の残りの部分は私の個人的な観察に入りますが、ほとんどの Perl と同様に、ベスト プラクティスは「これを行う 1 つの方法です。必要に応じて選択するか、またはそのままにすることができます」に要約されます。あなたが説明したあなたの好みは完全に有効で一貫しており、誰もあなたに別のことを言うべきではありません.

オブジェクトの構築中に発生する可能性のあるエラーの種類は、実行を停止する必要がある大きな明らかなエラーのみになるように設定しているため、 実際には、構築が失敗した場合は死ぬことを好みます。

一方、それが起こらないことを希望する場合は、フラグ変数をチェックするのと同じくらい簡単に未定義オブジェクトをチェックできるので、1 よりも 2 を好むと思います。これは C ではないため、コンストラクターがこの型のオブジェクトを返さなければならないという強い型指定の制約はありません。したがって、 を返しundef、それをチェックして成功または失敗を確認することは、優れた選択です。

構築失敗の「オーバーヘッド」は、特定のエッジ ケース (オーバーヘッドが発生する前にすぐに失敗できない場合) での考慮事項であるため、方法 1 を好む場合があります。工事。たとえば、構築の外で重い初期化を行うことを好みます。標準化に関しては、コンストラクターが定義済みのオブジェクトを返すかどうかを確認することは、フラグ変数を確認するのと同じくらい良い基準だと思います。

編集:イニシャライザがケース #2 を拒否することについての編集に応じて、イニシャライザがフラグ変数を設定するのではなく、成功または失敗を示す値を単純に返すことができない理由がわかりません。実際には、発生したエラーについてどの程度の詳細が必要かによって、両方を使用したい場合があります。undefしかし、初期化子が成功時と失敗時にtrue を返すことは完全に有効です。

于 2009-09-30T13:55:19.470 に答える
5

私が好む:

  1. コンストラクターでできる限り初期化を行いません。
  2. croak何か問題が発生したときに有益なメッセージを表示します。
  3. 適切な初期化メソッドを使用して、オブジェクトごとのエラー メッセージなどを提供します

さらにundef、有効なオブジェクトを取得したかどうかだけで、クラスのユーザーがエラーが発生した正確な理由を気にしない場合に備えて、(鳴く代わりに) 戻ることは問題ありません。

オブジェクトの内部状態が明確に定義されていない場合にメソッドが呼び出されないようにするために、忘れやすいis_validメソッドや追加のチェックを追加することは軽蔑します。

これらは、ベストプラクティスについては何も述べずに、非常に主観的な観点から述べています。

于 2009-09-30T13:41:51.413 に答える
5

#1 に反対することをお勧めします。これは、記述されないエラー処理コードが増えるためです。たとえば、false を返すだけの場合、これは正常に機能します。

my $obj = Class->new or die "Construction failed...";

しかし、無効なオブジェクトを返すと...

my $obj = Class->new;
die "Construction failed @{[ $obj->error_message ]}" if $obj->is_valid;

また、エラー処理コードの量が増えると、それが書き込まれる確率が低下します。そして、それは直線的ではありません。エラー処理システムの複雑さを増すことで、実際の使用でキャッチされるエラーの量を実際に減らすことができます。

また、任意のメソッドが呼び出されたときに問題の無効なオブジェクトが死ぬことにも注意する必要があります (is_validおよび を除くerror_message)。これにより、さらに多くのコードと間違いの可能性が生じます。

しかし、失敗に関する情報を取得できることには価値があることに同意します。これにより、 false ( でreturnはないreturn undef) を返すことが劣ります。伝統的に、これは DBI のようにクラス メソッドまたはグローバル変数を呼び出すことによって行われます。

my $dbh = DBI->connect($data_source, $username, $password) or die $DBI::errstr;

しかし、A) エラー処理コードを書く必要があり、B) 最後の操作に対してのみ有効であるという問題があります。

一般に、最善の方法は、例外をスローすることcroakです。通常、ユーザーは特別なコードを記述しません。エラーは問題の時点​​で発生し、デフォルトで適切なエラー メッセージが表示されます。

my $obj = Class->new;

ライブラリ コードで例外をスローすることに対する Perl の従来の推奨事項は、失礼であるとして古くなっています。Perl プログラマーは (ついに) 例外を受け入れています。エラー処理コードを何度も何度も書くのではなく、ひどく忘れがちな例外 DWIM を使用します。確信が持てない場合は、autodieを使い始めてください( pjf のビデオをご覧ください)。二度と戻ることはありません。

例外は、ハフマン エンコーディングを実際の使用に合わせます。コンストラクターが機能することを期待し、機能しない場合はエラーが必要になるという一般的なケースは、最小のコードになりました。そのエラーを処理したいというまれなケースでは、特別なコードを書く必要があります。そして、特別なコードはかなり小さいです。

my $obj = eval { Class->new } or do { something else };

すべての呼び出しを でラップしていることに気付いたeval場合、それは間違っています。例外は例外的であるため、そう呼ばれます。上記のコメントのように、ユーザーのために適切なエラー処理が必要な場合は、エラーがスタックにバブルアップするという事実を利用してください。たとえば、適切なユーザー エラー ページを提供し、エラーをログに記録する場合は、次のようにします。

eval {
    run_the_main_web_code();
} or do {
    log_the_error($@);
    print_the_pretty_error_page;
};

どこにでも散在するのではなく、コール スタックの一番上に 1 か所だけ必要です。たとえば、これをより小さな増分で利用できます...

my $users = eval { Users->search({ name => $name }) } or do {
    ...handle an error while finding a user...
};

2 つのことが起こっています。1)Users->search常に真の値を返します。この場合は配列参照です。それは簡単なmy $obj = eval { Class->method } or do仕事をします。それはオプションです。しかし、もっと重要なことは、2) 特別なエラー処理をUsers->search. 内部Users->searchで呼び出されるすべてのメソッドとそれらが呼び出すすべてのメソッド...それらは例外をスローするだけです。そして、それらはすべてある時点で捕捉され、同じように処理されます。例外を気にするポイントで例外を処理すると、はるかにきちんとした、コンパクトで柔軟なエラー処理コードが作成されます。

croak文字列だけではなく、文字列のオーバーロードされたオブジェクトを ing することで、より多くの情報を例外に詰め込むことができます。

my $obj = eval { Class->new }
  or die "Construction failed: $@ and there were @{[ $@->num_frobnitz ]} frobnitzes";

例外:

  • 発信者が何も考えずに正しいことを行う
  • 最も一般的なケースでは最小限のコードしか必要としない
  • 発信者に障害に関する最大限の柔軟性と情報を提供する

Try::Tinyなどのモジュールevalは、例外ハンドラーとしての使用に関するハングの問題のほとんどを修正します。

非常に高価なオブジェクトを持っていて、それを部分的にビルドして続行したいというユースケースについては...私にはYAGNIの匂いがします。本当に必要ですか?または、処理が早すぎる肥大化したオブジェクト設計があります。必要な場合は、構築を続行するために必要な情報を例外オブジェクトに入れることができます。

于 2010-05-18T18:46:13.760 に答える
2

まず、大げさな一般的な観察:

  1. コンストラクターの仕事は次のとおりです。有効な構築パラメーターを指定して、有効なオブジェクトを返します。
  2. 有効なオブジェクトを構築しないコンストラクターはそのジョブを実行できないため、例外生成の完全な候補です。
  3. 構築されたオブジェクトが有効であることを確認することは、コンストラクターの仕事の一部です。不良であることがわかっているオブジェクトを配布し、オブジェクトが有効であることを確認するためにクライアントに依存することは、明らかでない理由で離れた場所で爆発する無効なオブジェクトを巻き込む確実な方法です。
  4. コンストラクター呼び出しの前に、すべての正しい引数が配置されていることを確認するの、クライアントの仕事です。
  5. 例外は、壊れたオブジェクトを手元に置く必要なく、発生した特定のエラーを伝播するためのきめの細かい方法を提供します。
  6. return undef;常に悪い[1]
  7. bIlujDI' yIchegh()Qo'; yIHegh()!

ここで、実際の質問に移ります。これは、「ダーチ、あなたはベストプラクティスとその理由を何と考えていますか」という意味に解釈します。まず、失敗時に偽の値を返すことには Perl の長い歴史があり (たとえば、ほとんどのコアはそのように動作します)、多くのモジュールがこの規則に従っていることに注意してください。しかし、この規則は劣悪なクライアント コードを生成し、新しいモジュールはそれから離れていくことが判明しています[2]。

[これを裏付ける議論とコード サンプルは、 autodieの作成を促した例外のより一般的なケースであることが判明したため、ここでそのケースを作成する誘惑に抵抗します。その代わり:]

作成が成功したかどうかを確認することは、実際には、適切な例外処理レベルで例外を確認するよりも面倒です。他のソリューションでは、オブジェクトを取得するためだけに必要な作業よりも多くの作業を直接のクライアントが行う必要があります。これは、コンストラクターが例外をスローして失敗した場合に必要とされない作業です。[3] undef例外は、エラーを文書化し、コール スタックのさまざまなレベルでそれらに注釈を付ける目的で、破損したオブジェクトを返すよりもはるかに表現力があり、同等に表現力があります。

例外で戻すと、部分的に構築されたオブジェクトを取得することもできます。これは、コンストラクターとクライアントとの契約がどうあるべきかについての私の信念によると、悪い習慣だと思いますが、動作はサポートされています。ぎこちない。

したがって、有効なオブジェクトを作成できないコンストラクターは、できるだけ早く例外をスローする必要があります。コンストラクターがスローできる例外は、そのインターフェースの一部として文書化する必要があります。意味のある例外に対処できる呼び出しレベルだけが例外を探す必要があります。多くの場合、「この構築が失敗した場合は何もしない」という動作は正確です。

[1]: つまり、return;が厳密に優れていないユースケースを認識していません。誰かがこれについて私に電話した場合、実際に質問を開く必要があるかもしれません. だからしないでください。;)
[2]: 私が過去 2 年間に読んだモジュール インターフェイスの非常に非科学的な回想によると、選択バイアスと確認バイアスの両方が影響しています。
[3]: 例外のスローには、他の提案されたソリューションと同様に、エラー処理が必要であることに注意してください。evalこれは、実際にすべての構成で複雑なエラー処理を行いたい場合を除いて、すべてのインスタンス化を でラップすることを意味するものではありません(そう思っている場合は、おそらく間違っています)。これは、例外に対して有意義に動作できる呼び出しを でラップすることを意味しevalます。

于 2010-05-14T01:20:47.630 に答える