Dependency Injection in Swift

October 14, 2019

Author: Cyril Barthelet

Dependency Injection in Swift - Credit: Photo by Joshua Aragon on Unsplash

 

When building software, we use design patterns to solve many of the common problems we encounter. In object-oriented programming, the SOLID principles are some of the most important design patterns to follow. Two of these principles are the single responsibility principle and the dependency inversion principle.

The single responsibility principle states that a module should only have responsibility over a single piece of functionality. Let’s apply this principle to a car. A car contains an engine, but that does not mean that it is responsible for how the engine works or its creation. By creating this separation, we can change the internal mechanism of the engine without worrying about breaking the overall functioning of the car.

 

Single Responsibility Principle - Car and Engine example

Dependency inversion implies that classes should be decoupled so that high level modules do not depend on low level modules. Let’s revisit the car example again. The dependency inversion principle implies that it is possible to install a different engine and have it work without needing to modify any other part of the car.

Dependency Inversion - Car and Engine example

In this article, I will explain how the dependency injection pattern can be used to implement the above two design patterns. Below is a diagram illustrating this approach:

Dependency Injection Pattern - Car and Engine Scenario

With dependency injection, an injector is used to initialise a module with any of its required dependencies in a way that hides their implementation details and separates their creation from usage. This provides multiple advantages that become more apparent when dealing with a complex codebase. Maintainability, for one, is increased as new implementations of dependencies can seamlessly replace older ones without changing any of the modules code or multiple different implementations can coexist and be injected depending on the context, such as mocks required by unit tests. The injector also provides a layer of abstraction to the initialisation of modules which therefore no longer depends on details.

Code Examples

Now let’s explore some code examples by implementing the Car and Engine models from the diagram above. Consider the code below, which does not use dependency injection:

 

    
class Engine {
    func start() {
        // start engine…
    }
}

class Car {
    let engine = Engine(
}
 

The Car class is tightly coupled with the Engine class. It is responsible for initialisation and has direct access to that class. In Swift, utilising protocols for dependency injection, the above code would now look as follows:

  
protocol Engine {
    func start()
}

class EngineImplementation: Engine {
    func start() {
        // start engine…
    }
}

protocol Car {
    var engine: Engine { get }
}

class CarImplementation {
    let engine: Engine

    init(engine: Engine) {
        self.engine = engine
    }
}
 

 

The Car and Engine classes are now completely decoupled, and the injector will decide which implementation to use at initialisation. With the decoupling, the Car no longer has access to any of Engine’s implementation details. The only thing missing from the code now is the injector. We can create one ourselves, but in this example, we will use Swinject, an excellent dependency injection framework for Swift. Swinject uses containers to link protocol and implementations, using its ‘register’ function. So, for our example, the setup code would look like this (Note: the use of forced unwrapped, which is valid, as a class will never resolve to nil if it is registered).


let container = Container()
container.register(Engine.self) { _ in
    EngineImplementation()
}
container.register(Car.self) {
    CarImplementation(engine: resolver.resolve(Engine.self)!)
}
 

Here we created a Swinject container and registered the Car and Engine pairs, with the Engine implementation injected into the Car implementation using the resolver, which is the container itself. Now, we want to create an instance of Car.


let car = container.resolve(Car.self)!
 

That’s it! Now that we have set up our dependency injection let’s dive a little deeper into Swinject, and the other additions we can make to dependency injection. A scope can be provided as a configuration option to determine how instances are shared across the system as follows:

  • Default: A new instance will be created every time a class is resolved.
  • Container: The same instance will always be returned in a singleton pattern, something which will also apply to child containers.
  • Weak: This keeps a weak reference to the instance and will return the same instance until no more strong references to it exist.

Swinject offers other useful features including the ability to do circular dependency injection when two classes depend on each other and as previously mentioned the creation of container trees.

When using dependency injection, there are downsides to be aware of, such as increased upfront effort when building an app and difficulty debugging modules due to the separation of their creation and usage.

In summary, dependency injection is a useful design pattern. It enables the ability to decouple modules in our code and adheres to two of the SOLID principles. It increases maintainability, testability, and robustness of a codebase. In addition, it is easily implementable using a framework like Swinject to manage dependencies.

 

---------

Cyril Barthelet is a Software engineer who is fascinated about mobile development and likes to deep-dive into best practices across the tech industry.