10

クラスの機能をテストするために .NET で使用されるかなり一般的なパターンがあります。ここでは例として Stream クラスを使用しますが、問題はこのパターンを使用するすべてのクラスに当てはまります。

このパターンは、機能 XXX がクラスで使用可能であることを示すために、CanXXX と呼ばれるブール値のプロパティを提供することです。たとえば、Stream クラスには、Read、Write、および Seek メソッドを呼び出すことができることを示す CanRead、CanWrite、および CanSeek プロパティがあります。プロパティの値が false の場合、それぞれのメソッドを呼び出すと、NotSupportedException がスローされます。

ストリーム クラスに関する MSDN ドキュメントから:

基になるデータ ソースまたはリポジトリによっては、ストリームがこれらの機能の一部しかサポートしない場合があります。アプリケーションは、CanRead、CanWrite、および CanSeek プロパティを使用して、ストリームの機能を照会できます。

CanRead プロパティのドキュメント:

派生クラスでオーバーライドされると、現在のストリームが読み取りをサポートしているかどうかを示す値を取得します。

Stream から派生したクラスが読み取りをサポートしていない場合、Read、ReadByte、および BeginRead メソッドを呼び出すと、NotSupportedException がスローされます。

次の行に沿って書かれたコードがたくさんあります。

if (stream.CanRead)
{
    stream.Read(…)
}

たとえば、ストリーム オブジェクトを何らかの方法でロックするための同期コードがないことに注意してください。他のスレッドがストリーム オブジェクトまたはそれが参照するオブジェクトにアクセスしている可能性があります。NotSupportedException をキャッチするコードもありません。

MSDN のドキュメントには、プロパティ値が時間の経過とともに変更できないとは記載されていません。実際、ストリームが閉じられると CanSeek プロパティは false に変化し、これらのプロパティの動的な性質を示しています。そのため、上記のコード スニペットの Read() の呼び出しが NotSupportedException をスローしないという契約上の保証はありません。

この潜在的な問題に悩まされているコードがたくさんあると思います。この問題を特定した人たちは、どのように対処したのだろうか。ここで適切な設計パターンは何ですか?

また、このパターン (CanXXX、XXX() のペア) の妥当性についてコメントをいただければ幸いです。私にとって、少なくとも Stream クラスの場合、これは多すぎることをしようとしているクラス/インターフェースを表し、より基本的な部分に分割する必要があります。厳密で文書化された契約がないため、テストが不可能になり、実装がさらに難しくなります!

4

6 に答える 6

4

さて、これが私の他の答えよりも役立つことを願っている別の試みです...

CanRead残念なことに、MSDN は時間の経過とともに/ CanWrite/CanSeekがどのように変化するかについて具体的な保証を与えていません。ストリームが読み取り可能である場合、ストリームが閉じられるまで引き続き読み取り可能であると仮定するのが合理的だと思います-同じことが他のプロパティにも当てはまります

場合によっては、ストリームを後でシーク可能にするのが合理的だと思います。たとえば、基になるデータの最後に到達するまで読み取ったすべてをバッファリングし、後でストリーム内をシークして、クライアントがデータを再読み取りできるようにする場合があります。ただし、アダプターがその可能性を無視するのは合理的だと思います。

これにより、最も病的なケースを除くすべてのケースが処理されます。(大混乱を引き起こすように設計されたストリーム!) これらの要件を既存のドキュメントに追加することは、実装の 99.9% が既にそれに従っていると思われるにもかかわらず、理論的には破壊的な変更です。それでも、Connectで提案する価値があるかもしれません。

ここで、「機能ベース」の API ( などStream) とインターフェイス ベースの API を使用するかどうかの議論については、基本的な問題は、変数が複数のインターフェースの実装への参照になります。たとえば、次のように書くことはできません。

public static Foo ReadFoo(IReadable & ISeekable stream)
{
}

これ許可されている場合、それは合理的かもしれませんが、それがなければ、潜在的なインターフェースが爆発的に増加することになります:

IReadable
IWritable
ISeekable
IReadWritable
IReadSeekable
IWriteSeekable
IReadWriteSeekable

それは現在の状況よりも厄介だと思いますが、既存のクラスに加えて、ちょうどという考えを支持すると思います。これにより、クライアントが必要なものを宣言的に表現しやすくなります。IReadableIWritableStream

Code Contractsを使用すると、APIは提供するものと必要なものを宣言できます。

public Stream OpenForReading(string name)
{
    Contract.Ensures(Contract.Result<Stream>().CanRead);

    ...
}

public void ReadFrom(Stream stream)
{
    Contract.Requires(stream.CanRead);

    ...
}

静的チェッカーがそれをどの程度支援できるか、またはストリームが閉じられたときにストリーム読み取り不能/書き込み不能になるという事実にどのように対処するかはわかりません。

于 2009-08-01T11:22:40.227 に答える
3

オブジェクトの内部構造を知らなければ、オブジェクトが複数のスレッドで変更されている場合、「フラグ」プロパティは揮発性が高すぎて依存できないと想定する必要があります。

この質問は、ストリームよりも読み取り専用コレクションについてよく聞かれるのを見てきましたが、これは同じ設計パターンの別の例であり、同じ議論が当てはまると思います。

明確にするために、.NET の ICollection インターフェイスにはプロパティ IsReadOnly があります。これは、コレクションがコンテンツを変更するメソッドをサポートしているかどうかを示す指標として使用することを目的としています。ストリームと同様に、このプロパティはいつでも変更でき、InvalidOperationException または NotSupportedException がスローされる原因となります。

これに関する議論は通常、次のように要約されます。

  • 代わりに IReadOnlyCollection インターフェイスがないのはなぜですか?
  • NotSupportedException が適切かどうか。
  • 「モード」と明確な具体的な機能を持つことの長所と短所。

複数の「セット」の動作に対処する必要があるため、モードが良いことはめったにありません。いつでもモードを切り替えることができるものを持つことは、アプリケーションが複数の動作の「セット」も処理する必要があるため、かなり悪いことです。ただし、何かをより目立たない機能に分解できるからといって、必ずしも分解する必要があるとは限りません。特に、分解しても目前のタスクの複雑さを軽減するものがない場合はなおさらです。

私の個人的な意見では、あなたのクラスの消費者が理解できるとあなたが考えるメンタル モデルに最も近いパターンを選択する必要があります。あなたが唯一の消費者である場合は、最も好きなモデルを選択してください. Stream と ICollection の場合、これらの単一の定義を持つことは、同様のシステムでの長年の開発によって構築されたメンタル モデルに非常に近いと思います。ストリームについて話すときは、読み取り可能か書き込み可能かではなく、ファイル ストリームとメモリ ストリームについて話します。同様に、コレクションについて話すとき、「書き込み可能性」の観点から言及することはめったにありません。

これに関する私の経験則: 動作の「モード」を持つのではなく、単純なメンタル モデルを補完する限り、動作をより具体的なインターフェイスに分解する方法を常に探します。個別の動作を個別のものと考えるのが難しい場合は、モード ベースのパターンを使用して、非常に明確に文書化します。

于 2009-07-31T13:10:19.427 に答える
1

stream.CanReadは、基になるストリームに読み取りの可能性があるかどうかをチェックするだけです。実際の読み取りが可能かどうか(ディスクエラーなど)については何も述べていません。

* Readerクラスのいずれかを使用した場合、それらはすべて読み取りをサポートしているため、NotImplementedExceptionをキャッチする必要はありません。*WriterのみがCanRead=Falseを持ち、その例外をスローします。ストリームが読み取りをサポートしていることを知っている場合(たとえば、StreamReaderを使用した場合)、IMHOは追加のチェックを行う必要はありません。

読み取り中にエラーが発生すると例外がスローされるため(ディスクエラーなど)、例外をキャッチする必要があります。

また、スレッドセーフとして文書化されていないコードはスレッドセーフではないことに注意してください。通常、静的メンバーはスレッドセーフですが、インスタンスメンバーはそうではありません。ただし、各クラスのドキュメントを確認する必要があります。

于 2009-07-31T06:22:36.873 に答える
1

あなたの質問とその後のすべての解説から、あなたの問題は、明快で明快な契約書の「正確さ」にあると推測しています。記載されている契約は、MSDN オンライン ドキュメントに記載されているものです。

あなたが指摘したのは、ドキュメントに何かが欠けているため、契約について推測する必要があるということです。より具体的には、ストリームの可読性プロパティの揮発性については何も述べられていないため、対応する CanRead プロパティの値がわずかであっても、aがスローされる可能性があるという唯一の仮定を行うことができます。NotSupportedExceptionミリ秒 (またはそれ以上) 前。

この場合、このインターフェイスの意図を理解する必要があると思います。つまり、次のとおりです。

  1. 複数のスレッドを使用する場合、すべての賭けはオフになります。
  2. ストリームの状態を変更する可能性のある何かをインターフェースで呼び出すまでは、 の値CanReadは不変であると安全に想定できます。

上記にもかかわらず、Read* メソッド潜在的にNotSupportedException.

同じ引数を他のすべての Can* プロパティに適用できます。

于 2009-07-31T11:22:52.570 に答える
0

これは、実際的な問題というよりも理論的な問題のように思えます。ストリームが閉じられている以外、ストリームが読み取り不能/書き込み不能になる状況は本当に考えられません。

まれなケースもあるかもしれませんが、頻繁に現れるとは思いません。コードの大部分がこれについて心配する必要はないと思います。

しかし、これは興味深い哲学的問題です。

編集: CanRead などが有用かどうかという問題に対処しますが、それらはまだ有用であると思います-主に引数の検証のためです。たとえば、ある時点で読み取りたいストリームをメソッドが受け取るからといって、メソッドの開始時にそれを読み取りたいとは限りませんが、引数の検証は理想的にはそこで実行する必要があります。これは、パラメーターが null かどうかをチェックして、最初に逆参照したときに がスローされるArgumentNullExceptionのを待つのではなく、スローするのとまったく違いはありません。NullReferenceException

また、CanSeek少し異なります。場合によっては、コードがシーク可能なストリームとシークできないストリームの両方にうまく対処できる場合がありますが、シーク可能な場合の方が効率的です。

これは、「シーク可能性」などが一貫していることに依存していますが、私が言ったように、それは実際には真実であるようです.


よし、これを別の言い方にしてみよう...

メモリ内で読み取り/シークを行っていて、十分なデータがあることを既に確認している場合、または事前に割り当てられたバッファー内で書き込みを行っている場合を除き、問題が発生する可能性は常にあります。ディスクの故障や満杯、ネットワークの崩壊など。これらのこと実際に起こります。そのため、常に障害に耐えられるようにコーディングする必要があります (または、問題が重要でない場合は、問題を意識的に無視することを選択します)。

ディスク障害が発生した場合にコードが正しいことを行うことができれば、FileStream書き込み可能から書き込み不可に変わっても生き残る可能性があります。

Stream確固たるコントラクトがある場合、それらは信じられないほど弱いものでなければなりません。静的チェックを使用して、コードが常に機能することを証明することはできません。あなたができる最善のことは、失敗に直面して正しいことをしたことを証明することです.

Streamすぐに変わるとは思えません。より適切に文書化される可能性があることは確かに認めますが、「完全に壊れている」という考えは受け入れません。実生活で実際に使用できなかったら、もっと壊れていたでしょう...そして、今より壊れていたとしても、論理的には完全に壊れているわけではありません.

フレームワークには、日付/時刻 API の状態が比較的悪いなど、はるかに大きな問題があります。最後の数バージョンで大幅に改善されましたが、(たとえば) Joda Timeの多くの機能がまだ欠けています。組み込みの不変コレクションの欠如、言語での不変性の不十分なサポートなど - これらは私に実際の頭痛の種を引き起こす本当の問題です. Stream実生活ではほとんど問題を引き起こさない、やや扱いにくい理論的な問題のように思われる問題に何年も費やすよりも、それらが対処されるのを見たいと思います。

于 2009-07-31T06:46:45.513 に答える