The Dependency Iceberg - arcdev/engram404 GitHub Wiki
originally posted 2018-02-15 at https://engram404.net/the-dependency-iceberg/
Dependency Injection (or Dependency Inversion Principle or Inversion of Control) is a beautiful design pattern allowing the the developer to change the implementation at initialization time.
If you're here, I'm expecting that by now you've already heard of this pattern and may have even used it. If not, check out Wikipedia, or Microsoft's Unity docs, or the latest in ASP.NET Core docs.
It also forces the developer to use good interface design. But, there's a hidden cost to using it…
We usually think about the class we're trying to work with, but we tend to forget that every other class in the chain of dependencies is recursively created.
Just like an iceberg, we only see the tip, but so many more classes underneath are created.
We'll setup a nice simple set of interfaces & classes. (In my case, in a console app.)
Basically, Trunk depends on Branch, which depends on Trig, which depends on Leaf. (you get the idea: Trunk -> Branch -> Twig -> Leaf).
None of them have any behavior for our example (except to write a log statement when the concrete class is created).
For my DI framework, I'm going to use my favorite: Autofac. (It's an open-source, full-featured, mature dependency inversion framework.) And, yes – in case you're wondering – you can use it in place of Microsoft's [pitiful, minimal] implementation in ASP.NET Core [ref].
private static void Main(string[] args)
{
var container = SetupDependencies();
var demoContainer = container.Resolve<ITrunk>();
}
private static IContainer SetupDependencies()
{
var builder = new ContainerBuilder();
builder.RegisterType<Trunk>().As<ITrunk>().InstancePerLifetimeScope();
builder.RegisterType<Branch>().As<IBranch>().InstancePerLifetimeScope();
builder.RegisterType<Twig>().As<ITwig>().InstancePerLifetimeScope();
builder.RegisterType<Leaf>().As<ILeaf>().InstancePerLifetimeScope();
return builder.Build();
}
(They're all the same)
public interface ITrunk
{
void Execute();
}
public class Trunk : ITrunk
{
public Trunk(IBranch branch)
{
Console.WriteLine("ctor ---> Trunk");
}
public void Execute()
{
// NOOP just a placeholder
}
}
Notice that we only resolve ITrunk
, but we're getting every other dependency in the recursive set created… (oh, and in reverse order)
ctor ---> Leaf
ctor ---> Twig
ctor ---> Branch
ctor ---> Trunk
Let me say that again.
If your code is well-designed with:
- minimal (near zero) logic in your constructor
- minimal (or no) global fields in your objects
... then not much.
The CPU cost to construct all of these objects is minimal, but still non-zero.
A small example app like this is near-zero measurable CPU cost, and a tiny memory consumption, but it's still non-zero.
Consider a HUGE app with gads of dependencies… this could get large quickly – and costly.
What's worse: if your code isn't well-designed, you could end up with a large cost to create a single instance; especially if you never need the other objects. (Consider the case of validation – where validation fails and all of the other dependencies were constructed merely to be garbage collected almost instantly.)
Lazy initialization.
Basically, we'll change the constructors to look like this:
public class Branch : IBranch
{
public Branch(Lazy<ITwig> twig)
{
Console.WriteLine("ctor ---> Branch");
}
}
Of course that means we'll need to modify the DI setup.
The mapping between interface & implementation stays the same:
// for the final creation
builder.RegisterType<Trunk>().As<ITrunk>().InstancePerLifetimeScope();
builder.RegisterType<Branch>().As<IBranch>().InstancePerLifetimeScope();
builder.RegisterType<Twig>().As<ITwig>().InstancePerLifetimeScope();
builder.RegisterType<Leaf>().As<ILeaf>().InstancePerLifetimeScope();
But the real magic happens by registering the Lazy version. Here's just one of them:
builder.Register(context =>
{
Console.WriteLine("resolving Lazy<IBranch>");
return new Lazy<IBranch>(() =>
{
Console.WriteLine("resolving IBranch");
return context.Resolve<IBranch>();
});
}).As<Lazy<IBranch>>();
Basically, we're telling the DI framework that when we request a a Lazy instance (IBranch, in this case), then we're going to use a factory method to resolve the implementation of IBranch.
Of course, we've got the Console statements just for observation purposes. Once we get ride of those, we can really simplify this to:
builder.Register(context => new Lazy<IBranch>(context.Resolve<IBranch>)).As<Lazy<IBranch>>();
And, if we were really going to use this a lot, we'd probably create a helper method:
public static IRegistrationBuilder<Lazy<TInterface>, SimpleActivatorData, SingleRegistrationStyle> RegisterLazy<TInterface>(ContainerBuilder builder) {
return builder.Register(context => new Lazy<TInterface>(context.Resolve<TInterface>)).As<Lazy<TInterface>>();
}
So now when we run this app, we get exactly 1 construction:
ctor ---> Trunk
Now, it's barely even an ice cube.
Yup. For those of you who know Autofac pretty well, I kinda skimmed passed something: Autofac already knows how to use Lazy.
So as far as Autofac is concerned, you can register types simply:
builder.RegisterType<Trunk>().As<ITrunk>().InstancePerLifetimeScope();
builder.RegisterType<Branch>().As<IBranch>().InstancePerLifetimeScope();
builder.RegisterType<Twig>().As<ITwig>().InstancePerLifetimeScope();
builder.RegisterType<Leaf>().As<ILeaf>().InstancePerLifetimeScope();
and our constructors can still be bound to Lazy like:
public class Branch : IBranch
{
public Branch(Lazy<ITwig> twig)
{
Console.WriteLine("ctor ---> Branch");
}
Be aware of what's going on. Frameworks and patterns are marvelous things, but we should still have a basic understanding of what they're doing.
If your constructors are clean and the rest of your design is, there's not much need for modifying the standard Dependency Injection usage.
If you want some code to play with, here's my (cleaned-up) snippets while writing this.