Reputation: 3220
I'm trying to make a fluent interface with lots of generics and descriptors that extend base descriptors. I've put this in a github repo because pasting all of the code here would make it unreadable.
After having read Eric Lippert's post about type constraints (http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx) and reading No type inference with generic extension method I understood the subject a little bit better, but I still got questions.
Suppose you have some classes that allow fluent calls:
var giraffe = new Giraffe();
new ZooKeeper<Giraffe>()
.Name("Jaap")
.FeedAnimal(giraffe);
var reptile = new Reptile();
new ExperiencedZooKeeper<Reptile>()
.Name("Martijn")
.FeedAnimal(reptile)
.CureAnimal(reptile);
The classes look like this:
public class ZooKeeper<T>
where T : Animal
{
internal string name;
internal List<T> animalsFed = new List<T>();
// this method needs to be fluent
public ZooKeeper<T> Name(string name)
{
this.name = name;
return this;
}
// this method needs to be fluent
public ZooKeeper<T> FeedAnimal(T animal)
{
animalsFed.Add(animal);
return this;
}
}
public class ExperiencedZooKeeper<T> : ZooKeeper<T>
where T : Animal
{
internal List<T> animalsCured = new List<T>();
// this method needs to be fluent
// but we must new it in order to be able to call CureAnimal after this
public new ExperiencedZooKeeper<T> Name(string name)
{
base.Name(name);
return this;
}
// this method needs to be fluent
// but we must new it in order to be able to call CureAnimal after this
public new ExperiencedZooKeeper<T> FeedAnimal(T animal)
{
base.FeedAnimal(animal);
return this;
}
// this method needs to be fluent
public ExperiencedZooKeeper<T> CureAnimal(T animal)
{
animalsCured.Add(animal);
return this;
}
}
I tried to get rid of the 'new' methods in ExperiencedZooKeeper
hiding the implementation of ZooKeeper
. The difference is that the new
methods in ExperiencedZooKeeper
return the correct type. AFAIK there is no way to do this without new
methods.
Another approach I tried to take is to move the 'setters' to extension methods. This works well for the .Name() method, but it introduces a ZooKeeperBase
which contains the internal field:
public abstract class ZooKeeperBase
{
internal string name;
}
public class ZooKeeper<T> : ZooKeeperBase
where T : Animal
{
internal List<T> animalsFed = new List<T>();
// this method needs to be fluent
public ZooKeeper<T> FeedAnimal(T animal)
{
animalsFed.Add(animal);
return this;
}
}
public static class ZooKeeperExtensions
{
// this method needs to be fluent
public static TZooKeeper Name<TZooKeeper>(this TZooKeeper zooKeeper, string name)
where TZooKeeper : ZooKeeperBase
{
zooKeeper.name = name;
return zooKeeper;
}
}
But this exact approach doesn't work for FeedAnimal(T animal), it needs an extra type parameter :
// this method needs to be fluent
public static TZooKeeper FeedAnimal<TZooKeeper, T>(this TZooKeeper zooKeeper, T animal)
where TZooKeeper : ZooKeeper<T>
where T : Animal
{
zooKeeper.animalsFed.Add(animal);
return zooKeeper;
}
This is still OK and works well and you can still call it fluently:
new ExperiencedZooKeeper<Reptile>()
.Name("Martijn")
.FeedAnimal(reptile)
.CureAnimal(reptile);
The real problems start when I try to make the following method fluent:
public static TZooKeeper Favorite<TZooKeeper, T>(this TZooKeeper zooKeeper, Func<T, bool> animalSelector)
where TZooKeeper : ZooKeeper<T>
where T : Animal
{
zooKeeper.favoriteAnimal = zooKeeper.animalsFed.FirstOrDefault(animalSelector);
return zooKeeper;
}
You cannot call Favorite
like this:
new ExperiencedZooKeeper<Reptile>()
.Name("Eric")
.FeedAnimal(reptile)
.FeedAnimal(new Reptile())
.Favorite(r => r == reptile)
because it will result in the same problem as No type inference with generic extension method, however, this case is slightly more complicated, because we already have a Type parameter TZookKeeper which describes the T we need. But like Eric Lipperts blog post, the type constraints are not part of the signature:
The type arguments for method 'TestTypeInference5.ZooKeeperExtensions.Favorite<TZooKeeper,T>(TZooKeeper, System.Func<T,bool>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.
For the full code, please refer to https://github.com/q42jaap/TestTypeInference The README in this repo actually explains the real life problem I tried to solve.
So the question really is, is there a way of creating this fluent method style without adding every method of ZooKeeper to every subclass of ZooKeeper with new
hiding the method of ZooKeeper itself?
Upvotes: 4
Views: 416
Reputation: 174457
One possibility would be to create a base class for each level and an empty handler class deriving from it:
Base classes:
public abstract class ZooKeeperBase<TZooKeeper, TAnimal>
where TZooKeeper : ZooKeeperBase<TZooKeeper, TAnimal>
where TAnimal : Animal
{
private string name;
private List<TAnimal> animalsFed = new List<TAnimal>();
private TAnimal favoriteAnimal;
public TZooKeeper Name(string name)
{
this.name = name;
return (TZooKeeper)this;
}
public TZooKeeper FeedAnimal(TAnimal animal)
{
animalsFed.Add(animal);
return (TZooKeeper)this;
}
public TZooKeeper Favorite(Func<TAnimal, bool> animalSelector)
{
favoriteAnimal = animalsFed.FirstOrDefault(animalSelector);
return (TZooKeeper)this;
}
}
public abstract class ExperiencedZooKeeperBase<TZooKeeper, TAnimal>
: ZooKeeperBase<TZooKeeper, TAnimal>
where TZooKeeper : ExperiencedZooKeeperBase<TZooKeeper, TAnimal>
where TAnimal : Animal
{
private List<TAnimal> animalsCured = new List<TAnimal>();
public TZooKeeper CureAnimal(TAnimal animal)
{
animalsCured.Add(animal);
return (TZooKeeper)this;
}
}
Handler classes:
public class ZooKeeper<T> : ZooKeeperBase<ZooKeeper<T>, T>
where T : Animal
{
}
public class ExperiencedZooKeeper<T>
: ExperiencedZooKeeperBase<ExperiencedZooKeeper<T>, T>
where T : Animal
{
}
Usage would be just as you showed in your question.
Upvotes: 2