It seems that dependency injection (DI) as software development pattern has become the standard for many open source projects and software companies. Indeed, if you practice any kind of tiered development and employ test-driven practices, DI is likely a great direction for your projects.
When done right, DI allows developers to rather effortlessly extrapolate a complex design into well-defined and easily consumable modules. It gives the flexibility in how objects are interconnected thus allowing greater configuration facility for different modes of operation of your application. Having said all of this, the real trouble of DI is that in most cases the concept behind it is misunderstood and it becomes a major factor of abuse.
Here is just a few problems with dependency injection, which I come across time and time again.
- DI container is used as service locator.
- Object constructors explode with dependencies.
- Everything is abstracted and injected.
- The application tier has a reference to everything, even when it shouldn’t by-design, simply because it is configuring the DI container.
I have come up with a few practices over the years to minimize the negative impact of DI and basically lock it down in an effort to prevent abuse as much as possible.
The application loader
One of the things I completely despise about DI is the fact that the application tier is forced to reference every single component just because it is responsible for configuring the container. I think this smells.
In a typical tiered design all logic runs through some kind of service or business tier. The application tier should have no knowledge of anything below that architectural level. By introducing unnecessary dependencies at this level there are more chances for accidental use of any components contained within any architecturally restricted tiers.
To prevent this from happening I started using a separate project I call “The Loader.” This project is responsible for registering and configuring components on behalf of the application tier by providing a single point of access, called “The Kernel”, for convention-based component registration. Here is what this looks like in a sample project.
In the image above, the DependencyModule
class implements Autofac’s Module
to register the dependency graph. The loader project now has a reference to everything instead of the application layer, thus greatly diminishing the chance of abuse. And it just makes the application tier much cleaner.
public sealed class DependencyModule : Module
{
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.Register(c => new SqlConnection(ConfigurationManager.ConnectionStrings["MyDatabase"].ConnectionString))
.As<IDbConnection>()
.InstancePerHttpRequest();
builder.RegisterType<MemcacheProvider>()
.As<ICacheProvider>()
.SingleInstance();
...
}
}
The interesting bit here is the Kernel
class which provides convention-based component registration for the consumer (a.k.a your app).
public sealed class Kernel
{
private static readonly ContainerBuilder Builder;
static Kernel()
{
Builder = new ContainerBuilder();
Builder.RegisterModule(new DependencyModule());
}
public static void RegisterMvcControllers(Assembly assembly)
{
Builder.RegisterControllers(assembly);
}
public static void RegisterTasks(Assembly assembly)
{
Builder.RegisterAssemblyTypes(assembly)
.Where(task => typeof (IBootstrapperTask).IsAssignableFrom(task))
.As<IBootstrapperTask>()
.SingleInstance();
}
public static void RegisterMaps(Assembly assembly)
{
assembly.GetTypes()
.Where(type => typeof (Profile).IsAssignableFrom(type))
.ToList()
.ForEach(type => Mapper.AddProfile((Profile) Activator.CreateInstance(type)));
}
public static void Start()
{
var container = Builder.Build();
var mvcResolver = new AutofacDependencyResolver(container);
DependencyResolver.SetResolver(mvcResolver);
}
}
The application tier simply references the loader project and calls the Kernel
to configure itself.
Kernel.RegisterMvcControllers(Assembly.GetExecutingAssembly());
Kernel.RegisterTasks(Assembly.GetExecutingAssembly());
Kernel.RegisterMaps(Assembly.GetExecutingAssembly());
Kernel.Start();
The application tier doesn’t even know it’s using dependency injection container at all. Therefore, there is no way to use the DI container explicitly anywhere in the application tier. This removes the anti-pattern of using container as service locator.
While this is a very simple example of the kernel, this can be extended to cover more complex configuration scenarios where an application can configure components in a variety of different ways and take a different logical path during execution.
I’m sure there are some edge cases, but so far I have successfully used this on multiple large projects with great results.
The dependency overflow
Another thing to easily do wrong when using dependency injection is to mindlessly add constructor dependencies because, well, it’s so easy to do because the container will just resolve them.
The issue here is to realize that your architecture should be laid out without any reliance on any kind of inversion of control principles. Set a guideline for the number of dependencies per each component. If that number is exceeded it is time to break things up because your component is probably doing too much work. Always keep in mind the single responsibility principle.
What I found works rather well for me is to strictly adhere to the set number of dependencies in any component below the application tier while always strive to maintain this rule at the application tier (sometimes this isn’t possible due to strict design specs). The thought process here is to make sure you can easily construct and test any component below the application tier. As long as this is possible, the way in which components are stitched together at the application tier isn’t a very major issue.
The injection craze
DO NOT abstract and inject everything on earth. It is highly unlikely that you’re going to switch database technologies, so what’s the point of creating a highly flexible repository that works with ungodly amount of data access libraries? Not only are you wasting time, your abstraction is probably not going to be all-encompassing and will inevitably leak like a sieve.
Keep your design tight and sane.