Home > database >  Factory that should return different types based on input param
Factory that should return different types based on input param

Time:10-06

I have type based on which I need to create different objects. The one specific thing - all that object has some common part.

So I thought to have some base class for them. And in each separate class add only specific properties.

class BaseClass {
    public int Prop1 {get;set;}
    public string Prop2 {get;set;}
}

class MyClass1 : BaseClass {
    public int PropMyClass1 {get;set;}
}

class MyClass2 : BaseClass {
    public string PropMyClass2 {get;set;}
}

I thought about create factories:

interface ICreator<T>{
    bool CanHanlde(string type);
    T Create();
}

class Creator1: ICreator<MyClass1>
{
    bool CanHandle(string type) {return "type1" == type;}
    MyClass1 Create();
}

class Creator2: ICreator<MyClass2>
{
    bool CanHandle(string type) {return "type2" == type;}
    MyClass2 Create();   
} 

And now I would like to create some factory that will return concreate class based on type. Problem - I don't know how to pass type into generic. From where I need to get type? I can register all types for ICreator and inject it with DI. Select the one which return true in CanHandle method. But don't know how to get type for generic.

CodePudding user response:

I can register all types for ICreator and inject it with DI. Select the one which return true in CanHandle method. But don't know how to get type for generic.

Really bizarre question. My understanding is you want to iterate on the available types, call CanHandle and return that type if the method returns true. This will require instantiating an instance of each type. Probably this is not what you want, lots of ways for this to go wrong and can't even get compile time checking. But here's how to do this.

First, use reflection to get available types. Then filter the types to the ones that implement the interface. Then instantiate the class. Call "CanHandle" on the class (which the interface says will exist), and if true, you have found your class.

You can separate your classes by assembly/namespace for easier time finding particular classes.

using SomeNamespace;
using System.Reflection;

namespace SomeNamespace
{
    public class MyClass1 { public int Id { get; set; } }
    public class MyClass2 { public int Id { get; set; } }

    public interface ICreator<T>
    {
        bool CanHandle(string type);
        T Create();
    }

    public class Creator1 : ICreator<MyClass1>
    {
        public Creator1() { }
        public bool CanHandle(string type) { return "type1" == type; }
        public MyClass1 Create() { return new MyClass1(); }
    }

    public class Creator2 : ICreator<MyClass2>
    {
        public Creator2() { }
        public bool CanHandle(string type) { return "type2" == type; }
        public MyClass2 Create() { return new MyClass2(); }
    }
}

namespace SomeOtherNamespace
{ 
    internal class Program
    {
        static void Main(string[] args)
        {
            var types = Assembly.GetExecutingAssembly().GetTypes()
                .Where(t => t.IsClass && t.Namespace == "SomeNamespace")
                .ToList();

            var typeNameToTest = "type2";

            foreach (var type in types)
            {
                if (type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICreator<>)))
                {
                    // Requires resolving to a matching constructor, the default constructor (no arguments) is implemented above.
                    var newInstance = Activator.CreateInstance(type);
                    
                    // pick any implementation for nameof...
                    // Probably this can fail if `CanHandle` doesn't exist.
                    var canHandleMethod = type.GetMethod(nameof(Creator1.CanHandle));

                    // This can fail if the signature for `CanHandle` ever changes.
                    bool canHandleResult = (bool)canHandleMethod.Invoke(newInstance, new object[] { typeNameToTest });

                    if (canHandleResult)
                    {
                        Console.WriteLine($"Found class that can handle type \"{typeNameToTest}\": {newInstance.GetType().FullName}");

                        break; // or return newInstance
                    }
                }
            }
        }
    }
}

Console output:

Found class that can handle type "type2": SomeNamespace.Creator2

See also How to determine if a type implements a specific generic interface type and How can I get all classes within a namespace?.

CodePudding user response:

Since you are querying the types as string from a database, you have a dynamic scenario. I.e., you know the type only at runtime.

Generic types are always resolved at compile time. Therefore, they are not helpful here. Specifically it does not help much to return the concrete type (MyClass1, MyClass2) from the factory, as you cannot type the variable at runtime. You must use BaseClass at compile time.

 // You don't know whether this returns MyClass1 or MyClass2 at runtime
BaseType result = creator.Create(typeStringFromDb);

You can still use a generic interface to make it usable with other base types, but you will have to initialize it with a base type.

interface ICreator<TBase>{
    string Handles { get; }
    TBase Create();
}

Instead of having a method CanHandle returning a Boolean, I suggest returning the handled type in a string property. This allows you to store the factories in a dictionary and to use this string as key.

I'll do this in a static class with a static constructor to setup the dictionary.

// Given the classes
class Creator1 : ICreator<BaseClass>
{
    public string Handles => "type1";
    public BaseClass Create() => new MyClass1();
}

class Creator2 : ICreator<BaseClass>
{
    public string Handles => "type2";
    public BaseClass Create() => new MyClass2();
}

// The static creator
static class Creator
{
    private static readonly Dictionary<string, ICreator<BaseClass>> _creators = new();

    static Creator()
    {
        ICreator<BaseClass> creator1 = new Creator1();
        ICreator<BaseClass> creator2 = new Creator2();
        _creators.Add(creator1.Handles, creator1);
        _creators.Add(creator2.Handles, creator2);
    }

    public static BaseClass Create(string type)
    {
        if (_creators.TryGetValue(type, out var creator)) {
            return creator.Create();
        }
        return null; // Or throw exception.
    }
};

Usage

BaseClass result = Creator.Create("type1");
  • Related