ReadOnlyCollection VS里氏 - 如何正确模拟可变集合的不可变的表示(ReadOn

2019-08-03 21:35发布

里氏取代原则要求亚型必须满足超类型的合同。 在我的理解,这将意味着该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不明确公开任何国家。

在我的具体情况,我想建立一个一成不变的DAG类,以支持一个FSM 。 我显然需要AddNode / AddEdge开头的方法,但我不希望它是可能的,一旦它已经运行更改状态机。 我在代表DAG的可变和不可变表示之间的相似性困难。

现在,我的设计包括使用DAG生成器前面,然后创建不可改变的图形一次,此时它不再是可编辑。 建造和混凝土不可改变DAG之间唯一的公共接口是Accept(IVisitor visitor) 。 我很担心,这可能是过度设计/中可能更简单的方式在脸上过于抽象。 与此同时,我无法接受,我可以露出我的图形界面的方法可能会引发NotSupportedException ,如果客户端得到一个特定的实现。 什么是处理这个问题的正确方法?

Answer 1:

你总是可以有一个(只读)图形界面,并且与读扩展它/写修改,图形界面:

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

这是我希望微软曾与他们只读集合类做了什么 - 专为GET-数一个接口,让逐指数的行为等,并与添加接口,变化值等扩展它



Answer 2:

我不认为你在与建筑商目前的解决方案是过度设计。

它解决了两个问题:

  1. LSP的冲突
    你有一个可编辑的接口,其实现方式不会抛出NotSupportedException S于AddNode / AddEdge ,你有没有这些方法都不可编辑界面。

  2. 时空耦合
    如果你想带一个接口,而不是两个去,一个接口需要通过一些方法标志着开始以某种方式支持“初始化阶段”和“不变的阶段”,最有可能的,可能这些阶段的结束。



Answer 3:

阅读在.net只收藏不违背LSP。

你似乎在只读集合投掷如果add方法被称为不支持异常困扰,但并没有什么特殊的了。

大量类的代表可以在几个州之一,并不是所有操作都会在所有有效的状态域对象:流只能打开一次,他们被设置后不能正常显示窗口等。

在这种情况下抛出异常,只要有测试的当前状态,避免异常的方式是有效的。

在.NET藏品被设计为支持状态:只读和读/写。 这就是为什么该方法IsReadWrite存在。 它允许呼叫者测试收集的状态,避免例外。

LSP要求亚型兑现超类型的合同,但合同不仅仅是一个方法列表的更多; 它的投入和基于对象的状态预期行为的列表:

“如果你给我这个输入,当我在这种状态预计这种情况发生。”

ReadOnlyCollection充分荣誉的ICollection通过时是只读集合的​​状态抛出不支持异常的合同。 见的例外部分ICollection的文档 。



Answer 4:

您可以使用显式接口实现您的修饰方法从只读版本所需的作业分离。 也针对您的只读工作能够接受一个方法作为参数的方法。 这允许您将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>没有故意明确的转换,这是不是在合同规定的对象定义。



Answer 5:

我喜欢它(但也许这只是我)的方式,是有一个接口的阅读方法和类本身的编辑方法。 为了您的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
    }
}

这是从“你可以用它做什么”的数据结构的定义分开是非常有用的。

其他的事情,这个结构允许是设置泛型类型作为out T 。 这可以让你有参数类型的逆变。



Answer 6:

我喜欢我的设计数据结构不变摆在首位的想法。 有时,它只是没有可行的,但有一种方法来经常做到这一点。

为了您的DAG你最有可能有一个文件或用户界面的一些数据结构,你可以通过所有节点和边为IEnumerables你一成不变的DAG类的构造函数。 然后你可以使用LINQ的方法到源数据转换为节点和边缘。

然后构造函数(或工厂方法)可以建立类的私有结构的方式,很高效,为你的算法,并进行前期数据验证像acyclicy。

该解决方案由的方式,数据结构的重复建设是不可能的,但往往是不是真正需要的建设者模式辨别。

就个人而言,我不喜欢有独立的接口解决方案,用于读取和读取由同一个类实现,因为写功能是不是真的隐藏/写访问...铸造实例的读/写接口公开,突变的方法。 在这样的场景中的更好的解决方案是具有一个创建真正不可变数据结构复制的数据的方法AsReadOnly。



文章来源: ReadOnlyCollection vs Liskov - How to correctly model immutable representations of a mutable collection