I've reduced my question to an example involving animals. I want to define a set of interfaces (/abstract classes) that allow anyone to create a factory for a given animal and register it with a central registrar: AnimalRegistry
keeps track of all the registered AnimalFactory
objects, which in turn produces and provides a consistent set of functionality for Animal
objects.
With the way I've written this (code below), I have a pretty simple interface for working with generic animals:
AnimalRegistry registry = new AnimalRegistry();
registry.Register<ElephantFactory>();
registry.Register<GiraffeFactory>();
Animal a1 = registry.GetInstance<ElephantFactory>().Create(new ElephantParams(weight: 1500));
Animal a2 = registry.GetInstance<GiraffeFactory>().Create(new GiraffeParams(height: 180));
registry.Serialize(a1);
registry.Serialize(a2);
However, there's something I really don't like about this:
There's nothing at compile-time that stops ElephantParams
from accidentally getting passed to registry.GetInstance<GiraffeFactory>().Create(AnimalParams)
.
How can I write the AnimalFactory
base class in such a way that ensures at compile-time that only the correct type of AnimalParams
can be passed while still allowing others to write their own concrete implementations for other animals?
I could...
- Add explicit methods for
Create(ElephantParams)
andCreate(GiraffeParams)
to their respective classes, but that would require ditching the contract that all base classes have a Create() method. - Add an additional mapping in
AnimalRegistry
betweenAnimalParams
and the appropriate factory and define a newCreate()
method in the registry, but that's not really an elegant solution as the problem is just moved rather than solved.
I suspect the answer lies in more type generics, but it currently escapes me.
AnimalRegistry:
public class AnimalRegistry
{
Dictionary<Type, AnimalFactory> registry = new Dictionary<Type, AnimalFactory>();
public void Register<T>() where T : AnimalFactory, new()
{
AnimalFactory factory = new T();
registry[typeof(T)] = factory;
registry[factory.TypeCreated] = factory;
}
public T GetInstance<T>() where T : AnimalFactory
{
return (T)registry[typeof(T)];
}
public AnimalFactory GetInstance(Animal animal)
{
return registry[animal.GetType()];
}
public string Serialize(Animal animal)
{
return GetInstance(animal).Serialize(animal);
}
}
Base classes:
public abstract class AnimalFactory
{
public abstract string SpeciesName { get; }
public abstract Type TypeCreated { get; }
public abstract Animal Create(AnimalParams args);
public abstract string Serialize(Animal animal);
}
public abstract class Animal
{
public abstract int Size { get; }
}
public abstract class AnimalParams { }
Concrete implementations:
Elephant:
public class ElephantFactory : AnimalFactory
{
public override string SpeciesName => "Elephant";
public override Type TypeCreated => typeof(Elephant);
public override Animal Create(AnimalParams args)
{
if (args is ElephantParams e)
{
return new Elephant(e);
}
else
{
throw new Exception("Not elephant params");
}
}
public override string Serialize(Animal animal)
{
if (animal is Elephant elephant)
{
return $"Elephant({elephant.Weight})";
}
else
{
throw new Exception("Not an elephant");
}
}
}
public class Elephant : Animal
{
public int Weight;
public override int Size => Weight;
public Elephant(ElephantParams args)
{
Weight = args.Weight;
}
}
public class ElephantParams : AnimalParams
{
public readonly int Weight;
public ElephantParams(int weight) => Weight = weight;
}
Giraffe:
public class GiraffeFactory : AnimalFactory
{
public override string SpeciesName => "Giraffe";
public override Type TypeCreated => typeof(Giraffe);
public override Animal Create(AnimalParams args)
{
if (args is GiraffeParams g)
{
return new Giraffe(g);
}
else
{
throw new Exception("Not giraffe params");
}
}
public override string Serialize(Animal animal)
{
if (animal is Giraffe giraffe)
{
return $"Giraffe({giraffe.Height})";
}
else
{
throw new Exception("Not a giraffe");
}
}
}
public class Giraffe : Animal
{
public readonly int Height;
public override int Size => Height;
public Giraffe(GiraffeParams args)
{
Height = args.Height;
}
}
public class GiraffeParams : AnimalParams
{
public int Height;
public GiraffeParams(int height) => Height = height;
}