Understanding Dependency Injection with Swift
Before jumping into its application, let’s cover some basic concepts and terminology.
Dependency injection is a software design pattern that emphasizes, or rather, enables loose coupling between a service and client. The service is the dependency and the client is the dependent.
Great right? But… what does that even mean?
Well, taking a step back, dependency injection is also known as Inversion of Control — which sounds complicated but really means that rather than a custom method calling functions from frameworks directly, there is a layer of abstraction between the custom method and the framework. That layer is usually some generic protocol that the service conforms to — which is essentially the key to injection. Still confused? No worries, I think an example is in order.
Suppose we have a Restaurant class and an ItalianCuisineChef class like so:
This is great, but as you can see the Restaurant class is coupled with the ItalianCuisineChef class, which in turn will reduce the reusability of this code if we decide to support multiple cuisines, say with a JapaneseCuisineChef and a FrenchCuisineChef. Using our current style of implementation we would have to create new restaurants for both cuisines, duplicating much of the code we already have.
So how can we increase the reusability of our code by using dependency injection?
As we mentioned earlier, the utilization of dependency injection hinges on the use of a generic protocol that will serve as an abstraction layer between the client and the dependency — in this case the Restaurant and Chef class, respectively.
Let’s start with a protocol that we can use:
We can use this protocol as the common link between all Chef classes. Take a look at the new chef classes.
As you can see, although the two chef classes have different implementations, they both conform to the RestaurantWorkable protocol, achieving these generic tasks like, prepareLunch() and prepareIngredients(), their own way.
But it’s this conformance to RestaurantWorkable that makes them swappable in the Restaurant class.
To achieve that versatility, we will modify the Restaurant class’s initializer (constructor) so that we are injecting the dependency — all we have to do is make the chef parameter be of type RestaurantWorkable.
So far we’ve determined that dependency injection is a useful tool for passing in swappable components to a constructor, making code more modular and reusable. But, dependency injection will also help with testing.
In the case of a unit test we want to test the public API determined by a specific class and not test how a class interacts with its components, which falls more into the realm of an integration test.
But this can be a real hairy situation when a dependency is not injected and that dependency either (and this is not an exhaustive list) alters a database or performs an asynchronous operation. To simulate this let’s build the Restaurant class a bit further by adding a payment method and a parameter for an object capable of handling credit card transactions.
So here we have the Restaurant taking in a BigBankGateway object in its initializer and setting it to the gateway property. This gateway property is then used in the sell function to process the sale. However, taking a look at the BigBankGateway class it is immediately clear that its processSale function is asynchronous and involves a network layer, which is problematic for unit testing our Restaurant class as the BigBankGateway dependencies are now bleeding into our test. In other words, our test results for Restaurant’s sell function is now dependent on something external to the class.
Ok, so how do we remedy this?
Now if you don’t already know this, we can’t use libraries like OCMock or Kiwi to create a mock object and then perform stubbing on a Swift file, otherwise that would provide us a quick solution. For this reason, dependency injection becomes a very useful tool in Unit Testing.
What dependency injection will allow us to do, much like with the Chef classes, is inject a TestGateway to the Restaurant initializer, that will just automatically return to us the data that we expect from the processSale method. So we no longer have to worry about if the gateway hits the endpoint, times out, or any threading issues. Take a look:
First we build an abstraction layer with a protocol: CreditCardGateway
Then we change the initializer for Restaurant, so that it supports injection.
Now we can build a TestGateway and pass that into the Restaurant for our Unit Test. This allows us to bypass all the intricacies associated with the actual BigBankGateway and strictly test how our Restaurant’s public API functions. Additionally, we can also support other Credit Card Gateways, aside from our BigBankGateway (say SmallIndependentBankGateway for example), without having to change how our Restaurant class functions.
Dependency injection like all tools or patterns have a time and place.
- Increases modularization of code and reusability while decreasing coupling.
- It is particularly useful when dealing with 3rd party libraries.
(Sorta, the same message as the bullet above, but I felt his deserved special emphasis)
Having an abstraction layer that we use to wrap those 3rd party libraries allow us the flexibility of picking and choosing which libraries to use without having to change how the client functions — like in the credit card gateway example above.
- And when testing with Swift dependency injection is a must if you plan on swapping out dependencies in favor of a mock dependency object.
Well that’s it in a nutshell. Thanks for reading and hopefully dependency injection isn’t still sounding like some fancy term to you, as it first did to me!