Dependency injection for beginners
You're in the car business, your job is to make cars on-demand. The object-oriented programmer in you says: "no problem, I'll make a blueprint that I can use to make as many cars as I want!".
class Car { public function drive() { // ... } }
For this car to work, it needs an engine and wheels. Now, there are several approaches to achieve that goal. You could, for example, do the following:
class Car { public function __construct() { $this->engine = new Engine(); $this->wheels = [ new Wheel(), new Wheel(), new Wheel(), new Wheel(), ]; } public function drive() { ... } }
There's the blueprint for every car you'll make! Next up, your boss comes to you and says there's a new client and he wants an electric car.
So you end up doing this.
class ElectricCar extends Car { public function __construct() { parent::__construct(); $this->engine = new ElectricEngine(); } }
"Beautifully solved"โyou think.
There's of course that redundant normal engine that's created when calling parent::__construct()
,
but at least you could re-use the wheels!
I think you can see where this is going.
The next client wants a car with some fancy wheel covers,
another one would like a diesel engine with those same wheel covers,
another one requests a race car,
and the last one wants a self driving car.
Ohโthere also was a client who wanted to buy an engine to build a boat with himself,
but you told your boss that wouldn't be possible.
After a while, there's a ton of blueprints in your office, each describing a very specific variation of a car. You started with a neatly ordered pile of blueprints. But after a while you had to group them in different folders and boxes, because it was taking too long to find the blueprint you're looking for.
Object oriented programmers often fall into this trap of inheritance, ending in a completely messed up codebase. So let's look at a better approach. Maybe you've heard about "composition over inheritance" before?
Composition over inheritance is the principle that classes should achieve polymorphic behavior and code reuse by their composition rather than inheritance from a base or parent classโWikipedia
That's a lot of buzzwords. Let's just look at our car example.
The principle states that Car
should achieve its polymorphic behaviour
by being composed of other classes.
The word polymorphic literally means "many shapes"
and implies that Car
should be able to do drive
in many different ways,
depending on the context it's used in.
With code reuse, we're trying to make code reusable; so that we don't end up with tens of classes doing almost exactly the same thing.
# What does this have to do with dependency injection?
Instead of making a unique blueprint that describes every single possible variation of a car,
we'd rather have Car
do one thing, and do it good: drive.
This means it shouldn't be the car's concern how its engine is built, what wheels it has attached. It should only know the following thing:
Given a working engine and four wheels, I'm able to drive!
We could say that in order for Car
to work, it needs an engine and wheels.
In other words: Car
depends on Engine
and a collection of Wheels
.
Those dependencies should be given to the car. Or, said otherwise: injected.
class Car { public function __construct( Engine $engine, array $wheels ) { $this->engine = $engine; $this->wheels = $wheels; } public function drive() { $this->engine->connectTo($this->wheels); $this->engine->start(); $this->engine->accelerate(); } }
Would you like a race car? No problem!
$raceCar = new Car(new TurboEngine(), [ new RacingWheel(), new RacingWheel(), new RacingWheel(), new RacingWheel(), ]);
That client who wanted special wheel covers? You've got that covered!
$smugCar = new Car(new Engine(), [ new FancyWheel(), new FancyWheel(), new FancyWheel(), new FancyWheel(), ]);
You've got a lot more flexibility now!
Dependency injection is the idea of giving a class its requirements from the outside, instead of having that class being responsible for them itself.
# What dependency injection is not
Built upon this simple principle, there are frameworks and tools that take it to the next level. You might, for example, have heard about the following things before.
# Shared dependencies
One of the most beneficial side effects of injecting dependencies, is that the outside context can control them. This means that you can give the same instance of a class to several others that have a dependency on that class.
Shared- or reusable dependencies are the ones most often getting the label "dependency injection". Though it's certainly a very good practice, sharing a dependency is not actually the core meaning of dependency injection.
# The dependency container
Sometimes it's also called "inversion of control" container, though that's not an accurate name.
Whatever the exact name, the container is a set of class definitions. It's a big box that knows how objects in your application can be constructed with other dependencies. While such a container definitely has a lot of use cases, it's not necessary to do dependency injection.
# Auto wiring
To give developers even more flexibility, some containers allow for smart, automatically determined, class definitions. This means you don't have to manually describe how every class should be constructed. These containers will scan your code, and determine which dependencies are needed by looking at type hints and doc blocks.
A lot of magic happens here, but auto wiring can be a useful tool for rapid application development.
# Service location
Instead of injecting dependencies into a class, there are some tools and frameworks that allow a class to ask the container to "give it an instance of another class".
This might seem beneficial at first, because our class doesn't need to know how to construct a certain dependency anymore. However: by allowing a class to ask for dependencies on its own account, we're back to square one.
For service location to work, our class needs to know about the systems on the outside.
It doesn't differ a lot from calling new
in the class itself.
This idea is actually the opposite of what dependency injection tries to achieve.
It's a misuse of what the container is meant to do.
# Inject everything
As it goes in real-life projects, you'll notice that dependency injection is not always the solution for your problem.
It's important to realise that there are limits to the benefits of everything. You should always be alert that you're not taking this to the extreme, as there are valid cases in which a pragmatic approach is the better solution.
# In closing
The core idea behind dependency injection is very simple, yet allows for better maintainable, testable and decoupled code to be written.
Because it's such a powerful pattern, it's only natural that lots of tools emerge around it. I believe it's a good thing to first understand the underlying principle, before using the tools built upon it. And I hope this blog post has helped with that.
If there are any thoughts coming to your mind that you want to share, feel free to reach out to me on via Twitter or e-mail.
Also special thanks to /u/ImSuperObjective2 on Reddit and my colleague Sebastian for proof reading this post.