23 Mar, 2019

Dependency injection pattern in Swift

Dependency injection pattern in Swift

What it is for.

Decoupling Clients from their dependencies and separating the creation and configuration (if any) of said dependencies from the Client’s behavior.

Dependency injection is a technique that quite literally applies the “Dependency Inversion Principle” (DIP) that states:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

How it goes.

Its initial requirement is the definition of Protocols (Interfaces) which describe the expected behavior of the dependencies without specifying their concrete implementation.

At this point we are only describing the abstract behavior we expect out of this type of dependency.

In the example above we create a Protocol for our dependency with one method sayHelloWorld(). We haven’t committed to specifying how this will be implemented and this flexibility is one of the most important features of Dependency Injection.

Next, we can define a specific implementation.

This particular implementation prints “Hello world!” to the console

The HelloWorldConsole class implements HelloWorldProtocol to define a concrete behavior for sayHelloWorld().

Finally we can use this dependency in a Client class (in this case a UIViewController ) where we will inject our HelloWorldProtocol dependency.

There are 2 very important things to note here:

  • The instance property helloWorldService is defined to be of the abstract type HelloWorldProtocol and not the concrete type HelloWorldConsole.

This means that from this point on, both Dependency HelloWorldConsole and Client HelloWorldViewController are decoupled from each other and can be updated independently. Making changes to HelloWorldConsole doesn’t require us to touch HelloWorldViewController and vice versa.

  • This HelloWorldViewController does not have any code to create or configure the helloWorldService. It is instead delegating this responsibility to the caller of its constructor (the Injector).

The Injector code is now responsible for the creation of Client and Dependency. This powerful abstraction gives us the flexibility to easily change the concrete type of dependency.

For example, if we want to use another type of HelloWorldProtocol type service, we could do the following:

A Hello World service that uses iOS’s text-to-speech to say “Hello World!”

And then we would modify the Injector code to use this other type of HelloWorldProtocol implementation.

Additionally, writing a unit test for the HelloWorlViewController implementation becomes easy because we can create and inject a HelloWorldProtocol dependency specific for testing (a Mock HelloWorldProtocol).


So what’s the catch?

This type of Dependency Injection does has a few drawbacks that should be considered.

Checking the state of the dependencies

If you were paying attention to the example above, you might have noticed that the property helloWorldService in HelloWorldViewController is declared as implicitly unwrapped.

The Injector code uses then the convenience initializer HelloWorldViewController(helloWorldService:) This is called Constructor injection.

But what would happen if we initialized HelloWorldViewController using a constructor different than HelloWorldViewController(helloWorldService:) ?

Crash!

Our Application would crash because we haven’t checked the state of our dependencies. Which brings us to the first disadvantage of Dependency Injection. Because the creation of the dependencies is delegated outside the Client, it is hard to guarantee that these have been indeed properly setup.

Depending on the situation, this crash might be desirable as a way to signal to the developer so they correct the situation. You might even actually use throw or assert to show a meaningful error.

Line 9 is dedicated to checking the state of the HelloWorldService dependency

On the other hand, this type of boilerplate code can be tedious and decrease legibility. In this very simple example we only have one dependency called once in our code, but the situation can get ugly if we have multiple dependencies that are called at different points.

Furthermore, if we have just a few dependencies, Constructor Injection can make our initializers quite long and cumbersome to write.

Readers of Robert C. Martin’s Clean Code might remember the very strong recommendation of keeping function argument lists short:

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). The arguments (triadic) should be avoided where possible. More than three requires very special justification-and then shouldn’t be used anyway.

But you might say Swift permits us to declare the dependency as an Optional to avoid such a crashes, and you would be right.

When declared this way, the Injector code can use a different initializer than HelloWorldViewController(helloWorldService:) ~ as it is often the case with UIViewController~ and configure the dependency via a setter (Setter Injection). The dependency can be called even if not properly set and nothing bad will happen. Right?

The issue here is that in this approach the missing dependencies will go unnoticed. This might be appropriate in some cases, but most likely we will want to be sure that we consume all the dependencies that we have injected into a Client.

Furthermore, in most instances you will probably still use guard let or some other boilerplate code to unwrap your now Optional dependency. Not ideal.

Verbosity and Over-engineering

As we saw before, checking for the state of dependencies can have the side effect of causing you to write some amount of boilerplate code.

Additionally you might have noticed that to create this very simple “Hello World” example we had to create a few extra protocols and classes that we wouldn’t have to write if we had taken the alternative approach.

In a real world Application with many features, the number of protocols and classes will likely be bigger. It is possible to over-engineer your dependency injection to a point where the code becomes highly fragmented, hard to read and difficult to update. You might end up with a large number of small little classes that do not much at all, all over the place.

Loss of IDE functionality

Last but not least, the fact that Dependency Injection has a strong focus on working with abstract functionality and not concrete implementation, makes it hard for Xcode to infer which specific implementation a developer might be referring to.


Sources / Further reading