Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using generics with extensible factories?

Tags:

c#

generics

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) and Create(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 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;
}
like image 266
Benjin Avatar asked Aug 21 '18 03:08

Benjin


1 Answers

How can I write the base class AnimalFactory 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?

The answer is twofold:

  1. Introduce the same generic type parameter in Params<T> as in Factory<T>, which returns T objects.
  2. To truly comply with the Liskov Substitution Principle, you'll need to further remodel the base classes.

  1. Generics

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");
        }
    }
}

  1. Liskov

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:

  • Given 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.
  • To keep it polymorphic, however, we'll abstract this with TService : Factory<TAnimal>, ISerialize, new().
  • As we're using Factory<TAnimal> to indicate our type of factory, we'll need to specify TAnimal as well.
  • When we retrieve a service, we'll do so by referencing the desired interface, not the concrete class.

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,

  1. A generic collection (List<T>) can only contain elements of the same type
  2. typeof(Factory<Elephant>) != typeof(Factory<Giraffe>)
  3. You can't cast to 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.

like image 94
ingen Avatar answered Nov 02 '22 02:11

ingen