Printf("%s %s", dependency, injection).
One of the hardest things for me to grokk in enterprise programming, was dependency injection (DI). Namely because that word already had a meaning to me, which didn’t require a lot of book readin' to understand.
In functional programming, DI means passing functions to functions.
Here’s an example function (Erlang):
-module(example).
-export(add_one/1).
add_one(N) -> N + 1.
Easy peasy; take a number, add one.
Lets inject it into a mapping function over a collection of numbers:
Eshell V12.0 (abort with ^G)
1> c(examples).
{ok,examples}
2> lists:map(examples:add_one, [0, 1, 2, 3]).
[1, 2, 3, 4]
Nice! lists:map iterates over our list and applies add_one to it.
How about Lisp?
* (DEFINE ADD-ONE (N) (+ N 1))
ADD-ONE
* (MAPCAR #'ADD-ONE '(0 1 2 3))
(1 2 3 4)
Yeah! JavaScript?
> const addOne = n => n + 1
undefined
> [0, 1, 2, 3].map(addOne)
(4) [1, 2, 3, 4]
Wonderful!
Now all we have to do is rename map to fmap, pretend that we understand words like “monoid operation” and BAM! We’re Haskellers.
Speaking of Haskell, the nice people of the GHC were so kind as to implement this in .NET, in the form of Language-Integrated Query (LINQ):
using System.Linq;
using System.Collections.Generic;
public static int AddOne(int n) => n + 1;
new List<int>(){0, 1, 2, 3}
.Select(AddOne); // [1, 2, 3, 4]
They called it Select as to not arouse any suspicion, hinting that this was just typed SQL, not functional programming. Sneaky sneaky.
So!
Whenever I heard DI, this is what I thought that referred to. Turns out no.
Hold on to your hats.
public interface IGetAThing
{
IThing GetThing();
}
public MyThingGetter : IGetAThing
{
private readonly IThingFactory _factory;
public MyThingGetter(IThingFactory factory)
{
_factory = factory;
}
public IThing GetThing()
{
return _factory.Get(thing.NORMAL);
}
}
public MyApi
{
private readonly IGetAThing _myThingGetter;
public MyApi(IGetAThing thing)
{
_myThingGetter = thing;
}
public IThing GetThing()
{
return _myThingGetter.GetThing();
}
}
Sweet Alan Kay, what is this!?
Now I’ll admit to this being a bit facetious, but its only really incorrect in regards to brevity. An actual example would span this entire article.
Here’s the argument for this style of programming:
-
Every dependency is injected (except the enum).
-
We’ve successfully split our program into nicely SOLID parts.
-
MyAPI exposes an interface for the intended user of the program, without knowing about the underlying implementations of that interface.
-
MyThingGetter exposes an interface for retrieving a Thing, but delegates the actual getting to a Factory that is injected at runtime.
-
Factory accepts an enum, preventing magic-string errors.
-
Any level can be swapped out without disturbing the layers above or below.
There is magic here, and it draws its power from the depth of our program:
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
services.AddHostedService<Program>()
.AddScoped<IThingFactory, ThingFactory>()
.AddScoped<IGetAThing, MyThingGetter>());
(I've omitted the XML for the sake of your health)
Pros.
-
Clear lines between layers of abstraction.
-
Swappable components via the strategy pattern.
-
Have to touch existing code less often.
Cons.
-
Death by a thousand classes.
-
Garbage collector field day.
-
Gnarly behemoths to test.
Each invidivual component might be easy to grasp, put this comes at the cost of having to rewind the tape and insert the implementation into the slot that came before it. It’s as if you took one screen of Lisp code and shattered over several files.
Now, while you’re befriending your new best mate Go to definition (F12), there’s no one around to hold your hand once you get to testing. The plot thickens.
IMyThingGetter _myThingGetter;
public static void TearMeUp()
{
_myThingGetter = new Mock<MyThingGetter>().When(MyThing.GetThing).Do((ThingType t) => {
t == thing.NORMAL ? new Thing() : throw new ArgumentExceptionError();
}
}
[MakeThisTestRunPlease(true)]
public static void Test_MyThingGetter_Should_GetAThing_When_WeWantTo()
{
// ARRANGE.
TearMeUp();
// ACT.
var thing = _myThingGetter.GetThing(thing.NORMAL);
// BUY MY BOOK.
assert.Equal(thing, new Thing());
WakeMeUpInside();
}
public static void WakeMeUpInside()
{
_myThingGetter = null;
}
What does this have to do with DI?
Not much, which is my point.
Often in enterprise software, by you opting in for dependency injection of this style you seem to get the jungle attached to the Gorilla, attached to the banana you first wanted. Hence when discussing bananas, it’s not long before you’re on the subject of poison dart frogs.
There is no danger in manually constructing infrastructure by hand:
logger := log.New(log.DefaultConfig{})
dbConfig := db.NewConfig{
Logger: logger,
}
db := db.New(dbConfig)
myApi := &myApi{
Logger: logger,
DB: db,
}
It’s easy to read, easy to write, easy to understand. Requires no dependencies. If you need to add anything else, it’s right here. No registration, no XML files. Just code. Your code.
If you actually need more abstract DI, interfaces are wonderful, absolutely. Passing a Reader as a dependency to a struct might have its uses, but passing it directly to your function saves you even more headaches, especially when testing.
If you’re lucky to be writing in a functional style, passing functions to other functions is even easier, and can at times offer amazing compile time safeties.
Compose functions to make smarter ones, let data be data.
Pass some values to your functions!