Generics with constraints hierarchy

2019-08-01 08:22发布

问题:

I am currently facing a very disturbing problem:

interface IStateSpace<Position, Value>
where Position : IPosition           // <-- Problem starts here
where Value : IValue                 // <-- and here as I don't
{                                    //     know how to get away this
                                     //     circular dependency!
                                     //     Notice how I should be
                                     //     defining generics parameters
                                     //     here but I can't!
    Value GetStateAt(Position position);
    void SetStateAt(Position position, State state);
}

As you'll down here, both IPosition, IValue and IState depend on each other. How am I supposed to get away with this? I can't think of any other design that will circumvent this circular dependency and still describes exactly what I want to do!

interface IState<StateSpace, Value>
where StateSpace : IStateSpace        //problem
where Value : IValue                  //problem
{
    StateSpace StateSpace { get; };
    Value Value { get; set; }
}

interface IPosition
{
}

interface IValue<State>
where State : IState {      //here we have the problem again
    State State { get; }
}

Basically I have a state space IStateSpace that has states IState inside. Their position in the state space is given by an IPosition. Each state then has one (or more) values IValue. I am simplifying the hierarchy, as it's a bit more complex than described. The idea of having this hierarchy defined with generics is to allow for different implementations of the same concepts (an IStateSpace will be implemented both as a matrix as an graph, etc).

Would can I get away with this? How do you generally solve this kind of problems? Which kind of designs are used in these cases?

Thanks

回答1:

It's not entirely clear what the problem is - yes, you've got circular dependencies in your generic types, but that works.

I have a similar "problem" in Protocol Buffers: I have "messages" and "builders", and they come in pairs. So the interfaces look like this:

public interface IMessage<TMessage, TBuilder>
    where TMessage : IMessage<TMessage, TBuilder> 
    where TBuilder : IBuilder<TMessage, TBuilder>

and

public interface IBuilder<TMessage, TBuilder> : IBuilder
    where TMessage : IMessage<TMessage, TBuilder> 
    where TBuilder : IBuilder<TMessage, TBuilder>

It's certainly ugly, but it works. What do you want to be able to express that you can't currently express? You can see some of my thoughts about this on my blog. (Parts 2 and 3 of the series about Protocol Buffers are the most relevant here.)

(As an aside, it would make your code more conventional if you would add a T prefix to your type parameters. Currently it looks like State and Value are just classes.)



回答2:

I cannot see what you are trying to achieve - why do you want to force concrete types into your interfaces by using generics? That seems completely against the intention of interfaces - not to use concrete types. What's wrong with the following non-generic definition?

public interface IStateSpace
{
    IState GetStateAt(IPosition position);
    void SetStateAt(IPosition position, IState state);
}

public interface IState
{
    IStateSpace StateSpace { get; }
    IValue Value { get; set; }
}

public interface IPosition
{
}

public interface IValue
{
    IState State { get; }
}

Then you can create concrete implementations.

internal class MatrixStateSpace : IStateSpace
{
    IState GetStateAt(IPosition position)
    {
        return this.Matrix[position];
    }

    void SetStateAt(IPosition position, IState state)
    {
        this.Matrix[position] = state;
    }
}

internal class GraphStateSpace : IStateSpace
{
    IState GetStateAt(IPosition position)
    {
        return this.Graph.Find(position).State;
    }

    void SetStateAt(IPosition position, IState state)
    {
        this.Graph.Find(position).State = state;
    }
}

And now you can decide to use MatrixStateSpace or GraphStateSpace instances where ever an IStateSpace instance is required. The same of course holds for all other types.

The challenging part of the implementation is when you have to create new instances. For example there might be a method IState AddNewState() defined in IStateSpace. Any concrete implementation of IStateSpace faces the problem of creating an IState instance (ideally) without knowing any concrete type implementing IState. This is were factories, dependency injection, and related concepts come into play.