2

I'm trying to figure out if there's a better way to structure some code. Although it currently works, it always gets at me due to the complexity and crazy nature that it feels to have with generics constraints. I'd be interested to know if anyone has an idea for a neater solution.

I've included a class diagram to make things a little easier. enter image description here

This is all going within a library, and I wanted to try and keep all my type safety where I can. The library has 3 layers of increasing complexity. Which I'll explain in a moment.

I'm using Generics to make all the types work. For example a Route is actually a List<T> where T is a Visit. Now because I've got the 3 layers, I want to be able to access properties on these Visits (and the Nodes that they correspond to) from the Route itself (and to make it easier to consume). So that actually makes a Route<Visit<Node>>. Once you add that to a Solution which needs to be strongly typed Solution<Route<Visit<Node>>> things get complicated.

This results in some kinda unslightly code:

public abstract class Solution<TSolution, TRoute, TVisit, TNode, TResource>
   where TSolution : Solution<TSolution, TRoute, TVisit, TNode, TResource>, new()
   where TRoute : Route<TRoute, TVisit, TNode, TResource>, new()
   where TVisit : Visit<TVisit, TNode>, new()
   where TNode : Element<TNode>
   where TResource : Resource<TResource>

It all works nicely, but I have to define these constraints at each class/level. At each level of complexity I create some simple consumable class such as the following, essentially hiding the generic constraints which would make it impossible to consume.

Level1.Solution : Common.Solution<Solution, Route, Visit, Node, Resource>

They have also been made recursive based on advice from this question to allow me to extend the class. For example a Level2.Solution will needs to be able to specify a Level2.Route as one of the constraints, normally this won't work (co/contra variance and generics) without the recursion.

All in all, it works, but is a bit cringy to say the least. Anyone have any ideas how this can be re-worked nicely?

Community
  • 1
  • 1
Ian
  • 33,605
  • 26
  • 118
  • 198
  • 4
    what is that class diagram made in!? Sorry for a useless comment: I had to ask, it looks great ;) I swear i see gradients and such. If it's by hand, i'll be facepalming all day. – Yuji 'Tomita' Tomita Jan 31 '11 at 20:30
  • do you actually need generics ? for example, why having Visit and not a hard coded type node property in Visit ? – Steve B Jan 31 '11 at 20:32
  • @Steve: If I don't have the Generics then when I'd have to introduce lots of casting instead. For example there'd be no difference between adding a Level2.Visit and a Level1.Visit to a Route. Whereas I need to ensure they are all the same type, and that I can access the properties on them. Without using the Generics I'd have to be casting all the time. – Ian Jan 31 '11 at 20:34
  • @Yuji : I don't actually like it very much because it's slow but... http://yuml.me/ – Ian Jan 31 '11 at 20:35
  • @Steve: You updated your comment. It just means for example, anyone extending a Route for example.. They'd be introducing loads of casting ==> foreach(Visit v in Route) { MyVisit mV = v as myVisit; ... and so on... } } – Ian Jan 31 '11 at 20:36
  • 1
    @Ian, I'm not sure to understand. You have defined Visit, which can be Visit for example. Do you have any other class that may fit into this class ? – Steve B Jan 31 '11 at 20:38
  • 3
    Not really an answer, but I made a conclusion that *once the generics start biting you back*, then aren't helping. Drop them back to the minimum that you **strictly need**. That might even be none at all. Collections should probably be generic, for sure - but I would be very tempted to drop that lot down to a small number of *non-generic* interfaces. – Marc Gravell Jan 31 '11 at 20:40
  • @Ian (concurrency comment exception;)), can't you simply abstract Visit ? if you have specific method or property in MyVisit you will have to cast it anyways... maybe you can post more code to help understanding ? – Steve B Jan 31 '11 at 20:41
  • @Marc: Well it is an answer, and one I'm thinking about. I think if co/contra variance worked with classes and Generics I wouldn't have a problem, but indeed, they're starting to bite at the moment. – Ian Jan 31 '11 at 21:01
  • @Steve: Nice new exception. It's difficult to post just a small amount of code to illustrate the problem. The reason for a Visit is then you could have a TSP.Visit or a VRP.Visit or a VRPTW.Visit. Each type of Node could then be accessed via a 'Node' property on the Visit and all the other properties required by a Visit would just be there rather than needing a cast. (Note that all the Visits in this example would then inherit from Visit) – Ian Jan 31 '11 at 21:04

3 Answers3

2

a Route is actually a List

I think your code will be much simplified if you prefer composition over inheritance. If a Route has a List, it will get much simpler, much easier to manage.

Community
  • 1
  • 1
Carl Manaster
  • 39,912
  • 17
  • 102
  • 155
1

I think the problem here is that you try to combine them all in a single place. For example, you don't have to tell anything to Solution about Node and its contract but in your case you do. Try to to work with interfaces rather than generic constraints - you will still be able to create Solutions from different Routes that are combined from Resources and Nodes however in the end you will need just to specify concrete solution:

interface INode {}
class Node: INode {}

interface IVisit
class Visit: IVisit
{
    Visit(INode node) {}
}

interface ISolution {}
class Solution: ISolution
{
   Solution(IList<IRoute> routes)
   {
   }
}
Andrey Taptunov
  • 9,367
  • 5
  • 31
  • 44
1

I admit that I could be missing something, but looking at this question and your linked question, I get the feeling that this is all overly complicated. It looks like a case of syntaxophilia gone wild. When I run into something like this, it usually pays to sit back and think about what I'm modeling in simpler terms.

As I understand it, those generic constraints are just ensuring that the types are compatible among the Resource, Node, Visit, Route, and Solution type parameters. All well and good, if highly complicated. The only "variables" there are your TResource and TNode types. What I mean is, you could have:

class Solution<TResource, TNode>
    where TNode : Element<TNode>
    where TResource : Resource<TResource>
{
     //  Construct properties for the Visit and Route, as required
}

Your examples indicate that a VSPSolution always has a VSPVisit and a VSPRoute, for example. If that's the case, then the composition solution here greatly simplifies things. In fact, if I more understood the purposes of TNode and TResource, you could probably eliminate those generic constraints, too.

The real question is whether the Visit (or Route) object has to exist outside of the Solution. It looks to me like a pretty strict hierarchy: Solution -> Route -> Visit, so it makes sense for the Solution to contain the Route, and for the Route to contain the Visit collection.

Doing it that way is much simpler, and a whole lot more clear what's going on.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351