38

データベース接続を作成するために使用される小さなルーチンを使用していました。

public DbConnection GetConnection(String connectionName)
{
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];
   DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);
   DbConnection conn = factory.CreateConnection();
   conn.ConnectionString = cs.ConnectionString;
   conn.Open();

   return conn;
}

次に、.NET フレームワークのドキュメントを調べて、ドキュメントに記載されているさまざまな動作がどのようなものかを確認し、それらを処理できるかどうかを確認しました。

例えば:

ConfigurationManager.ConnectionStrings...

ドキュメントには、コレクションを取得できなかった場合にConnectionStringsを呼び出すとConfigurationErrorExceptionがスローされることが記載されています。この場合、この例外を処理するためにできることは何もないので、手放します。


次の部分は、 connectionNameを見つけるためのConnectionStringsの実際のインデックス付けです。

...ConnectionStrings[connectionName];

この場合、ConnectionStrings のドキュメントには、接続名が見つからない場合、プロパティはnullを返すと記載されています。私はこれが起こっていることを確認し、例外をスローして、無効な接続名を与えたことを誰かに知らせることができます:

ConnectionStringSettings cs= 
      ConfigurationManager.ConnectionStrings[connectionName];
if (cs == null)
   throw new ArgumentException("Could not find connection string \""+connectionName+"\"");

私は同じ演習を繰り返します:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);

GetFactoryProviderNameメソッドには、指定されたのファクトリが見つからなかった場合に何が起こるかについてのドキュメントがありません。を返すようnullに文書化されていませんが、私はまだ防御的で、nullをチェックすることができます:

DbProviderFactory factory = 
      DbProviderFactories.GetFactory(cs.ProviderName);
if (factory == null) 
   throw new Exception("Could not obtain factory for provider \""+cs.ProviderName+"\"");

次は、DbConnection オブジェクトの構築です。

DbConnection conn = factory.CreateConnection()

繰り返しますが、ドキュメントには、接続を作成できなかった場合に何が起こるかは記載されていませんが、null の戻りオブジェクトを確認できます。

DbConnection conn = factory.CreateConnection()
if (conn == null) 
   throw new Exception.Create("Connection factory did not return a connection object");

次に、Connection オブジェクトのプロパティを設定します。

conn.ConnectionString = cs.ConnectionString;

ドキュメントには、接続文字列を設定できなかった場合にどうなるかは記載されていません。例外をスローしますか?それは無視しますか?ほとんどの例外と同様に、接続の ConnectionString を設定しようとしたときにエラーが発生した場合、そこから回復するためにできることは何もありません。だから私は何もしません。


最後に、データベース接続を開きます。

conn.Open();

DbConnectionのOpen メソッドは抽象的であるため、どのプロバイダーがスローする例外を決定するかは、DbConnection から派生しているプロバイダー次第です。エラーが発生した場合に何が起こるかについて、抽象 Open メソッドのドキュメントにもガイダンスはありません。接続中にエラーが発生した場合、それを処理できないことはわかっています。発信者がユーザーに UI を表示できる場所でバブルを発生させ、再試行させる必要があります。


public DbConnection GetConnection(String connectionName)
{
   //Get the connection string info from web.config
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];

   //documented to return null if it couldn't be found
    if (cs == null)
       throw new ArgumentException("Could not find connection string \""+connectionName+"\"");

   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = DbProviderFactories.GetFactory(cs.ProviderName);

   //Undefined behaviour if GetFactory couldn't find a provider.
   //Defensive test for null factory anyway
   if (factory == null)
      throw new Exception("Could not obtain factory for provider \""+cs.ProviderName+"\"");

   //Have the factory give us the right connection object
   DbConnection conn = factory.CreateConnection();

   //Undefined behaviour if CreateConnection failed
   //Defensive test for null connection anyway
   if (conn == null)
      throw new Exception("Could not obtain connection from factory");

   //Knowing the connection string, open the connection
   conn.ConnectionString = cs.ConnectionString;
   conn.Open()

   return conn;
}

概要

したがって、私の 4 行の関数は 12 行になり、5 分間のドキュメント検索が必要になりました。最後に、メソッドが null を返すことが許可されている 1 つのケースをキャッチしました。しかし、実際には、アクセス違反の例外 (null 参照でメソッドを呼び出そうとした場合) をInvalidArgumentExceptionに変換するだけでした。

また、 nullの戻りオブジェクトが存在する可能性のある 2 つのケースもキャッチします。しかし、繰り返しになりますが、1 つの例外を別の例外と交換しただけです。

良い面としては、2 つの問題を検出し、例外メッセージで何が起こったのかを説明した方が、後で悪いことが起こった (つまり、ここでお金が止まった) ということではありませんでした。

しかし、それは価値がありますか?これはやり過ぎですか?この防御的なプログラミングはうまくいかないのでしょうか?

4

14 に答える 14

31

構成を手動でチェックして例外をスローすることは、構成が欠落している場合にフレームワークに例外をスローさせることに勝るものはありません。とにかくフレームワークメソッド内で発生する前提条件チェックを複製しているだけであり、コードが冗長になり、何のメリットもありません。(実際には、基本の Exception クラスとしてすべてをスローすることによって、情報を削除している可能性があります。通常、フレームワークによってスローされる例外は、より具体的です。)

編集:この答えはやや物議を醸しているように見えるので、少し詳しく説明します:防御的プログラミングは「予期しないことに備える」(または「偏執的になる」)ことを意味し、それを行う方法の1つは、多くの前提条件チェックを行うことです。多くの場合、これは優れたプラクティスですが、すべてのプラクティスと同様に、コストとメリットを比較検討する必要があります。

たとえば、「ファクトリから接続を取得できませんでした」という例外をスローしても、プロバイダーを取得できなかった理由について何も述べられていないため、何のメリットもありません。プロバイダーは null です。したがって、前提条件チェックのコスト (開発時間とコードの複雑さ) は正当化されません。

一方、例外は開発者に問題の解決方法を伝えるのに役立つため、接続文字列構成が存在することを確認するチェックは正当化される場合があります。とにかく次の行で取得する null 例外は、欠落している接続文字列の名前を通知しないため、前提条件チェックは何らかの値を提供します。たとえば、コードがコンポーネントの一部である場合、コンポーネントのユーザーはコンポーネントが必要とする構成を知らない可能性があるため、値は非常に大きくなります。

防御的プログラミングの別の解釈は、エラー状態を検出するだけでなく、発生する可能性のあるエラーまたは例外からの回復も試みる必要があるというものです。これは一般的に良い考えだとは思いません。

基本的に、何かできる例外のみを処理する必要があります。とにかく回復できない例外は、トップレベルのハンドラーに上向きに渡す必要があります。Web アプリケーションでは、最上位のハンドラーはおそらく一般的なエラー ページを表示するだけです。しかし、データベースがオフラインであるか、重要な構成が欠落している場合、ほとんどの場合、やるべきことはあまりありません。

この種の防御的プログラミングが理にかなっている場合は、ユーザー入力を受け入れる場合であり、その入力がエラーにつながる可能性があります。たとえば、ユーザーが入力として URL を提供し、アプリケーションがその URL から何かを取得しようとした場合、URL が正しいように見えることを確認し、リクエストから発生する可能性のある例外を処理することが非常に重要です。これにより、ユーザーに貴重なフィードバックを提供できます。

于 2009-06-27T17:22:24.663 に答える
13

まあ、それはあなたの聴衆が誰であるかによって異なります。

他の多くの人が使用することを期待しているライブラリ コードを書いていて、その使用方法について話してくれない場合、それはやり過ぎではありません。彼らはあなたの努力に感謝します。

(そうは言っても、それを行っている場合は、System.Exception だけでなく、より優れた例外を定義することをお勧めします。これにより、一部の例外をキャッチしたいが他の例外をキャッチしたくない人が簡単にできるようになります。)

しかし、それを自分自身 (またはあなたとあなたの仲間) だけで使用する場合は、明らかにやり過ぎであり、コードが読みにくくなるため、最終的にはおそらく害を及ぼすことになります。

于 2009-06-27T17:23:03.277 に答える
7

チームにこのようなコーディングをさせたいと思っています。ほとんどの人は、防御的プログラミングの要点さえ理解していません。彼らが行う最善の方法は、メソッド全体を try catch ステートメントでラップし、すべての例外を一般的な例外ブロックで処理できるようにすることです!

イアン君に脱帽です。あなたのジレンマは理解できます。私自身も同じことを経験してきました。しかし、あなたがしたことは、何人かの開発者が何時間もキーボードを叩くのを助けたことでしょう。

.net フレームワーク API を使用している場合、何を期待していることを思い出してください。自然に見えるものは何ですか?コードで同じことを行います。

時間がかかることはわかっています。しかし、品質にはコストがかかります。

PS: すべてのエラーを処理してカスタム例外をスローする必要はありません。メソッドは他の開発者のみが使用することを忘れないでください。彼らは、一般的なフレームワークの例外を自分で把握できる必要があります。それは面倒なことではありません。

于 2009-06-27T17:22:37.347 に答える
6

「前」の例には、明確で簡潔であるという違いがあります。

何か問題がある場合は、最終的にフレームワークによって例外がスローされます。例外について何もできない場合は、コール スタックを伝播させたほうがよいでしょう。

ただし、フレームワークの奥深くで例外がスローされ、実際の問題が何であるかが明らかにならない場合があります。有効な接続文字列がないことが問題であるにもかかわらず、フレームワークが「null の無効な使用」などの例外をスローする場合は、例外をキャッチして、より意味のあるメッセージで再スローする方がよい場合があります。

実際のオブジェクトを操作する必要があるため、null オブジェクトを頻繁にチェックします。オブジェクトが空の場合、スローされる例外は控えめに言っても斜めになります。ただし、null オブジェクトが発生することがわかっている場合にのみ、null オブジェクトをチェックします。オブジェクト ファクトリの中には、null オブジェクトを返さないものがあります。代わりに例外をスローし、これらの場合、null のチェックは役に立ちません。

于 2009-06-27T17:33:47.317 に答える
3

私はそのヌル参照チェックロジックを書くつもりはないと思います - 少なくとも、あなたがやった方法ではありません。

アプリケーション構成ファイルから構成設定を取得する私のプログラムは、起動時にそれらの設定をすべてチェックします。私は通常、設定を含む静的クラスを作成し、そのクラスのプロパティ ( ではなくConfigurationManager) をアプリケーションの他の場所で参照します。これには 2 つの理由があります。

まず、アプリケーションが適切に構成されていないと、機能しません。将来データベース接続を作成しようとするときよりも、プログラムが構成ファイルを読み取る時点でこれを知りたいと思います。

第 2 に、構成の有効性を確認することは、構成に依存するオブジェクトの関心事ではありません。事前にそれらのチェックを既に実行している場合、コード全体のあらゆる場所にチェックを挿入することは意味がありません。(確かにこれには例外があります - たとえば、プログラムの実行中に構成を変更し、それらの変更をプログラムの動作に反映させる必要がある長時間実行アプリケーション; このようなプログラムでは、ConfigurationManager設定が必要な場合はいつでも にアクセスしてください。)

GetFactoryとのCreateConnection呼び出しでもnull 参照チェックは行いません。そのコードを実行するためのテスト ケースをどのように作成しますか? それらのメソッドが null を返すようにする方法がわからないため、できません。これらのメソッドが null を返すようにすることが可能であることさえ知らないからです。NullReferenceExceptionつまり、1 つの問題 (プログラムが理解できない条件下でエラーをスローする可能性がある) を別のより重大な問題 (同じ不思議な条件下で、プログラムはテストしていないコードを実行する) に置き換えました。

于 2009-06-28T19:52:50.647 に答える
1

私の経験則は次のとおりです。

スローされた例外のメッセージが呼び出し元に関連する場合はキャッチしません。

したがって、NullReferenceException には関連するメッセージがありません。null かどうかを確認し、より適切なメッセージで例外をスローします。ConfigurationErrorException は関連性があるため、キャッチしません。

唯一の例外は、GetConnection の "コントラクト" が構成ファイル内の接続文字列を必ずしも取得しない場合です。

GetConnection が、接続を取得できなかったというカスタム例外とのコントラクトを持つ必要がある場合は、カスタム例外で ConfigurationErrorException をラップできます。

もう 1 つの解決策は、GetConnection がスローできない (ただし、null を返すことができる) ことを指定してから、"ExceptionHandler" をクラスに追加することです。

于 2009-06-28T20:35:10.717 に答える
1

メソッドのドキュメントがありません。;-)

各メソッドには、いくつかの定義済みの入力パラメーターと出力パラメーター、および定義済みの結果の動作があります。あなたの場合、「成功した場合は有効な開いている接続を返し、そうでない場合はnullを返します(または必要に応じてXXXExceptionをスローします)。この動作を念頭に置いて、プログラムする必要がある防御方法を決定できます。

  • あなたのメソッドが失敗した理由と何が失敗したかの詳細な情報を公開する必要がある場合は、あなたがしたようにそれを行い、すべてをチェックしてキャッチし、適切な情報を返します。

  • ただし、開いている DBConnection または失敗時にnull (またはユーザー定義の例外)だけに関心がある場合は、すべてを try/catch でラップし、エラー時にnull (または何らかの例外) を返し、それ以外の場合はオブジェクトを返します。

つまり、メソッドの動作と期待される出力に依存します。

于 2009-06-27T17:35:26.067 に答える
1

DataAccessFailure一般に、データベース固有の例外はキャッチして、(仮想の)例外などのより一般的なものとして再スローする必要があります。ほとんどの場合、高レベルのコードは、データベースからデータを読み取っていることを知る必要はありません。

これらのエラーを迅速にトラップするもう 1 つの理由は、「そのようなテーブルはありません: ACCOUNTS_BLOCKED」や「ユーザー キーが無効です: 234234」など、メッセージにデータベースの詳細が含まれていることが多いためです。これがエンド ユーザーに伝播する場合、いくつかの点で問題があります。

  1. 紛らわしい。
  2. 潜在的なセキュリティ違反。
  3. あなたの会社のイメージに恥をかかせる (顧客が粗雑な文法でエラー メッセージを読んでいると想像してください)。
于 2009-06-27T17:42:40.910 に答える
0

AppDomain.UnexpectedExceptionアプリケーションに、チェーンとすべてのスタック トレースを何らかのログ ファイルにダンプするexception.InnerException(または、ミニダンプをキャプチャする)ハンドラーがある限り、修正されたバージョンにはあまり価値がありませんEnvironment.FailFast

その情報から、エラー チェックを追加してメソッド コードを複雑にすることなく、問題の原因を特定するのはかなり簡単です。

トップレベルを使用する代わりに、処理AppDomain.UnexpectedExceptionして呼び出す方が望ましいことに注意してください。後者の場合、問題の元の原因がおそらくさらなる例外によって不明瞭になるためです。Environment.FailFasttry/catch (Exception x)

これは、例外をキャッチすると、開いているfinallyブロックが実行され、おそらくさらに例外がスローされ、元の例外が隠されるためです (さらに悪いことに、状態を元に戻そうとしてファイルが削除され、おそらく間違ったファイルが削除される可能性があります。重要なファイルも)。mainトップレベルの関数try/catchブロックであっても、処理方法がわからない無効なプログラム状態を示す例外をキャッチしないでください。処理AppDomain.UnexpectedExceptionと呼び出しEnvironment.FailFastは、ブロックの実行を停止するため、別の (そしてより望ましい) やかんfinallyです。プログラムを停止して、それ以上のダメージを与えずに役立つ情報をログに記録しようとしている場合は、絶対に実行したくありません。あなたのfinallyブロック。

于 2009-06-28T20:51:46.533 に答える
0

OutOfMemoryExceptions を確認することを忘れないでください。発生する可能性があります。

于 2009-06-28T20:51:57.500 に答える
0

Iain の変更は、私には理にかなっているように見えます。

システムを使用していて、それを不適切に使用している場合、誤用に関する最大限の情報が必要です。たとえば、メソッドを呼び出す前に構成に値を挿入するのを忘れた場合、KeyNotFoundException / NullReferenceException ではなく、エラーの詳細を示すメッセージを含む InvalidOperationException が必要です。

それはすべてコンテキストIMOに関するものです。私の時代には、かなり不可解な例外メッセージがいくつか見られましたが、それ以外の場合は、フレームワークからのデフォルトの例外でまったく問題ありません。

一般に、特に他の人が頻繁に使用するものや、通常はエラーの診断が難しいコール グラフの奥深くにあるものを書いている場合は、注意して過ちを犯す方がよいと思います。

私は、コードやシステムの開発者として、それを使用しているだけの人よりも、障害を診断するのに適した立場にあることを常に念頭に置いています。場合によっては、数行のチェック コードとカスタムの例外メッセージを追加するだけで、累積的に何時間ものデバッグ時間を節約できます (また、問題をデバッグするために他の人のマシンに引っ張られることがなくなるため、自分自身の作業が楽になります)。

于 2009-06-28T20:52:35.423 に答える
0

私の目には、あなたの「アフター」サンプルは本当に防御的ではありません。守備は、制御下にある部分をチェックするためです。これは引数になりますconnectionName(nullまたは空をチェックし、ArgumentNullExceptionをスローします)。

于 2009-06-28T20:52:52.867 に答える
0

すべての防御的プログラミングを追加した後で、メソッドを分割してみませんか? 個別のメソッドを保証する一連の個別のロジック ブロックがあります。なんで?次に、一緒に属するロジックをカプセル化し、結果として得られるパブリック メソッドがそれらのブロックを正しい方法で配線するだけだからです。

このようなもの(SOエディターで編集されているため、構文/コンパイラチェックはありません。コンパイルできない可能性があります;-))

private string GetConnectionString(String connectionName)
{

   //Get the connection string info from web.config
   ConnectionStringSettings cs= ConfigurationManager.ConnectionStrings[connectionName];

   //documented to return null if it couldn't be found
   if (cs == null)
       throw new ArgumentException("Could not find connection string \""+connectionName+"\"");
   return cs;
}

private DbProviderFactory GetFactory(String ProviderName)
{
   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = DbProviderFactories.GetFactory(ProviderName);

   //Undefined behaviour if GetFactory couldn't find a provider.
   //Defensive test for null factory anyway
   if (factory == null)
      throw new Exception("Could not obtain factory for provider \""+ProviderName+"\"");
   return factory;
}

public DbConnection GetConnection(String connectionName)
{
   //Get the connection string info from web.config
   ConnectionStringSettings cs = GetConnectionString(connectionName);

   //Get the factory for the given provider (e.g. "System.Data.SqlClient")
   DbProviderFactory factory = GetFactory(cs.ProviderName);


   //Have the factory give us the right connection object
   DbConnection conn = factory.CreateConnection();

   //Undefined behaviour if CreateConnection failed
   //Defensive test for null connection anyway
   if (conn == null)
      throw new Exception("Could not obtain connection from factory");

   //Knowing the connection string, open the connection
   conn.ConnectionString = cs.ConnectionString;
   conn.Open()

   return conn;
}

PS: これは完全なリファクタリングではなく、最初の 2 つのブロックのみを行いました。

于 2009-07-02T12:17:24.187 に答える
0

最初の試みとまったく同じようにコーディングしたでしょう。

ただし、その関数のユーザーは、接続オブジェクトを USING ブロックで保護します。

他のバージョンのように例外を翻訳するのは本当に好きではありません。なぜなら、それが壊れた理由を見つけるのが非常に難しくなるためです (データベースがダウンした? 構成ファイルを読み取る権限がないなど?)。

于 2009-06-28T20:11:27.270 に答える