Instantiating Abstract Class Subclasses via Reflection in C#

Instantiating Abstract Class Subclasses via Reflection in C#

This post demonstrates a way of instantiating abstract class subclasses via reflection in C#. As an example, it uses the Calculator project, which implements arithmetic expressions, including their construction, evaluation, and conversion to and from JSON and XML.

Here are a few related posts that cover various other aspects of the calculator project:

The starting point is the following method:

public static Operation FromName(string operationName)
{
	return operationName switch
	{
		Addition.OperationName => new Addition(),
		Division.OperationName => new Division(),
		Multiplication.OperationName => new Multiplication(),
		Subtraction.OperationName => new Subtraction(),
		_ => throw new Exception("Unsupported operation name")
	};
}

This post reworks the method to eliminate the need for manual updates to it when adding new operations and discusses other optimisations to simplify the process of adding new operations.

All changes described in this post are part of this pull request.

Step 1: Operations Instantiater

The first commit introduces the OperationsInstantiater class.

Previously, the logic to instantiate operations by name was in the Operation class. This update is a good opportunity to isolate that logic into its own class.

Step 2: Tests Standardisation

At this stage, the operations instantiation logic is untested, so it cannot yet be modified. This section addresses that oversight.

The second commit organises all operations into the Operations folder/namespace. This change intends to make the distinction between operation and non-operation classes more explicit.

The third commit introduces tests to ensure that each operation can be instantiated. Since the focus is on operations rather than OperationsInstantiater, each operation gets its own unit tests class.

The fourth commit adds tests to ensure that each operation can be evaluated. This checks that multi-valued expressions for each operation can be correctly evaluated.

As a result, all operations are implemented in the same, standardised way: located in the Operations folder and tested for instantiation and evaluation.

Step 3: Reflection

The fifth commit rewrites the OperationsInstantiater class to dynamically collect all operation classes without the need for explicit updates. It filters for classes that inherit from the Operation class, are not abstract, and have a default constructor. In particular, this excludes the Constant class and the Operation class itself.

This technique of creating instances of types at runtime is known as reflection.

The class uses a static constructor, which, according to the documentation, is a special method executed the first time the class is used. It is called at most once.

public sealed class OperationsInstantiater
{
    private static readonly Dictionary<string, Type> OperationTypes;
    static OperationsInstantiater()
    {
        OperationTypes = Assembly.GetExecutingAssembly()
            .GetTypes()
            .Where(t => typeof(Operation).IsAssignableFrom(t) && !t.IsAbstract)
            .Where(t => t.GetConstructor(Type.EmptyTypes) != null)
            .ToDictionary(t => ((Operation)Activator.CreateInstance(t)!).Name, t => t);
    }
    public Operation Create(string operationName)
    {
        if (OperationTypes.TryGetValue(operationName, out Type? operationType))
        {
            return (Operation)Activator.CreateInstance(operationType)!;
        }

        throw new Exception($"Unsupported operation name: {operationName}");
    }
}

Step 4: More Operations

At this stage, everything is prepared to make implementation of new operations as simple as possible. The sixth commit demonstrates this by adding direction, exponentiation, logarithm, and standard deviation operations.

Conclusion

This post showcased how reflection and standardised tests can streamline the process of adding new operations to the calculator project.

Read more