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...
Create(ElephantParams)
and Create(GiraffeParams)
to their respective classes, but that would require ditching the contract that all base classes have a Create() method.AnimalRegistry
between AnimalParams
and the appropriate factory and define a new Create()
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;
}
How can I write the base class
AnimalFactory
in such a way that ensures at compile-time that only the correct type ofAnimalParams
can be passed, while still allowing others to write their own concrete implementations for other animals?
The answer is twofold:
Params<T>
as in Factory<T>
, which returns T
objects. First, let's have a look at your AnimalFactory
.
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);
}
The Create
method is an ideal candidate for strongly typed args
. However, AnimalParams
is too coarse-grained, preventing the compiler to enforce the correct type.
The Serialize
method, on the other hand, is fine the way it is. We don't want to narrow down the type of argument. Keeping it as wide as Animal
gives us the most flexibility.
These conflicting interests raise a question. Are we trying to model too much in the abstract class' interface? Shouldn't providing animals be the factory's sole responsibility? Let's follow the Interface Segregation Principle and factor out the Serialize
method.
Rewriting AnimalFactory
, clarifying it's intent.
public abstract class Factory<T> where T : Animal
{
public abstract string SpeciesName { get; }
public abstract Type TypeCreated { get; }
public abstract T Create(Params<T> args);
}
public interface ISerialize
{
string Serialize(Animal animal);
}
public abstract class Animal
{
public abstract int Size { get; }
}
public abstract class Params<T> where T : Animal { }
Note the change from AnimalParams
to Params<T> where T : Animal
. This is key to providing type safety.
public class ElephantParams : Params<Elephant>
{
public readonly int Weight;
public ElephantParams(int weight) => Weight = weight;
}
Only one descendant of Params<Elephant>
is allowed, enforced with the cast (ElephantParams)p
.
public class ElephantService : Factory<Elephant>, ISerialize
{
public override string SpeciesName => "Elephant";
public override Type TypeCreated => typeof(Elephant);
public override Elephant Create(Params<Elephant> p)
{
return new Elephant((ElephantParams)p);
}
public string Serialize(Animal animal)
{
if (animal is Elephant elephant)
{
return $"Elephant({elephant.Weight})";
}
else
{
throw new Exception("Not an elephant");
}
}
}
You could skip this part, however, the previous sample has something of a code smell.
public override Elephant Create(Params<Elephant> p)
{
return new Elephant((ElephantParams)p);
}
It's hard to shake the feeling that we're doing things the other way round. It starts in the abstract base classes.
public abstract class Animal
{
public abstract int Size { get; }
}
public abstract class Params<T> where T : Animal { }
Where Params<T>
is no more than a marker interface. The Liskov Substitution Principle is based on the fact that an interface should define polymorphic behavior, that all instances implement. Thus, ensuring each call on such an instance can provide a meaningful result, given the functionality is always present.
For the sake of argument, let's make Animal
the marker interface (which isn't a good idea either).
public abstract class Animal { }
public abstract class Params<T> where T : Animal
{
public abstract int Size { get; }
}
This reflects in the following changes.
public class Elephant : Animal
{
public int Weight;
public Elephant(Params<Elephant> args) => Weight = args.Size;
}
public class ElephantParams : Params<Elephant>
{
private readonly int weight;
public ElephantParams(int weight) => this.weight = weight;
public override int Size => weight;
}
Allowing us to resolve the code smell and adhere to the Liskov Substitution Principle.
public override Elephant Create(Params<Elephant> p)
{
return new Elephant(p);
}
Safe to say this brings quite a change of mind, now the base class designer has to abstract all possible concepts that future developers might need in the Params<T>
definition. If not, they'll be forced to cast to a specific type within the Create
method and gracefully handle the case where the type isn't the expected one. Otherwise, the app might still crash if someone injects another derived class (with the same type argument T
in the base class Params<T>
).
Registry types:
Register
generates services on the fly, we'll need to supply a TService
type argument that's a concrete class (in our case one with a default constructor), such as ElephantService
. TService : Factory<TAnimal>, ISerialize, new()
.Factory<TAnimal>
to indicate our type of factory, we'll need to specify TAnimal
as well.The Serialize
signature is same as before, again, it wouldn't make much sense narrowing the field and giving in on flexibility. As that would require us to specify the derived type of Animal
before serializing.
public class AnimalRegistry
{
Dictionary<Type, object> registry = new Dictionary<Type, object>();
public void Register<TService, TAnimal>()
where TService : Factory<TAnimal>, ISerialize, new()
where TAnimal : Animal
{
TService service = new TService();
registry[service.GetType()] = service;
registry[service.TypeCreated] = service;
}
public Factory<TAnimal> GetInstance<TAnimal>()
where TAnimal : Animal
{
return (Factory<TAnimal>)registry[typeof(TAnimal)];
}
public string Serialize(Animal animal)
{
return ((ISerialize)registry[animal.GetType()]).Serialize(animal);
}
}
Composition root remains much like before, with the exception of Register
's second type argument and added type safety.
AnimalRegistry registry = new AnimalRegistry();
registry.Register<ElephantService, Elephant>();
registry.Register<GiraffeService, Giraffe>();
Animal a1 = registry.GetInstance<Elephant>().Create(new ElephantParams(weight: 1500));
Animal a2 = registry.GetInstance<Giraffe>().Create(new GiraffeParams(height: 180));
//Doesn't compile
//Animal a3 = registry.GetInstance<Elephant>().Create(new GiraffeParams(height: 180));
registry.Serialize(a1);
registry.Serialize(a2);
UPDATE
If I wanted to write a GetInstances() method that would return all AnimalFactory instances in the registry, how would I go about typing that method?
You can use reflection to filter types that extend Factory<T>
.
private bool IsFactory(Type type)
{
return
type.BaseType.IsGenericType &&
type.BaseType.GetGenericTypeDefinition() == typeof(Factory<>);
}
public List<object> GetInstances()
{
var factoryTypes = registry.Keys.Where(IsFactory);
return factoryTypes.Select(key => registry[key]).ToList();
}
However,
List<T>
) can only contain elements of the same typetypeof(Factory<Elephant>) != typeof(Factory<Giraffe>)
Factory<Animal>
, ref generic variance
So, the List<object>
may not prove that useful. As suggested, you could use an auxiliary interface or derive Factory<T>
from an abstract Factory
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With