Dependency Injection sounds complex, but it’s actually a surprisingly simple concept. In this tutorial, you’ll learn how dependency injection works. Understanding dependency injection (DI) will greatly improve your code quality and productivity as a Swift developer.
Many software design principles, like Don’t Repeat Yourself and SOLID, have one thing in common: they make your code more maintainable by making it modular. Instead of creating one big pile of code, you create separate your code into modules that interact with each other. Dependency injection also helps increase the modularity of your code.
Heard of the term “spaghetti code” before? It’s a codebase where every component is connected with every other component. It’s chaotic, hard to maintain, and even harder to debug. You want to avoid spaghetti code at all costs, and dependency injection helps with that.
Here’s what we’ll discuss:
Ready? Let’s find out how!
We’re going to start with finding out what a dependency really is, and how that factors into dependency injection.
You’ll soon realize that the term “dependency injection” is, in fact, a perfect word for this development concept. You’re literally injecting a dependency with code.
What’s a dependency? A dependency is an object that other code depends upon to function. Let’s look at this code example.
protocol Propulsion {
func move()
}
class Vehicle
{
var engine: Propulsion
init() {
engine = RaceCarEngine()
}
func forward() {
engine.move()
}
}
Imagine the following scenario:
You see two things in the code:
The protocol Propulsion
defines one function: move()
. When a class wants to conform to the protocol, it will need to implement that move()
function.
The class has one property engine
, of protocol type Propulsion
. You can assign any type of object to engine
as long as it conforms to the Propulsion
protocol. This is called protocols as a type.
When an instance of Vehicle
is initialized, with the init()
function, an instance of RaceCarEngine
is assigned to the property engine
. Then, when the function forward()
is called, the vehicle will call the move()
function on the engine
object.
The protocol Propulsion
defines the rules for the engine, and within the Vehicle
functions the engine is initialized and used to call the move()
function.
Here’s the actual car engine:
class RaceCarEngine: Propulsion {
func move() {
print("Vrrrooooommm!!")
}
}
The RaceCarEngine
class implements the Propulsion
protocol, and defines an implementation for the required move()
function.
Here’s how you use them together:
var car = Vehicle()
car.forward() // Output: Vrrrooooommm!!
You define a car
variable of type Vehicle
, and call the forward()
function to move the car with the engine.
What is the dependency here?
The dependency is the RaceCarEngine
class, inside the init()
function of Vehicle
. The Vehicle
class is tightly coupled with the RaceCarEngine
class, because the Vehicle
class directly references the RaceCarEngine
class in its initializer.
You’ve directly referenced RaceCarEngine
from Vehicle
, so the Vehicle
class now depends on the RaceCarEngine
to function. This is a dependency. You want to avoid it, if you can, because right now it’s hard to use the Vehicle
class without the RaceCarEngine
class.
How can we get around that? That’s where dependency injection comes in.
There’s no real way to avoid dependencies altogether, and that’s a good thing. Dependencies aren’t bad! Your app’s code works together; it’s interconnected. Writing code on top of other code – abstraction – is what makes code work.
It’s easy to create tightly coupled code with bad dependency management. That’s the problem we’re trying to solve here. A typical sign of spaghetti code is non-modular, tightly coupled code and no structural approach to managing dependencies.
Let’s go back to the Vehicle
class from the previous section. How can we use dependency injection to improve the code? Check this out:
class Vehicle
{
var engine: Propulsion
init(engine: Propulsion) {
self.engine = engine
}
func forward() {
engine.move()
}
}
It’s a subtle change, but it makes all the difference. First, notice that the Vehicle
class does not mention the RaceCarEngine
at all!
Instead of hard-coding the RaceCarEngine
object into the init()
function of Vehicle
, we’ve now added a parameter to the initializer. The parameter is called engine
, its type is Propulsion
(the protocol), and it’s used to set the engine
property of Vehicle
upon initialization.
Here’s how everything works together:
let fastEngine = RaceCarEngine()
var car = Vehicle(engine: fastEngine)
car.forward() // Output: Vrrrooooommm!!
In the above code, we’re first creating an instance of RaceCarEngine
. Then, an instance of Vehicle
is initialized.
See the engine
parameter? The fastEngine
object is injected into the Vehicle
object from the outside. This is dependency injection! Both classes still depend on each other, but they’re not tightly coupled anymore. They’re modular; you can use one without the other.
Let’s look at some more code:
class RocketEngine: Propulsion {
func move() {
print("3-2-1... IGNITION!... PPPPSSSSCHHHHOOOOOOOOOMMMMMM!!!")
}
}
Yeah, it’s a rocket engine alright! Just like RaceCarEngine
, the RocketEngine
class conforms to the Propulsion
protocol.
You can switch out the race car engine in your car for a rocket engine, with zero effort. Check this out:
let rocket = RocketEngine()
var car = Vehicle(engine: rocket)
car.forward() // 3-2-1... IGNITION!... PPPPSSSSCHHHHOOOOOOOOOMMMMMM!!!
In the above code, we’ve created an instance of RocketEngine
. When a Vehicle
object is initialized, we’re injecting the rocket
object by using the initializer’s engine
parameter.
Just like before, we’re creating a dependency and inject that into the component that relies upon it. Instead of tightly coupling the components, hard-coding them into the class, we introduce modularity with dependency injection.
A key component to make dependency injection work, of course, is the protocol as a type. Both engines conform to that protocol, which is why we can switch ’em. Do you always need a protocol for this? No. You can use one of Swift’s many other powerful features, such as subclassing, generics and opaque types.
Don’t get this the wrong way, though! You could have switched out the engine without using dependency injection, in our initial example. You simply could have changed the Vehicle
class, right?
3 reasons why that might not work:
Vehicle
class code, because someone else wrote it – or it’s a framework whose code you cannot changeLet’s discuss these 3 use cases in the next section…
Do you really need the protocol, to make DI work? It’s a point of discussion. Depending on your code, you could allow the injection of a concrete object that’s configurable. You could even substitute a dependency with a subclass!
Dependency injection is useful in the following scenarios:
Let’s get to it.
You’re working with code you don’t have access to all the time! Think about iOS frameworks or a 3rd-party library. Even though, strictly speaking, you have “access” to the code – you can’t change it without breaking it!
The Cocoa Touch SDK has a neat solution for that: delegation. Delegation is a programming design pattern that enables a class to hand-off some of its responsibilities to another class instance.
In many Cocoa Touch classes you can assign your own object to protocolized delegate
properties. The framework classes then call functions on your delegate objects. Since you have control over the delegate objects, you can change the behavior of framework classes without changing their code directly.
Dependency injection is a similar technique, in the sense that it allows you to customize the implementation of code that would otherwise be unreachable.
Consider the following scenario:
WebAPI
class, that you depend on.WebAPI
must implement a function getItems()
, so you come up with a protocol called API
that includes this functionWebAPI
, you create your own stub API called FakeAPI
. Its API returns a simple list of items.API
. You don’t care how it works, as long as it has that getItems()
function!WebAPI
is ready, you switch the classes and inject the right object into your code. You only have to change 1-2 lines, tops.When the time comes to replace the API with a brand new component, somewhere in the future, you only have to code the new API implementation. If it conforms to the protocol, and exposes the same getItems()
function, it should work perfectly fine. Yay, modularity!
Modularity and composability, i.e. separate things that you can combine, lie at the heart of the Unix philosophy. You find the same design principles in Swift and iOS.
With dependency injection you can easily switch out dependencies. In the previous example, the one with the car engine, you saw that you could change the engine implementation of the car.
Say that you were really building a rocket engine. You’ve got two types of engines:
The diagnostics rocket engine sends logging data to an external service. When it starts up, it’ll send a I'm starting up
message to the logging service. The rocket engine also does more calculations to generate diagnostics data, so it’s more resource intensive.
With dependency injection you can switch out the actual rocket engine, and the diagnostics engine without changing any core rocket code. This is similar to switching one component for a stub component, like we’ve discussed before.
Stubbing and mocking is similar, but different. A stub is a function (or a class) that has the correct signature, but it’s (almost) empty – it doesn’t do anything. A mock is a function (or class) that has a fake implementation, that’s typically much simpler than its real-world counterpart.
A common scenario for mocking is generating diagnostics data. For example, you’ve got a Log
class that normally gathers logging data in a text file when you’re developing your app. In a production environment, you don’t want to log to a text file – or log anything at all – so you replace the Log
component with a mock substitute.
Unit testing is a software development practice that tests the output of code, based on a given input, and compares it against predefined value. When the output matches the predefined value, the test is successful. You know that the implementation, the unit you’re testing, matches expectations.
Compare it to a simple pocket calculator. You test whether the addition function works correctly by calculating 1 + 1
. When the result is 2
, you know it works, and when the result is something else, you know the function is broken.
This is useful if you’re going to change the implementation of some code. You first design the test and run it against your current code, to ensure that the test is correct. You then change the code that’s tested, called Unit Under Test, and run the test again.
If the test succeeds, you know that the new code’s output is OK. Other code that depends on the output of the tested code should be unaffected by the new changes, and continue to run OK.
Unit testing is typically labor intensive, so you want to automate as much as possible. It’s easier to test modular code that isn’t tightly coupled. With dependency injection you can quickly inject a testing class as a dependency without having to replace entire blocks of code.
You can test the dependency itself, i.e. if the rocket engine runs smoothly, or the code that uses the dependency, i.e. if the rocket moves based on input from the rocket engine.
With unit testing you typically replace components in the code with mocks or stubs. Imagine you want to test a WebAPI
. Instead of downloading items from the internet, the networking requests code is replaced with a simpler alternatives that reads the same data from a local file. You can then test if that data ends up OK in a view controller, for example.
You can inject dependencies in Swift with 2 approaches:
init()
You can use either, but in a few use cases, one is better than the other.
nil
at initialization, it’s better to use property injectionBefore, we’ve used initializer injection to provide the RocketEngine
object to an instance of the Vehicle
class. Like this:
let car = Vehicle(engine: rocketEngine)
In the above code we’re using the initializer function Vehicle(engine:)
to provide a dependency to the Vehicle
instance. That’s why it’s called initializer injection, of course!
This approach is sometimes called constructor injection, because in other programming languages initializers are sometimes called constructors. Good to know!
Property injection isn’t much different. Instead of injecting the dependency as an initializer argument, you directly assign a value to the property:
var car = Vehicle()
car.engine = rocketEngine
In the above code, we’re passing the rocketEngine
object to the Vehicle
instance by using a stored property.
You’re not required to use one or the other, but you can see how initializer injection suggests that a dependency is used throughout the lifetime of the dependant object. It’s injected right there at the inception of the object.
You can also use a setter function to inject a dependency. This is called method injection. Strictly speaking, functions that belong to a class are called methods.
car.setEngine(rocketEngine)
Method injection is much less common in Swift than property or initializer injection. Setter functions, like the one above, aren’t commonly used. Instead, you typically use a property to assign an object directly.
It’s good to know method injection is an alternative approach, though. You could use it to provide a dependency for the lifetime of a function, for example.
Understanding dependency injection is a great step towards creating more modular, more maintainable and less error-prone code. In this tutorial, we’ve discussed how you can use dependency injection in your project.
Here’s what we focused on:
Want to learn more? Check out these resources: