What is a Dependency?
In software engineering, there is a complex-sounding technique called dependency injection that aims to help organize code modularly by creating objects that depend on other objects. The objects that other objects depend on are called dependencies.
The use of dependency injection helps solve the problem known as 'spaghetti code.' If you haven't heard of this term, it refers to software that is 'held' together by bad design and architectural planning, in which each object is connected to one another. This makes codebases hard to maintain. To build software that lasts - thoughtful planning and execution are crucial, and dependency injection can help with the process of producing modular code.
Using Dependencies in Code
Before we dive into injecting dependencies, I will show you a basic example of how to use one. To reiterate, a dependency is an object that other objects depend on in order to operate.
In the following code, you will see a Colony
class with a queen
property, an initializer, and a formColony
method. There is also the QueenBee
class and the Bee
protocol.
class Colony {
var queen: Bee
init() {
queen = QueenBee()
}
func formColony() {
queen.startMating()
}
}
class QueenBee: Bee {
func startMating() {
print("Begin mating flight.")
}
}
protocol Bee {
func startMating()
}
When an instance of Colony
is initialized, the queen
property is assigned to an instance of the QueenBee
class. Note that this queen
property can be anything that is of the Bee
type.
The formColony
method calls the queen
object's startMating
method. As you can see, the QueenBee
class conforms to the Bee
protocol and will print "Begin mating flight." when the startMating
method is called.
The dependency in this setup is the QueenBee
object inside the Colony
initializer. Since Colony
directly references QueenBee
in the initializer, it is considered tightly coupled with the QueenBee
object. This is not good, because now Colony
depends on QueenBee
to function correctly.
The use of dependency injection will help avoid using dependencies as you have seen here.
Dependency Injection Using Swift
Within the Swift programming language, there are a few different ways to go about dependency injection - initializer injection, property injection, and the lesser-used method injection.
Initializer Injection
When using initializer injection, you pass the dependency object through to another object via its initializer. The usage of the dependency object (sometimes called a service) is defined within the object it's being passed to (sometimes called a client) - but the actual creation doesn't happen until it's passed through the client's initializer.
To modify the previous code to adapt dependency injection using the initializer injection method:
class Colony {
var queen: Bee
init(queen: Bee) {
self.queen = queen
}
func formColony() {
queen.startMating()
}
}
let firstQueen = QueenBee()
let firstColony = Colony(queen: firstQueen)
firstColony.formColony()
Now, Colony
doesn't directly reference the QueenBee
object in its initializer. Which means the tight coupling problem has been solved, and any object of the Bee
type can be used with Colony
. The above code will print "Begin mating flight."
Note that I mentioned any object that is of the Bee
type can be passed into the initializer. This is great because you can exchange the type of bee used as the colony's queen. Of course, this wouldn't happen in the real world because bee colonies must have a queen bee - but a good example is changing the type of hive the colony lives in. I've made slight modifications to the code to show this:
class Colony {
var queen: Bee
init(queen: Bee, hiveType: Hive) {
self.queen = queen
}
func formColony() {
queen.startMating()
}
}
let firstQueen = QueenBee()
let topBar = TopBarHive()
let firstColony = Colony(queen: firstQueen, hiveType: topBar)
You can now change the type of bee as well as the type of hive used to form this colony. Another way to integrate dependency injection is through the property injection method.
Property Injection
Property injection is pretty much exactly what it sounds like - you pass the dependency directly through to an object's property. Here is a modified version of the Colony
class:
class Colony {
var queen: Bee!
func formColony() {
queen.startMating()
}
}
let firstQueen = QueenBee()
let firstColony = Colony()
firstColony.queen = firstQueen
firstColony.queen.startMating()
The queen is being assigned via a property, and all methods of the queen will operate as expected when called as seen. In other words, this will again print "Begin mating flight."
Method Injection
A lesser-used way to integrate dependency injection is by using a setter method. Setter methods are custom methods of an object that use a parameter to set a certain property's value based on what was passed through the parameter. It works somewhat like initializer injection (in that it uses a parameter to give value a property), but you have to call it yourself after the object has been created.
class Colony {
var queen: Bee!
func formColony() {
queen.startMating()
}
func setQueenBee(_ queen: Bee) {
self.queen = queen
}
}
let firstQueen = QueenBee()
let firstColony = Colony()
firstColony.setQueenBee(firstQueen)
Here, the setter method is the setQueenBee
method within Colony.
When an object that conforms to the bee
protocol is passed through to that method's parameter, it will set the bee
property to the value of the parameter. This is another way of integrating dependency injection, but it's not the most convenient.
Conclusion
That's it for dependency injection!
It really isn't as scary as it sounds, and after you try it out for yourself, it will become much more instinctive. It's a simple technique that helps developers build better software by making it modular, maintainable, and scalable.
Thanks for reading! If you'd like more programming content, be sure to check out the rest of the Rusty Nail Software Blog. You can read the original post here.