23

Liskov-substitution principle requires that subtypes must satisfy the contracts of super-types. In my understanding, this would entail that ReadOnlyCollection<T> violates Liskov. ICollection<T>'s contract exposes Add and Remove operations, but the read only subtype does not satisfy this contract. For example,

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

    -- not supported exception

There is clearly a need for immutable collections. Is there something broken about .NET's way of modeling them? What would be the better way to do it? IEnumerable<T> does a good job of exposing a collection while, at least, appearing to be immutable. However, the semantics are very different, primarily because IEnumerable doesn't explicitly expose any of state.

In my particular case, I am trying to build an immutable DAG class to support an FSM. I will obviously need AddNode / AddEdge methods at the beginning but I don't want it to be possible to change the state machine once it is already running. I'm having difficulty representing the similarity between the immutable and mutable representations of the DAG.

Right now, my design involves using a DAG Builder up front, and then creating the immutable graph once, at which point it is no longer editable. The only common interface between the Builder and the concrete immutable DAG is an Accept(IVisitor visitor). I'm concerned that this may be over-engineered / too abstract in the face of possibly simpler options. At the same time, I'm having trouble accepting that I can expose methods on the my graph interface that may throw NotSupportedException if the client gets a particular implementation. What is the right way to handle this?

Daniel Hilgarth
  • 159,901
  • 39
  • 297
  • 411
smartcaveman
  • 38,142
  • 26
  • 119
  • 203
  • Well, they are implemented but they throw exceptions. I wonder, if the framework designers were starting again, would there be readonly base classes for collection types. – Jodrell Dec 11 '12 at 11:14
  • 6
    @Jodrell Liskov principle also states that method in child class should not throw new exception. Only the same exceptions or exceptions derived from exceptions thrown in the method in the parent class. – Guillaume Dec 11 '12 at 11:19
  • 2
    I agree: ReadOnlyCollection violates LSP. – Daniel Hilgarth Dec 11 '12 at 11:20
  • 2
    @Guillaume Thank you, that's my "today I learned" for today. – Rawling Dec 11 '12 at 11:20
  • 1
    Principles in order to be broken. :) – Hamlet Hakobyan Dec 11 '12 at 11:21
  • 1
    Well, the `IList` interface full contract include the fact the list can be Read Only or not because of the implicit `ICollection.IsReadOnly` property. So with regards to this Read Only state, I don't think the interface/inheritance contract really stipulates anything in itself. In other terms, if you are an `IList`, you are free to throw when Add is called, provided IsReadOnly returns true. I agree that doesn't really answer your question though :-) – Simon Mourier Dec 14 '12 at 15:39
  • @Guillaume: It's perfectly legitimate for derived classes to throw exceptions which the parent never actually throws, provided that the parent *defines* the conditions in which child classes may throw exceptions. – supercat Feb 08 '14 at 23:30
  • @SimonMourier: Actually `ReadOnlyCollection`'s implementation of `IList` is not nearly as big a violator of LSP as is that of `T[]`, since `ReadOnlyCollection` accurately says writes will fail. Although `Mammal[]` implements `IList` and the indexed setter of that `IList` will at compile-time accept anything of type `Mammal`, it's not possible to tell whether an attempt to store a particular `Mammal` will succeed other than by using Reflection, or by trying it can catching the exception. – supercat Feb 08 '14 at 23:36

6 Answers6

10

You could always have a (read-only) graph interface, and extend it with a read/write modifiable-graph interface:

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);
}

(I can't figure out how to split these methods into get/set halves of a property.)

// 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

This is what I wish Microsoft had done with their read-only collection classes - made one interface for get-count, get-by-index behaviour etc., and extend it with an interface to add, change values etc.

Rawling
  • 45,907
  • 6
  • 80
  • 115
  • I tried something like this, but I think the interface still needs some work. The problem I've realized with separating add/remove operations from access operations is that there really isn't a clear name for the components of the partition.. `ISupportAdd`? I'd also like to see removing and adding functionality distinct, as I often have situations where I don't ever need to remove anything from a collection and don't want the capability enabled within a specific context. (E.G. a delegate registry) – smartcaveman Dec 11 '12 at 16:50
  • I've often wished that the `ICollection` interface was done this way as well, rather than "back-forming" the read-only collection from the read-write case. (I also wish that a read-only collection was an interface, rather than a concrete class. Ditto for `ReadOnlyObservableCollection`.) – JaredReisinger Dec 21 '12 at 01:17
  • @smartcaveman I see your point, and you will potentially end up with a lot of interfaces, but... if you have a lot of different behaviours, you're going to need a lot of _somethings_ to represent them all, whatever something ends up being. – Rawling Dec 21 '12 at 08:12
  • @smartcaveman: be careful not overdoing it. It is clear that having a read-only interface can prove very useful for API design. However multiplying interfaces just for the sake of saying 'sometimes I don't need that' is not a good practice. It gives you a false sense of good conceptual representation of your problem, but in fact, it doesn't add anything and makes your life complicated. Imagine if .NET had created IAddableList, IRemovableList, ICountableList, each of these can make sense in various context, but really it would just make the usage awkward. Only create interfaces when needed. – edeboursetty Dec 21 '12 at 13:17
3

I don't think that your current solution with the builder is overengineered.

It solves two problems:

  1. Violation of LSP
    You have an editable interface whose implementations will never throw NotSupportedExceptions on AddNode / AddEdge and you have a non-editable interface that doesn't have these methods at all.

  2. Temporal coupling
    If you would go with one interface instead of two, that one interface would need to somehow support the "initialization phase" and the "immutable phase", most likely by some methods marking the start and possibly end of those phases.

Daniel Hilgarth
  • 159,901
  • 39
  • 297
  • 411
  • 2
    I don't think my current solution is over-engineered either. The problem is that I also don't think anyone ever thinks their solutions are over-engineered. Thanks, though. – smartcaveman Dec 11 '12 at 16:45
3

Read only collections in .Net do not go against LSP.

You seem bothered by the read only collection throwing a not supported exception if the add method is called, but there is nothing exceptional about it.

A lot of classes represent domain objects that can be in one of several states and not every operation is valid in all states: streams can only be opened once, windows cannot be shown after they are disposed, etc..

Throwing exceptions in those cases is valid as long as there is a way to test the current state and avoid the exceptions.

The .Net collections were engineered to support the states: read-only and read/write. Which is why the method IsReadWrite is present. It allows callers to test the state of the collection and avoid exceptions.

LSP requires subtypes to honor the contract of the super type, but a contract is more than just a list of methods; it is a list of inputs and expected behavior based on the state of the object:

"If you give me this input, when I'm in this state expect this to happen."

ReadOnlyCollection fully honors the contract of ICollection by throwing a not supported exception when the state of the collection is read only. See the exceptions section in the ICollection documentation.

Eli Algranti
  • 7,922
  • 2
  • 35
  • 47
  • 1
    IMHO, a major weakness in the .net collection interfaces is that they only allow code to identify two "states", when there are many different combinations of things that various collections of `T` might be able to unable to do. Many can only be tested by trying them and seeing if they work, and one important one ("Always return the same set of `T`") can't be tested at all. – supercat Jan 02 '13 at 19:21
  • Interesting explanation. – Mic Feb 04 '15 at 13:33
  • Hmm, maybe I'm missing something, but the whole point of LSP is that an instance of a subtype can be substituted for the original object, and the program will continue to work. Very clearly this is not the case here: if you substitute an instance of immutable object in place of a mutable object, it will break the program. Your argument with states would be correct if the immutable object started in the same, mutable, state, and could be changed to immutable by calling a method. As it is, it starts in the wrong state to satisfy the requirements of the LSP. – max Jul 31 '20 at 00:26
  • @max the point you are missing is that `ReadOnlyCollection` is not a subtype of `Collection`. It is a subtype of `ICollection`. `ICollection` has the method `IsReadOnly`, if you threat any `ICollection` as if it is read-write you are not using it correctly. The .Net library can be criticised in that it has an `IReadOnlyCollection` interface, but no `IReadWriteCollection`. Using only the library a function can specify that it expects a readonly collection, but it cannot specify that it expects a read/write collection. – Eli Algranti Aug 01 '20 at 22:34
1

You can use explict interface implementations to separate your modification methods from the operations needed in the read-only version. Also on your read-only implementation have a method that takes a method as an argument. This allows you to isolate your building of the DAC from the navigation and querying. see the code below and its comments:

// 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();
    }
}

From this code, the user can only call Build(Action<IModifiable<T>> m) to get access to a modifiable version. and the method call returns an immutable one. There is no way to access it as IModifiable<T> without an intentional explicit cast, which isn't defined in the contract for your object.

Rawling
  • 45,907
  • 6
  • 80
  • 115
Charles Lambert
  • 4,723
  • 24
  • 44
1

The way I like it (but maybe that's just me), is to have the reading methods in an interface and the editing methods in the class itself. For your DAG, it is highly unlikely that you will have multiple implementations of the data structure, so having an interface to edit the graph is kind of an overkill and usually not very pretty.

I find having the class representing the data-structure and the interface being the reading structure pretty clean.

for instance:

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(...) {...}
}

Then, when you require editing the structure, you pass the class, if you just need the readonly structure, pass the interface. It's a fake 'read-only' because you can always cast as the class, but read-only is never real anyway...

This allows you to have more complex reading structure. As in Linq, you can then extend your reading structure with extension methods defined on the interface. For instance:

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
    }
}

This is extremely useful to separate the definition of the datastructure from 'what you can do with it'.

The other thing that this structure allows is to set the generic type as out T. That allows you to have contravariance of argument types.

edeboursetty
  • 5,344
  • 1
  • 31
  • 59
1

I like the idea of designing my data structures immutable in the first place. Sometimes it's just not feasible but there's a way to accomplish this quite often.

For your DAG you most probably have some data structure in a file or a user interface and you could pass all the nodes and edges as IEnumerables to your immutable DAG class' constructor. Then you can use the Linq methods to transform your source data to nodes and edges.

The constructor (or a factory method) can then build the class' private structures in a way that's efficient for your algorithm and do upfront data validations like acyclicy.

This solution distinguishes from the builder pattern in a way that iterative construction of the data structure is not possible but often that's not really required.

Personally, I don't like the solutions with separate interfaces for read and read/write access implemented by the same class because the write functionality is not really hidden... casting the instance to the read/write interface exposes the mutating methods. The better solution in such a scenario is having an AsReadOnly method that creates a really immutable data structure copying the data.

CSharper
  • 516
  • 3
  • 8