Dependency Injection - What is It, and How to Use It.

Dependency Injection - What is It, and How to Use It.

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 Colonydepends 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.