Abusing dependency injection containers

What? You really want to abuse DI containers – one of the holy buzzwords of today’s software architecture?!

Yes, but please give me the time to explain the why and how.

There haven’t been much posts on my blog, but quite some progress has been made on the Graph Storage technology in the last few months. This means that a tremendous amount of new code has been written. Also, many of the old code has been refactored to cope with new insights. I guess there is still plenty room for improvement, but at this moment the overall structure of the solutions, projects and source is pretty nice.

One thing that I really grown fond of is using the DI container just a little bit different than propagated everywhere on the internet.

Whenever I see DI applied the container takes the central role in compositing the whole application. This isn’t that big of a problem for small and average applications. However, things can become quite messy if a container is used to apply structure to a large set of intertwined class instances.

Don’t get me wrong, your application will most of the time benefit if you use DI to setup things. It will allow you to better work towards the SOLID principles, ensure you DRY and allow for better unit/integration tests.

However, having one single container and the necessary configuration (registrations etc. – I don’t like automagically registered items) to get things up and running also has its pitfalls. One of the aspects that I really don’t like is that it allows developers to create illogical compositions. They have access to all the classes that the container is able to resolve, and can make spooky combinations with them. One single constructor parameter suffices to inject one class into the other. And even if the two classes are functionally absolutely unrelated, the container happily uses them. It allows the developers to create relations between components that have nothing in common.

Just imagine an container that is able to inject all instances needed to run some kind of cool website. It would be really bad if it was possible to inject high level validation components into low level data access parts of your code base, right?

Creating such relations should in my opinion not be possible because it leads to unnecessary entanglement that increases complexity and reduced maintenance.

Before we continue: Let me give a bit of background about the project I have been working on.

I’ve been working for more than two years on a new (hopefully innovative) homebrew ‘Graph Storage’ technology. At this moment we are talking about a 4 solution, 60+ project setup verified by 1600+ automated unit and integration tests (I know, it should be more :-)). The somewhat abstract purpose of this monster is to create a radical new method to cope with all kinds of (personal) digital data.

During the time working on it, I have seen at least three primary subsystems being born:

  • Storage – Contains all storage related features needed to implement a Graph Storage solution and at the same time abstract away the underlying storage facility (Ntfs, Amazon, Azure, GFS etc.)
  • Infrastructure & hosting. These two subsystems enables connectivity and scalability, of course again using abstraction to ensure that more no communication technology lock-in can occur (Web Api, SignalR, X-Sockets, TCP/IP etc.)
  • Api – The complexities of graph based storage needs to be hidden to become useful. This layer allows developers both low-level and high-level (script / Linq based) access to the stored information.

There is not that much software architecture experience needed to conclude that all three systems are in fact huge n-level compositions of classes, made available by some facades to hide away the enormous underlying complexity.

DI factory/façade/composition guidelines

In the first few months only one single container was used to instantiate and wire-up all the needed code. Although this worked, it truly became a development nightmare very fast. As I already stated in the first part of this post DI allows classes to be related to others. Adding unnecessary (but still valid) relations do not need to cause immediate, detectable side-effects like compiler errors, unit tests failing or runtime errors. They can however create something I like to call OO spaghetti – a dangerous combination of many, small intertwined classes with such a high level of complexity that developers completely lose overview.

One thing that I really don’t like is determining the correct lifetime (transient, singleton, scoped) for instances with lots and lots (indirect) relations to others. Somehow I always tend to create structures in which singletons keep other instances alive for far too long. Most of the time this isn’t that big of a problem. But be aware for situations in which you need to create asynchronous processing pipelines, are working with other technologies or need to communicate with external platforms.

To get rid of such problems I introduced the personal rules below:

Rule 1: Keep the size of container configuration(s) interpretable.

It’s actually quite simple: Don’t use a DI container like a magic wand that instantiates your whole application. Doing so will come back and hunt you at the moment the application increases in complexity.

The first sign I saw the container configuration getting too big was when I tried to spread out all container registrations across multiple classes. I was somehow looking for a way to keeping track of the responsibilities assigned to the DI container. However, I still kept trying to use one single container because at that time I believed it was a good idea performance-wise.

The reason for this is the well-known rule of thumb that we should ‘only use one container’ per application.

– In my opinion that’s just plain silly –

As we all know dependency injection comes with a performance penalty. It depends on the DI container used, but it just takes some time to create the container configuration and use it to instantiate all required classes. New containers should therefore only be sparsely to layout the global application structure, and not in situations that could cause a serious performance degradation (for example in a loop piece of code).

However, the global structure of any application needs to be defined and instantiated – one way or the other. As far as I can see the best way to do this is with multiple containers that all are responsible for setting up a small portion of the application structure hierarchy.

Benefits of this approach are:

  • It scales really well when your application grows in size.
  • It enforces you to think about a more subdivided architecture.
  • Such an architecture introduces hard boundaries: Invalid instance relations are hard to achieve.
  • Also, applications developed this way can be maintained easier because they are less intertwined in comparison to similar ‘one single container’ powered counterparts.

The approach that I tend to take is to identify groups of related classes and determine a method to instantiate those using dedicated containers. Let’s talk about this approach a bit more.

Rule 2: Use factories and facades to wrap usage of complex compositions.

So if we have an feeling how the application can be approximately divided into different subsystems, we could use a container to register all services for each of those subsystems. But where would we put that code? What pattern would be a good fit for this approach?

Ok, I know that’s a stupid question after putting quite some hints in the title of this rule, but yes, a factory class is a perfect fit to collect all container registration magic. Especially when it spits out a façade that hides the complexities of the composition it wraps around.

So how does this work in code? What I have started doing is adding all container registrations and related configuration stuff into the Create method of my factories. These Create methods are actually the only places a container is used as of now. This way I always know where to find the code that defines the subsystem and also know that I should be a bit anxious on executing it too frequently. Let me demonstrate with a snippet of the Graph Storage project.

namespace EtAlii.Ubigia.Api.Logical
{
    using EtAlii.Ubigia.Api.Fabric;
    using EtAlii.xTechnology.MicroContainer;
    using EtAlii.xTechnology.Logging;

    public class LogicalContextFactory : ILogicalContextFactory
    {
        public ILogicalContext Create(IFabricContext fabric, IDiagnosticsConfiguration diagnostics = null)
        {
            var container = new Container();
            container.Register(() => fabric);
            container.Register<ILogicalContext, LogicalContext>();
            container.Register<ILogicalConfiguration, LogicalConfiguration>();
            container.Register<ILogicalNodeSet, LogicalNodeSet>();
            container.Register<ILogicalRootSet, LogicalRootSet>();
            container.Register<IPropertiesManager, PropertiesManager>();
            container.Register<IPropertiesGetter, PropertiesGetter>();
            container.Register<IContentManagerFactory>(() => new ContentManagerFactory().Create(fabric));
            diagnostics = diagnostics ?? new DiagnosticsFactory().Create(false, false, false
                () => new DisabledLogFactory(),
                () => new DisabledProfilerFactory(),
                (factory) => factory.Create("EtAlii", "EtAlii.Ubigia.Api"),
                (factory) => factory.Create("EtAlii", "EtAlii.Ubigia.Api"));
            container.Register<IDiagnosticsConfiguration>(() => diagnostics);
            container.Register<IGraphPathBuilder, GraphPathBuilder>();
            container.Register<IGraphPathTraverserFactory, GraphPathTraverserFactory>();
            container.Register<IGraphComposerFactory, GraphComposerFactory>();
            container.Register<IGraphPathAssignerFactory, GraphPathAssignerFactory>();
            return container.GetInstance<ILogicalContext>();
        }
    }
}

As you can see the LogicalContextFactory contains only the registrations necessary to get a LogicalContext (the façade) up and running. Internally it uses a GraphPathBuilder, GraphPathTraverser GraphComposer  and GraphPathAssigner that are all instantiated using their designated factories.

Rule 3: Only use singleton registrations  

During refactoring towards DI powered factories ‘n facades, I came to the conclusion that I could not think of any SOLID reason to actually apply transient container registrations anymore. Each containers primary purpose has changed into a tool with the sole purpose to compose all permanent components of a subsystem together. Nothing more and nothing else. Being able to create transient instances only makes things more complex than needed and should in my humble opinion not be a responsibility of a container. Of course transient registrations will most probably find their usage somewhere, but I’m betting 99% of the container related code could do without. For this reason I removed this option from my homebrew container implementation and haven’t looked back once. 🙂

Rule 4: Use the DI factory/façade pattern for all high-level application scaffolding.

In the past few months I’m also started to introduce this approach in other (commercial) projects as well and must say being surprised by its flexibility. Last week we ran into an issue in one of our windows services. The problem was related to non-thread safe database access. The first proposals to fix this actually involved introducing new patterns (lazy instances etc.), but having the correct factories already in place it was easy to fine-tune the instantiation and lifetime of the necessary classes to run on the correct threads.

The statement that I want to make is that a good architecture provides developers enough control to tune and tweak the codebase when needed. In my opinion this doesn’t mean that there should be zillion different ways to achieve something, but a few simple, flexible ones that can be reused over and over (KISS).

Rule 5: All registrations should be done against interfaces.

I know, SOLID already tells us to do so but the benefits become even more visible when a good factory powered DI composition is in place. I’ve personally needed a really long time to see the true benefit of this one, but doing this for all application ‘scaffolding’ components and subsystems (i.e. everything that is not data) really makes your life far easier in the long run.

One of the benefits that I especially like is that if for some reason we would like to substitute a part of the system (say a faster way for graph path traversal) we can completely replace a subsystem temporary with ease.

Also stubbing (for unit testing) and logging (using decorators) can be applied very easy without making your primary code ‘dirty’. It would be wrong to state that this is easier in comparison to single-container applications. However, having a solid DI powered factory/façade structure in place also allows to do fancy tricks with stubbing/decorating during runtime of your application. Just imagine the possibility to toggle profile/logging/diagnostics or even whole subsystems on or off when requested.

Remark.

There are millions and zillions of applications and architectures to choose from. The rules I mention in this post work for me, but are definitely not written in stone. There will probably be enough applications that really require another approach – Each situation is unique and based on different requirements. But still the essence stays the same: Finding a fitting architecture that fits your team and growths when needed is a challenging job..

Nevertheless I would like to use the above rules as a catalyst to get the conversation about high-level DI usage started. Have you got ideas, opinions or questions about the topic then please feel free to post them so that everyone can benefit.
Just keep it nice. 🙂

Thanks!

programming

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s