5
\$\begingroup\$

Since I had a lot of classes to store inside 1 main class I decided to create a handler system. I was having to create 30 - 50 private readonly fields for private instances of classes, then assigning them all in the constructor, then creating a public method to get them via a return. I decided instead of having up to 150 lines of code that I would try and sort out a more organised system and put it in place.

I start by declaring an empty interface for all my handlers to inherit that interface. This interface is in a totally seperate .cs file.

namespace Panda.Root.Core.Other
{
    public interface IPandaClass { }
}

Now, I declare a new dictionary in my main class (PandaServer.cs)

private readonly Dictionary<string, IPandaClass> _handlers;

I then assign the dictionary:

public PandaServer()
{
    _handlers = new Dictionary<string, IPandaClass>(StringComparer.InvariantCultureIgnoreCase);
}

I create two methods, one for registering new handlers (or replacing them) and one for returning the handler by name.

private void RegisterHandler(string handlerName, IPandaClass handler)
{
    _handlers[handlerName] = handler;
}

public IPandaClass GetHandler(string handlerName)
{
    return _handlers[handlerName];
}

And then I can register as many hadlers as I can, pretty easily:

RegisterHandler("messages", Utilities.CreateInstanceOf<MessageHandler>());
RegisterHandler("configuration", Utilities.CreateInstanceOf<ConfigurationManager>());
RegisterHandler("console_worker", Utilities.CreateInstanceOf<ConsoleWorker>());
RegisterHandler("base_handler", Utilities.CreateInstanceOf<BaseHandler>());

I then declare them where I need them like this

var panda = Program.GetPandaServer();
var pandaBase = panda.GetHandler("base") as BaseHandler;

if (pandaBase == null)
{
    return;
}

var playerHandler = pandaBase.GetHandler("player") as PlayerHandler;

I was told declaring all your classes there instead of making multiple calls in future was better as you would always have that variable there incase you wanted to make multiple calls to the same method or methods of that method's return type.

The only major bad things I see with this system is you're hardcoding the name of the handlers but I guess there isn't a way around that, its how it works. If anyone can think of any other improvements please let me know.

\$\endgroup\$
3
\$\begingroup\$

IServiceProvider

So you decided to reinvent the IServiceProvider Interface :-) which

Defines a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.

Your public interface IPandaClass { } is exactly the same. I guess you forgot to mention that it has the GetHandler method or otherwise it would be completely meaningless.

Then we read:

This interface is implemented by a class or value type that provides a service to other objects.

This is the missing GetHandler method.


Generics vs strings

RegisterHandler("messages", Utilities.CreateInstanceOf<MessageHandler>());

Instead of hardcoding strings you could use generics and derive the name from the class name or just use the type as a key.


Dependency Injection

Do you really need all classes in one handler? Have you tried something like Autofac etc.?

\$\endgroup\$
2
\$\begingroup\$

Problems in the current solution

Your solution is not optimal due to 2 reasons:

  1. Using string as key can be simple to make but it's often times hard to use and maintain. Different people have different conventions - I would write BaseHandler as a class name but someone else might prefer Base_Handler, which would make accessing the types by string difficult, the weird thing is you're using both conventions in 1 place.

    One solution to this problem would be a class that converts conventions which does solve the problem in some way, but the major problem is that you still need to remember every single class' name that you have.

    What I would suggest is simply make the Key collection use Type instead of string. This way you can make use of the type safety and there wont be conflict between different conventions.

  2. Even tho you're able to register KeyValuePair<,> in a single line, you still need to write new line for every single class, while there is a shorter way using System.Reflection.

Constructing the solution

First let's create a class that makes operating with types easier.

  1. We will need all types that inherit our interface.

  2. We will also need an instance of those types.

For this part we will be using the System.Reflection namespace, this will allows us to obtain different information during the run-time of the program.

Obtaining all the types

All types are contained in some namespace, even if you don't add one in your file, it will be part of the global assembly which is usually your project.

Most classes with similar functionality are contained in the same assembly.

The first part is always true, but the second one might not be and if it's not true than this will cause us problem, I will explain later in the example why. But for now let's assume both points are true.

With that in mind we can build the basics of our class:

public class TypeHelper
{
    private readonly Type[] _executingAssemblyTypes;

    public TypeHelper(Assembly executingAssembly)
    {
        if (executingAssembly == null)
        {
            throw new ArgumentNullException(nameof(executingAssembly));
        }
        _executingAssemblyTypes = executingAssembly.GetTypes();
    }

    public TypeHelper() : this(Assembly.GetExecutingAssembly())
    {
    }
}

We'd like to cache the result of all the types from the selected assembly because it would be inefficient to obtain them every single time we operate on them.

Now, if you have a class that is contained in 'X' assembly but that class also inherits an interface from assembly 'Y', you can tell that there will be some difficulties here as this class accepts only a single assembly. I don't believe this applies to your case and that's why I haven't included it. But if it does make sure to let me know in the comments and I will update the answer.

Filtering only derived types

We just got all of the types contained in set assembly, we know need to remove the ones that don't inherit our interface.

public IEnumerable<Type> GetDerivedTypes(Type baseType, Func<Type, Type, bool> filter)
{
    if (baseType == null)
    {
        throw new ArgumentNullException(nameof(baseType));
    }
    if (filter == null)
    {
        throw new ArgumentNullException(nameof(filter));
    }
    return _executingAssemblyTypes.Where(t => filter.Invoke(baseType, t));
}

public IEnumerable<Type> GetDerivedTypes(Type baseType)
{
    return GetDerivedTypes(baseType, IsDerivedTypeDefault);
}

private bool IsDerivedTypeDefault(Type baseType, Type targetType)
{
    return baseType.IsAssignableFrom(targetType) && baseType != targetType;
}

We are using a simple Dependency Injection to allow the user to select the filter and determine what is a derived type. For example you might not want to get any abstract classes or interfaces because we cant know exactly what the user wants we offer him a default version where we get all objects that inherit the base one and a version where he can specify the filter.

Creating type instances

We can solve this part in many different ways depending on the freedom we want to provide the user.

I will give you a few examples of methods in ascending order of complexity and flexibility:

  1. The most basic function would be to create a instance of the type using Activator.CreateInstance(type):

    public IEnumerable<T> CreateInstancesOfType<T>(IEnumerable<Type> types)
    {
        if (types == null)
        {
            throw new ArgumentNullException(nameof(types));
        }
        return types.Select(type => (T)Activator.CreateInstance(type));
    }
    
  2. Next we can allow the user to specify the arguments passed to the CreateInstance method:

    public IEnumerable<T> CreateInstancesOfType<T>(IEnumerable<Type> types, IEnumerable<object[]> typeArguments)
    {
        return CreateInstancesOfType<T>(types, typeArguments,
            Enumerable.Repeat<Func<Type, object[], object>>(
                Activator.CreateInstance, typeArguments.Count()));
    }
    
  3. Notice that we are calling a different overload of the method which accepts 3 arguments, this is because that's the last variant I made of this method. It allows the user to specify also how to create the objects:

    public IEnumerable<T> CreateInstancesOfType<T>(IEnumerable<Type> types,
        IEnumerable<object[]> typeArguments, IEnumerable<Func<Type, object[], object>> initialisators)
    {
        if (types == null)
        {
            throw new ArgumentNullException(nameof(types));
        }
        if (typeArguments == null)
        {
            throw new ArgumentNullException(nameof(typeArguments));
        }
        if (initialisators == null)
        {
            throw new ArgumentNullException(nameof(initialisators));
        }
        var items = typeArguments.Zip(initialisators,
                (arguments, initialisator) => new {Arguments = arguments, Initialisator = initialisator})
            .Zip(types, (t, type) => new {Type = type, T = t});
    
        return items.Select(item => (T) item.T.Initialisator.Invoke(item.Type, item.T.Arguments));
    }
    

There are other options if you want to have a single class to accept parameters and all the others are parameterless, for example using the extension method Except on the returned collection and then manually adding the instantiated class.

Note You can also cache the results and put them into a private dictionary to fetch items from there if you plan on handling better multiple calls from the same type.

Creating the Dictionary<,>

With that we are pretty much done, all that's left is to initialize a dictionary.

We want our dictionary initialization to be short and clean. We can create a method for that too. We will also need an instance of the TypeHelper class.

With that your class can look like this:

public class PandaServer
{
    private readonly Dictionary<Type, IPandaClass> _handlers;
    private readonly TypeHelper _typeHelper = new TypeHelper();

    public PandaServer()
    {
        IEnumerable<Type> types = _typeHelper.GetDerivedTypes(typeof(IPandaClass));
        IEnumerable<IPandaClass> typeInstances = _typeHelper.CreateInstancesOfType<IPandaClass>(types);

        _handlers = MapToDictionary(types, typeInstances);
    }

    private Dictionary<TKey, TValue> MapToDictionary<TKey, TValue>(IEnumerable<TKey> keys,
        IEnumerable<TValue> values, IEqualityComparer<TKey> comparer)
    {
        return keys.Zip(values, (k, v) => new { Key = k, Value = v })
            .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, comparer);
    }

    private Dictionary<TKey, TValue> MapToDictionary<TKey, TValue>(IEnumerable<TKey> keys,
        IEnumerable<TValue> values)
    {
        return MapToDictionary(keys, values, EqualityComparer<TKey>.Default);
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.