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...
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.
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.
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();
}
}
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.
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.
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.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.