Decorators, IoC, and DI

Decorators and Dependency Injection

We love Inversion of Control (IoC) and Dependency Injection (DI) at Netwealth. They help us write clean, testable code. We're also coming to love Decorators as a great pattern to flexibly extend functionality.

The problem is getting them to all work together. First, some background...

What are decorators?

Decorators are a software Design Pattern that allow flexible layering of functionality. This is achieved by recursively nesting objects that implement a common interface. For example, if you needed to supply some data that was currently persisted in a file, you might create a decorator that would cache the results. One key benefit is that this same decorator could be reused on your Blob Storage implementation, and your API implementation.

What are Ioc Containers?

IoC Containers provide a simple, yet very powerful, way to recursively inject objects into class constructors. There are many available implementations, but they all do broadly the same thing. A series of lookups are registered with the container that tell it to supply X if Y is requested.

For instance, if a constructor needs an IThing the container will instantiate a Thing and inject it into the constructor. If Thing also has a parameterised constructor, the container will recursively deal with that in the same way.

IoC containers remove the burden from developers to manually instantiate objects.

Basic Example

Let's start with a simple example where we need 'something' to supply a list of Instruments.


internal interface IInstrumentReader
{
    Task<IList<Instrument>> ReadAsync();
}
This is needed by our Instrument processing class...

internal class InstrumentProcessor
{
    private readonly IInstrumentReader _reader;

    public InstrumentProcessor(IInstrumentReader reader)
    {
        _reader = reader;
    }

    internal async Task ProcessAsync()
    {
        var instruments = await _reader.ReadAsync();

        // Process instruments...
    }
}
We're building this out as a little console app, so we'll read the Instruments from a known file...

internal class InstrumentFileReader : IInstrumentReader
{
    public Task<IList<Instrument>> ReadAsync()
    {
        // Do the reading here...
    }
}
Then our IoC registration will look something like this...

internal static class ServiceProviderSingleton
{
    internal static IServiceProvider Instance { get; }

    static ServiceProviderSingleton()
    {
        var services = new ServiceCollection();

        Instance = services
                    .AddSingleton<IInstrumentReader, InstrumentFileReader>()
                    .BuildServiceProvider();
    }
}

The Problem

All this works fine, but we found that it took too long to always read from the file every time anything needed the list of Instruments. We'd like to introduce a decorator to handle caching, but how are we going to register that configuration with our IoC container?

The nature of the Decorator Pattern involves injecting different implementations of the same interface. Our 'vanilla' IoC container is going to struggle with that.

Cached Decorator

Here's our Decorator to cache the results of anything that implements IInstrumentReader...

internal class CachedInstrumentReader : IInstrumentReader
{
    private readonly IInstrumentReader _inner;
    private IList<Instrument> _cache;

    public CachedInstrumentReader(IInstrumentReader inner)
    {
        _inner = inner;
    }

    public async Task<IList<Instrument>> ReadAsync()
    {
        if (_cache == null)
        {
            _cache = await _inner.ReadAsync();
        }

        return _cache;
    }
}
But now, how to register this in our IoC container? The problem we have is that now 2 classes need an IInstrumentReader for their constructor, but each needs a different implementation.
The solution, for us, was to introduce the Factory Pattern for our File reader...

internal class InstrumentFileReaderFactory
{
    public InstrumentFileReader Create()
    {
        // We can also inject any needed parameters into the factory's constructor.
        return new InstrumentFileReader();
    }
}

The final step is to alter our IoC registration a little bit by hooking up the new Factory, and extending the existing routines for the IInstrumentReader singleton...

internal static class ServiceProviderSingleton
{
    internal static IServiceProvider Instance { get; }

    static ServiceProviderSingleton()
    {
        var services = new ServiceCollection();

        Instance = services
            .AddSingleton<InstrumentFileReaderFactory>()
            .AddSingleton<IInstrumentReader, sp =>
            {
                var factory = sp.GetService<InstrumentFileReaderFactory>();

                var inner = factory.Create();

                return new CachedInstrumentReader(inner);
            })
            .BuildServiceProvider();
        }
}
The result is that our InstrumentProcessor class still receives something that allows it to read instruments, but has absolutely no clue that we've added caching.

Why use a Factory?

Good question. We could have solved the problem like this...


internal static class ServiceProviderSingleton
{
    internal static IServiceProvider Instance { get; }

    static ServiceProviderSingleton()
    {
        var services = new ServiceCollection();

        Instance = services
            .AddSingleton<IInstrumentReader, sp =>
            {
                var inner = new InstrumentFileReader();

                return new CachedInstrumentReader(inner);
            })
            .BuildServiceProvider();
    }
}
But what if InstrumentFileReader needs some constructor arguments? We then have to manually get those. If the order changes or additions are made later, we'll need to amend our code again. This is a pain and we're not utilising the power of our IoC container in this way.

If you also love Design Patterns and would like to work with other like-minded developers, check out our Open Engineering Positions.