どのプロパティが値を変更し、どのプロパティが一定のままであるかが明確になるように、どのようにインターフェイスを設計しますか?


12

.NETプロパティに関する設計上の問題があります。

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

問題:

このインターフェイスには、2つの読み取り専用プロパティとがIdありIsInvalidatedます。ただし、それらが読み取り専用であるという事実は、それらの値が一定のままであることを保証するものではありません。

それを非常に明確にすることが私の意図だったとしましょう…

  • Id 定数値を表します(したがって、安全にキャッシュできます)。
  • IsInvalidatedIXオブジェクトの存続期間中にその値を変更する可能性があります(したがって、キャッシュすべきではありません)。

interface IXその契約を十分に明確にするためにどのように変更できますか?

私自身の解決策の3つの試み:

  1. インターフェイスはすでに適切に設計されています。呼び出されたメソッドの存在Invalidate()により、プログラマは、同様の名前のプロパティの値IsInvalidatedが影響を受ける可能性があることを推測できます。

    この引数は、メソッドとプロパティの名前が似ている場合にのみ有効です。

  2. このインターフェースをイベントで拡張しますIsInvalidatedChanged

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;

    …Changedイベントの存在は、IsInvalidatedこのプロパティがその値を変更する可能性があることを示し、同様のイベントが存在しないことは、Idそのプロパティがその値を変更しないという約束です。

    私はこのソリューションが好きですが、それはまったく使われないかもしれない追加のものがたくさんあります。

  3. プロパティIsInvalidatedをメソッドに置き換えますIsInvalidated()

    bool IsInvalidated();

    これはあまりにも微妙な変更かもしれません。値は毎回新しく計算されるというヒントになるはずです-定数である場合は必要ありません。MSDNのトピック「プロパティとメソッドの選択」には、次のように書かれています。

    次の状況では、プロパティではなくメソッドを使用してください。[…]パラメータが変更されていなくても、操作は呼び出されるたびに異なる結果を返します。

どのような答えが期待できますか?

  • 私は、問題に対するまったく異なる解決策と、上記の試みをどのように打ち負かすかについての説明に最も興味があります。

  • 私の試みが論理的に欠陥があるか、まだ言及されていない重大な欠点があり、解決策が1つしか残っていない(または何も残っていない)場合、どこで間違ったのかを聞きたいと思います。

    欠陥が軽微であり、それを考慮した後、複数の解決策が残っている場合は、コメントしてください。

  • 少なくとも、どちらがあなたの優先解決策であり、どのような理由であるかについてのフィードバックをお願いします。


IsInvalidated必要としませんprivate setか?
ジェームズ

2
@ジェームズ:必ずしも、インターフェイス宣言でも実装でもありません:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

インターフェイスの@Jamesプロパティは、同じ構文を使用していても、自動プロパティと同じではありません。
svick

私は疑問に思っていました:インターフェイスはそのような振る舞いを課す「力」を持っていますか?この動作は各実装に委任されるべきではありませんか?そのレベルに物事を強制したい場合は、特定のインターフェイスを実装することなく、サブタイプがそれを継承するように抽象クラスを作成することを検討する必要があると思います。(ところで、私の理論的根拠は理にかなっていますか?)
heltonbiker

@heltonbiker残念ながら、ほとんどの言語(私が知っている)は、型に対するそのような制約を表現することはできませんが、間違いなくそうすべきです。これは仕様の問題であり、実装の問題ではありません。私の意見では、欠けているのは、クラスの不変式事後条件を言語で直接表現する可能性です。理想的にはInvalidStateException、たとえばs について心配する必要はないはずですが、これが理論的にも可能かどうかはわかりません。しかし、いいでしょう。
proskor

回答:


2

私は1と2よりもソリューション3を好むでしょう。

解決策1の私の問題は、Invalidate方法がない場合はどうなりますか?IsValidを返すプロパティを持つインターフェイスを想定しますDateTime.Now > MyExpirationDateSetInvalidここでは明示的なメソッドは必要ないかもしれません。メソッドが複数のプロパティに影響する場合はどうなりますか?IsOpenおよびの接続タイプを想定しIsConnectedます–両方ともCloseメソッドの影響を受けます。

解決策2:イベントの唯一のポイントが、同様の名前のプロパティが各呼び出しで異なる値を返す可能性があることを開発者に通知することである場合、それに対して強くアドバイスします。インターフェイスは短く明確にする必要があります。さらに、すべての実装がそのイベントを起動できるとは限りません。IsValid上記の私の例を再利用します:に到達したら、タイマーを実装し、イベントを発生させる必要がありMyExpirationDateます。結局、そのイベントがパブリックインターフェイスの一部である場合、インターフェイスのユーザーはそのイベントが機能することを期待します。

これらのソリューションについて言われていること:それは悪いことではありません。メソッドまたはイベントのいずれかが存在することは、同様の名前のプロパティが各呼び出しで異なる値を返す可能性があることを示します。私が言っているのは、それらだけでは常にその意味を伝えるのに十分ではないということです。

解決策3は私が目指していることです。avivが述べたように、これはC#開発者に対してのみ機能する可能性があります。C#-devとしての私にとってIsInvalidated、プロパティではないという事実は、「単なるアクセサではなく、ここで何かが起こっている」という意味をすぐに伝えます。ただし、これはすべての人に当てはまるわけではなく、MainMaが指摘したように、.NETフレームワーク自体はここでは一貫していません。

ソリューション3を使用したい場合は、それを規則として宣言し、チーム全体にそれに従うことをお勧めします。ドキュメントも不可欠です。私の意見では、それが実際の値に変化のヒントにはかなり簡単です:「値がある場合はtrueを返しますまだ有効」、「接続がいるかどうかを示し、既に開かれて」対「このオブジェクトがある場合にtrueを返しますがある有効」。ただし、ドキュメントでより明示的に説明してもまったく問題はありません。

だから私のアドバイスは:

ソリューション3を規約として宣言し、一貫してそれに従ってください。プロパティのドキュメントで、プロパティの値が変化するかどうかを明確にします。単純なプロパティを操作する開発者は、プロパティが変更されないと正しく仮定します(つまり、オブジェクトの状態が変更された場合にのみ変更されます)。それのような音が財産ことができること方法に遭遇開発者は(Count()Length()IsOpen())そのいろいろ書いさんが上に行くと(たぶん)を正確に方法が何、それがどのように動作するかを理解するための方法のドキュメントを読んで知っています。


この質問には非常に良い回答が寄せられており、そのうちの1つしか受け入れられないのが残念です。私があなたの答えを選んだのは、それが他の答えで提起されたいくつかのポイントの良い要約を提供し、それが多かれ少なかれ私がやることに決めたからです。回答を投稿してくださった皆さんに感謝します!
stakx

8

4番目の解決策があります:ドキュメントに依存します

stringクラスが不変であることをどのように知っていますか?MSDNドキュメントを読んでいるので、あなたはそれを知っています。StringBuilder一方、ドキュメントは変更可能であるため、変更可能です。

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

3番目の解決策は悪くありませんが、.NET Frameworkはそれに従いません。例えば:

StringBuilder.Length
DateTime.Now

プロパティですが、毎回一定であると期待しないでください。

.NET Framework自体で一貫して使用されているのは次のとおりです。

IEnumerable<T>.Count()

メソッドですが、

IList<T>.Length

プロパティです。前者の場合、データベースを照会するなど、追加作業が必要になる場合があります。この追加作業には時間がかかる場合があります。プロパティの場合、短い時間がかかることが予想されます。実際、リストの有効期間中に長さが変化する可能性がありますが、実際に返すには何もかかりません。


クラスの場合、コードを見るだけで、オブジェクトの存続期間中にプロパティの値が変化しないことが明確に示されます。例えば、

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

明確です:価格は同じままです。

残念ながら、同じパターンをインターフェイスに適用することはできません。そのような場合、C#の表現力が十分でないため、ドキュメント(XMLドキュメントとUML図を含む)を使用してギャップを埋める必要があります。


「追加作業なし」のガイドラインでさえ、絶対に一貫して使用されていません。たとえば、Lazy<T>.Value計算に非常に長い時間がかかる場合があります(初めてアクセスされたとき)。
svick

1
私の意見では、.NETフレームワークがソリューション3の規則に従うかどうかは問題ではありません。開発者は、社内タイプまたはフレームワークタイプのどちらで作業しているかを知るのに十分賢明である必要があります。わからない開発者はドキュメントを読まないため、ソリューション4も役に立たないでしょう。ソリューション3が会社/チームの慣例として実装されている場合、他のAPIもそれに続くかどうかは関係ないと思います(もちろん、非常に高く評価されますが)。
enzi

4

私の2セント:

オプション3:非C#-erとしては、あまり手がかりにはなりません。しかし、持ってきた見積もりから判断すると、熟練したC#プログラマーにとって、これが何かを意味することは明らかです。ただし、これに関するドキュメントを追加する必要があります。

オプション2:変更する可能性のあるすべてのものにイベントを追加する予定がない限り、そこに行かないでください。

他のオプション:

  • インターフェースの名前をIXMutableに変更して、状態を変更できることは明らかです。あなたの例で唯一の不変の値はidです。これは通常、可変オブジェクトでも不変であると想定されています。
  • 言及していません。インターフェイスは、振る舞いを記述するのが得意ではありません。部分的には、「このメソッドは特定のオブジェクトに対して常に同じ値を返す」、または「このメソッドを引数x <0で呼び出すと例外がスローされる」などの説明が言語でできないためです。
  • 言語を拡張し、構造に関するより正確な情報を提供するメカニズムがあることを除いて-Annotations / Attributes。彼らは時々いくらかの仕事を必要としますが、あなたはあなたのメソッドについて任意に複雑なものを指定することを可能にします。「このメソッド/プロパティの値はオブジェクトの存続期間中に変化する可能性があります」という注釈を見つけて作成し、メソッドに追加します。実装メソッドに「継承」させるためにいくつかのトリックを実行する必要があり、ユーザーはその注釈のドキュメントを読む必要があるかもしれませんが、それはあなたが言いたいことを正確に言う代わりにあなたが言いたいことを正確に言うことができるツールになりますそれで。

3

別のオプションは、インターフェイスを分割することです。Invalidate()すべての呼び出しを呼び出した後IsInvalidated、同じ値を返す必要があると仮定すると、呼び出した同じ部分trueに対して呼び出す理由はないようです。IsInvalidatedInvalidate()

そのため、無効化を確認するか、無効化を行うことをお勧めします、両方を実行するのは理にかなっていないようです。したがって、Invalidate()操作を含む1つのインターフェイスを最初の部分に提供し、別のインターフェイスを別のインターフェイスにチェック(IsInvalidated)するために提供することは理にかなっています。最初のインターフェイスのシグネチャからインスタンスが無効になることは明らかなので、残りの質問(2番目のインターフェイスの場合)は実際には非常に一般的です:指定された型が不変かどうかを指定する方法。

interface Invalidatable
{
    bool IsInvalidated { get; }
}

私は知っていますが、これは質問に直接答えませんが、少なくともそれをいくつかのタイプ不変をマークする方法の質問に還元します。たとえば、特に定義されていない限り、すべての型が可変であると想定することで、2番目のインターフェイス(IsInvalidated)が可変であるため、値をIsInvalidated時々変更できると結論付けることができます。一方、最初のインターフェースが次のようになっていると仮定します(疑似構文):

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

不変とマークされいるため、IDは変更されないことがわかります。もちろん、無効化を呼び出すと変更にインスタンスの状態が発生しますが、この変更はできません観測可能な、このインタフェースを介して。


悪くないアイデア!ただし、[Immutable]突然変異を引き起こすことが唯一の目的であるメソッドを含む限り、インターフェイスをマークするのは間違っていると私は主張します。Invalidate()メソッドをInvalidatableインターフェイスに移動します。
stakx

1
@stakx私は同意しません。:) 不変性純度(副作用がないこと)の概念を混同していると思います。実際、提案されたインターフェイスIXの観点からは、観察可能な突然変異はまったくありません。を呼び出すたびにId、毎回同じ値を取得します。同じことが当てはまりますInvalidate()(戻り値の型はなので、何も得られませんvoid)。したがって、型は不変です。もちろん、副作用はありますが、インターフェースを介してそれらを観察するIXことはできませんので、それらはクライアントに影響を与えませんIX
proskor

OK、それIXは不変です。そして、それらが副作用であること。クライアントが気にする必要がないように(の場合IX)、または副作用が何であるかが明らかであるように(2つの場合)、2つの分割されたインターフェイス間で分散されますInvalidatable。しかし、なぜに移動InvalidateしないのInvalidatableですか?それはIX不変純粋にInvalidatableなり、より不変不純になることはありません(これらはすでに両方であるため)。
stakx

(実際のシナリオでは、インターフェイスの分割の性質はおそらく技術的な問題よりも実際の使用例に依存することを理解していますが、ここであなたの推論を完全に理解しようとしています。)
stakx

1
@stakxまず、インターフェイスのセマンティクスをより明確にするためのさらなる可能性について尋ねました。私の考えは、インターフェースを分割する必要がある不変性の問題に問題を減らすことでした。分割が1つのインターフェース不変を作るために必要だっただけでなくので、しかし、それはまた、偉大な理にかなって、両方を起動する理由はないInvalidate()IsInvalidatedのインターフェイスの同じ消費者が。を呼び出す場合Invalidate()、それが無効になることを知っている必要がありますので、なぜそれを確認しますか?したがって、異なる目的のために2つの異なるインターフェースを提供できます。
proskor
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.