23

リスコフ置換の原則では、サブタイプがスーパータイプの契約を満たす必要があります。私の理解では、これはReadOnlyCollection<T>リスコフに違反することを伴います。 ICollection<T>のコントラクトはエクスポーズAddRemove操作を行いますが、読み取り専用サブタイプはこのコントラクトを満たしません。例えば、

IList<object> collection = new List<object>();
collection = new System.Collections.ObjectModel.ReadOnlyCollection<object>(collection);
collection.Add(new object());

    -- not supported exception

不変のコレクションが必要であることは明らかです。それらをモデル化する .NET の方法に何か問題がありますか? それを行うためのより良い方法は何ですか? IEnumerable<T>少なくとも不変に見える一方で、コレクションを公開するという良い仕事をします。ただし、セマンティクスは大きく異なります。これは主にIEnumerable、状態を明示的に公開しないためです。

私の特定のケースでは、 FSMをサポートするために不変のDAGクラスを構築しようとしています。最初に/メソッドが明らかに必要ですが、すでに実行されている状態マシンを変更できるようにしたくありません。DAG の不変表現と可変表現の類似性を表現するのに苦労しています。AddNodeAddEdge

現在、私の設計では、前もって DAG Builder を使用してから、不変グラフを一度作成する必要があります。その時点で、それは編集できなくなります。Builder と具体的な不変 DAG の間の唯一の共通インターフェイスはAccept(IVisitor visitor). おそらくより単純なオプションに直面して、これが過度に設計されている/抽象的すぎる可能性があることを懸念しています。同時に、NotSupportedExceptionクライアントが特定の実装を取得した場合にスローされる可能性のあるメソッドをグラフ インターフェイスに公開できることを受け入れるのに苦労しています。これを処理する正しい方法は何ですか?

4

6 に答える 6

10

常に (読み取り専用) グラフ インターフェイスを使用し、それを読み取り/書き込みの変更可能なグラフ インターフェイスで拡張できます。

public interface IDirectedAcyclicGraph
{
    int GetNodeCount();
    bool GetConnected(int from, int to);
}

public interface IModifiableDAG : IDirectedAcyclicGraph
{
    void SetNodeCount(int nodeCount);
    void SetConnected(int from, int to, bool connected);
}

get(これらのメソッドをsetプロパティの半分に分割する方法がわかりません。)

// Rubbish implementation
public class ConcreteModifiableDAG : IModifiableDAG
{
    private int nodeCount;
    private Dictionary<int, Dictionary<int, bool>> connections;

    public void SetNodeCount(int nodeCount) {
        this.nodeCount = nodeCount;
    }

    public void SetConnected(int from, int to, bool connected) {
        connections[from][to] = connected;
    }

    public int GetNodeCount() {
        return nodeCount;
    }

    public bool GetConnected(int from, int to) {
        return connections[from][to];
    }
}

// Create graph
IModifiableDAG mdag = new ConcreteModifiableDAG();
mdag.SetNodeCount(5);
mdag.SetConnected(1, 5, true);

// Pass fixed graph
IDirectedAcyclicGraph dag = (IDirectedAcyclicGraph)mdag;
dag.SetNodeCount(5);          // Doesn't exist
dag.SetConnected(1, 5, true); // Doesn't exist

これは、Microsoftが読み取り専用のコレクションクラスで行ってほしいことです-カウントの取得、インデックスによる取得の動作などのための1つのインターフェイスを作成し、値の追加、変更などのインターフェイスでそれを拡張します.

于 2012-12-11T11:36:36.190 に答える
3

.Net の読み取り専用コレクションは、LSP に反しません。

add メソッドが呼び出された場合にサポートされていない例外をスローする読み取り専用コレクションに悩まされているようですが、例外はありません。

多くのクラスは、いくつかの状態のいずれかになるドメイン オブジェクトを表し、すべての操作がすべての状態で有効であるとは限りません。ストリームは 1 回しか開くことができず、ウィンドウは破棄された後は表示できません。

そのような場合に例外をスローしても、現在の状態をテストして例外を回避する方法がある限り有効です。

.Net コレクションは、読み取り専用と読み取り/書き込みの状態をサポートするように設計されています。メソッド IsReadWrite が存在するのはそのためです。これにより、呼び出し元はコレクションの状態をテストし、例外を回避できます。

LSP では、スーパー タイプのコントラクトを尊重するためにサブタイプが必要ですが、コントラクトは単なるメソッドのリストではありません。これは、オブジェクトの状態に基づいた入力と予想される動作のリストです。

「あなたが私にこの情報を与えてくれたら、私がこの状態にあるときに、これが起こることを期待してください.」

ReadOnlyCollection は、コレクションの状態が読み取り専用の場合、サポートされていない例外をスローすることにより、ICollection のコントラクトを完全に尊重します。ICollection ドキュメントの例外セクションを参照してください。

于 2012-12-21T00:42:08.313 に答える
3

ビルダーを使用した現在のソリューションが過度に設計されているとは思いません。

次の 2 つの問題を解決します。

  1. LSP 違反実装が/でs
    を決してスローしない編集可能なインターフェースがあり、これらのメソッドをまったく持たない編集不可能なインターフェースがあります。NotSupportedExceptionAddNodeAddEdge

  2. 時間結合
    2 つではなく 1 つのインターフェイスを使用する場合、その 1 つのインターフェイスは、「初期化フェーズ」と「不変フェーズ」を何らかの方法でサポートする必要があります。おそらく、これらのフェーズの開始と終了をマークするいくつかのメソッドによって行われます。

于 2012-12-11T11:26:32.133 に答える
1

明示的なインターフェイスの実装を使用して、読み取り専用バージョンで必要な操作から変更メソッドを分離できます。また、読み取り専用の実装には、メソッドを引数として取るメソッドがあります。これにより、DAC の構築をナビゲーションとクエリから分離できます。以下のコードとそのコメントを参照してください。

// your read only operations and the
// method that allows for building
public interface IDac<T>
{
    IDac<T> Build(Action<IModifiableDac<T>> f);
    // other navigation methods
}

// modifiable operations, its still an IDac<T>
public interface IModifiableDac<T> : IDac<T>
{
    void AddEdge(T item);
    IModifiableDac<T> CreateChildNode();
}

// implementation explicitly implements IModifableDac<T> so
// accidental calling of modification methods won't happen
// (an explicit cast to IModifiable<T> is required)
public class Dac<T> : IDac<T>, IModifiableDac<T>
{
    public IDac<T> Build(Action<IModifiableDac<T>> f)
    {
        f(this);
        return this;
    }

    void IModifiableDac<T>.AddEdge(T item)
    {
        throw new NotImplementedException();
    }

    public IModifiableDac<T> CreateChildNode() {
        // crate, add, child and return it
        throw new NotImplementedException();
    }

    public void DoStuff() { }
}

public class DacConsumer
{
    public void Foo()
    {
        var dac = new Dac<int>();
        // build your graph
        var newDac = dac.Build(m => {
            m.AddEdge(1);
            var node = m.CreateChildNode();
            node.AddEdge(2);
            //etc.
        });

        // now do what ever you want, IDac<T> does not have modification methods
        newDac.DoStuff();
    }
}

このコードから、ユーザーは呼び出しBuild(Action<IModifiable<T>> m)て変更可能なバージョンにアクセスすることしかできません。メソッド呼び出しは不変のものを返します。IModifiable<T>オブジェクトのコントラクトで定義されていない意図的な明示的なキャストがなければ、それにアクセスする方法はありません。

于 2012-12-20T15:13:39.217 に答える
1

そもそも不変のデータ構造を設計するというアイデアが気に入っています。実行できない場合もありますが、これを頻繁に実行する方法があります。

DAG の場合、ほとんどの場合、ファイルまたはユーザー インターフェイスに何らかのデータ構造があり、すべてのノードとエッジを IEnumerables として不変の DAG クラスのコンストラクターに渡すことができます。その後、Linq メソッドを使用して、ソース データをノードとエッジに変換できます。

コンストラクター (またはファクトリ メソッド) は、アルゴリズムにとって効率的な方法でクラスのプライベート構造を構築し、非循環のような事前のデータ検証を行うことができます。

このソリューションは、データ構造の反復構築が不可能であるという点でビルダー パターンとは異なりますが、多くの場合、実際には必要ありません。

個人的には、書き込み機能が実際には隠されているわけではないため、同じクラスによって実装された読み取りおよび読み取り/書き込みアクセス用の個別のインターフェイスを備えたソリューションは好きではありません...インスタンスを読み取り/書き込みインターフェイスにキャストすると、変更メソッドが公開されます。このようなシナリオでのより良い解決策は、データをコピーする本当に不変のデータ構造を作成する AsReadOnly メソッドを持つことです。

于 2012-12-20T21:03:10.453 に答える
1

私が気に入っている方法 (しかし、それは私だけかもしれません) は、インターフェイスに読み取りメソッドを持ち、クラス自体に編集メソッドを持つことです。DAG の場合、データ構造の複数の実装がある可能性はほとんどないため、グラフを編集するためのインターフェイスを持つことは、やり過ぎであり、通常はあまりきれいではありません。

データ構造を表すクラスと、読み取り構造であるインターフェイスを持つことがわかりました。

例えば:

public interface IDAG<out T>
{
    public int NodeCount { get; }
    public bool AreConnected(int from, int to);
    public T GetItem(int node);
}

public class DAG<T> : IDAG<T>
{
    public void SetCount(...) {...}
    public void SetEdge(...) {...}
    public int NodeCount { get {...} }
    public bool AreConnected(...) {...}
    public T GetItem(...) {...}
}

次に、構造を編集する必要がある場合はクラスを渡し、読み取り専用構造が必要な場合はインターフェースを渡します。いつでもクラスとしてキャストできるため、偽の「読み取り専用」ですが、とにかく読み取り専用は決して現実的ではありません...

これにより、より複雑な読み取り構造を持つことができます。Linq と同様に、インターフェイスで定義された拡張メソッドを使用して読み取り構造を拡張できます。例えば:

public static class IDAGExtensions
{
    public static List<T> FindPathBetween(this IDAG<T> dag, int from, int to)
    {
        // Use backtracking to determine if a path exists between `from` and `to`
    }

    public static IDAG<U> Cast<U>(this IDAG<T> dag)
    {
        // Create a wrapper for the DAG class that casts all T outputs as U
    }
}

これは、データ構造の定義を「それでできること」から分離するのに非常に役立ちます。

この構造が許可するもう 1 つのことは、ジェネリック型を として設定することですout T。これにより、引数の型の反変性が可能になります。

于 2012-12-20T17:38:43.453 に答える