Swift Dependency Injection With Functions

Written by twittemb | Published 2020/08/20
Tech Story Tags: swift | dependency-injection | functional-programming | higher-order-functions | code-quality | testing | software-development | software-engineering

TLDR Dependency injection is a technique at the crossroads of well known software engineering best practices: abstraction and decoupling, single responsibility, dependency inversion. It provides flexible and testable implementations of higher order free functions. This article assumes that the merits of dependency injection are accepted. We will see how dependency injection and higher order functions fit very well together. The UsersRepository is injected with an ApiService protocol whose purpose is to fetch users from the Rest endpoints. The repository doesn’t care about where they come from, it is an implementation detail. The publisher should only be built if and when needed.via the TL;DR App

Implementing an architecture within an application can be challenging. There are rules we can follow (SOLID, Clean Architecture) and patterns to guide us (MVVM, MVP, MVI, Redux, …) but sometimes, things we thought were well established deserve a step back.
Lately, I’ve been in this situation while developing an application that relied on the use of higher order free functions.
In this article, I’ll try to guide you from the sparkle that lit this desire for higher order free functions to its implementation regarding dependency injection.

Dependency Injection

Dependency injection is a technique at the crossroads of well known software engineering best practices: abstraction and decoupling, single responsibility, dependency inversion. It provides flexible and testable implementations.
This article assumes that the merits of dependency injection are accepted. We will see how dependency injection and higher order functions fit very well together.

Implementation in a traditional way

In the rest of this article we will focus on a UsersRepository object that fetches users from a Rest API and filters them out before returning them. For the sake of the demonstration all the endpoints of this API return users but with different flavours depending on the fetched route.
To do so the UsersRepository will be injected with an ApiService protocol whose purpose is to provide a way to fetch users from the Rest endpoints:
This is a pretty common implementation. Of course, in real life implementation, we would try to make the ApiService more versatile and safe by using generic types and constraints.
As I said in the introduction, let’s give it a second thought … why an ApiService?
In fact, we just want to retrieve users, the repository doesn’t care about where they come from, it is an implementation detail. A Rest API is one way among many.
A trivial approach would be to refactor the ApiService into a more versatile DataProvider protocol.
But what about its definition?
  • the “fetch” function doesn’t really make sense anymore
  • the “route” parameter becomes irrelevant outside an API context
  • the “ApiError” type is too specific
Of course, we could find a way to pick more « any purpose » names and data structures but I invite you to look for another way.

Injecting functions

After all, the UsersRepository only relies on an AnyPublisher<[User], ApiError> to perform its work, right?
The temptation is strong to directly inject this publisher as a dependency, but we should not. Injecting it directly would mean to build this publisher pretty early in the injection process and this is something that might lead to unwanted side effects: perhaps it takes time to be built or it needs other dependencies to be resolved? The UsersRepository doesn’t know that and can make no assumption on that. The publisher should only be built if and when needed.
If we can’t inject the publisher, we can inject a function that builds it 👍, and execute it at our convenience.
Let’s give it a try then.
What have we done here:
  • To ease the reading, we’ve declared a typealias describing the function signature
  • We’ve injected the function
  • We’ve used the function as a replacement of the ApiService
  • The publisher signature has changed a bit, from AnyPublisher<[User], ApiError> to AnyPublisher<[User], Error>. We do not want to leak implementation details here because of the error type.
So far so good … but what about the Route we used to pass to the fetch function? In fact we don’t need it anymore as it was specific to the ApiService; nevertheless, we will cover this precise point later in this article.
As a bonus we can also inject the filtering function so we can change its behaviour depending on the context (the filter might differ between dev, QA or prod environment for instance).
Once again we take advantage of typealiases to pass the functions around. This becomes almost mandatory when using functions as dependencies.
While we are at it, perhaps we can push our reflexion a bit further and take a final step towards the systematic use of functions.
Why do we need a repository? Usually, the repository pattern is used to group all the CRUD operations about an entity. We can assume that a UsersRepository will have several load, update, and delete functions. Once implemented, those functions will require a certain number of dependencies.
We will end up with an initializer taking all the needed functions as parameters, whereas we might only need some of them depending on the feature we want to accomplish. This is not optimal.
Getting rid of the repository implies that every function (load, update, delete …) is self-supporting and is being injected only with the needed dependencies.
Here we go, we have a 100% autonomous function. For the sake of clarity we can wrap the whole thing inside a namespace to shorten some names:
We can also have a default value for the filterFunction parameter to make a more concise API

The positive impact on the unit tests

Let’s get back to the original UsersRepository implementation. In order to unit test the loadUsers function, we would have had to create a mocked ApiService to fulfill the initializer requirements. The mocked ApiService should be able to succeed or fail in order to test the output of the loadUsers function.
In the unfortunate case where the ApiService also needs a dependency on its own, then we would have been forced to implement another mocked dependency as well. If we apply that strategy to the whole application, we might end up with a complex hierarchy of mock objects.
When using functions as dependencies, we still need mocks of course, but they are in general very small, simple and defined right next to the unit test:
Unit tests are super easy to imagine, to write, and to read. Personally, I find this technique a very good tool to reach a high (and meaningful) code coverage. I think it can help a lot when using a TDD approach.

A trick from functional programming

Do you remember the original implementation of the UsersRepository: it used an ApiService to fetch an endpoint defined by a route. Even though this has been erased by the abstraction brought by the injection of functions, at the end we still need to provide a function that can retrieve users for real.
It is up to the dependency injection mechanism to provide a compatible concrete implementation.
We have an ApiService at our disposal but the fetch definition does not match the signature we need:
We need to perform two changes here:
  • Erase the Route parameter
  • Change the error type
To erase the Route parameter we can borrow some techniques from the functional programming like partial functions or currying.
We will go with partial functions here, even though currying could be a reasonable choice as well.
Partializing a function is like saying to the compiler « Hey, I know some parameters of this function, I can set them right now, but the rest of the parameters are still undefined, please give me back a function that will only take those parameters so I can call it later!»
Let’s take a look at an example 😏.
dumbFunction is a function that takes two parameters and returns a Bool.
We can partialize that function to be able to « freeze » the first parameter and get back in return a function that only takes the second one.
Now, instead of using the function ‘dumbFunction’ with 2 parameters, we can use its partialized counterpart with only one parameter
It is a little bit like if we had performed dependency injection of the first parameter, the partialization has « captured » it.
Of course, we cannot write a partialized version of every function in our code base. There is a way to make it generic with any numbers of parameters.
And we can write as much versions of partial functions as needed.
Do you see where I’m going here ? We went from a function with two parameters to a function with only one. Then, we can go from a function with one parameter to a function with none !
Let’s get back to the ApiService.fetch function and apply a partialization on it:
We now have a partialized function with the signature:
() -> AnyPublisher<[User], ApiError>, the route parameter has been captured by the partialization and will be used when executing partializedFetchFunction.
We’re almost there, all we need to do is hide the ApiError, fortunately Combine can help use with that:
Aaaaaand we have our dependency ! the retrieveUsersFunction has that signature:
() -> AnyPublisher<[User], Swift.Error>
We can inject it in the Users.load function 👌.

Sum up

1: use function as dependencies:
2: use partialization to build the dependencies:
3: inject the dependency
The most delicate part is the second one as it asks some plumbing and boilerplate, but it should be segregated in specific areas of your code dedicated to dependency injection, like Swinject Assemblies for instance.

Conclusion

Although injecting traditional data structures is perfectly fine and is in line with best practices, injecting functions offers two major benefits:
  • This puts under a new light how easy it is to leak the implementation details and how we can avoid it. The less we know about our dependencies, the better. I guess this illustrates perfectly the Law of Demeter
  • This makes unit tests easier to write, to read, and to reason about
Using functions « everywhere » can have some unpleasant aspects like making signatures hard to read for instance. But this is something we can easily work around by using typealiases.
Using functions « everywhere » also opens the door to functional programming. We had a taste of it with partialization but it is a whole new world that you should gently explore.
Thanks for reading. Of course, feel free to comment and stay tuned.

Bonus: Swinject and the injection of functions

Swinject is a well established dependency injection framework in the Swift community. It is used to register and resolve the « recipes » to build your dependencies. But what about registering and resolving functions ?
There is nothing more like a (String) -> String function than another (String) -> String function, right !
What happens if we need to resolve a function that has been registered several time with the same signature ?
Swinject provides a way to discriminate registered services by using a unique name. If we want to use that in our (String) -> String case, we will end up with something like this:
When it comes to resolve things:
While there is nothing wrong about that, this is kind of ugly to read. We can improve this a bit with a protocol that groups the function signature and its name:
With that in place, the Swinject assembly now becomes:
When it comes to resolve things:
Even if we do not win that much in doing this, we make the function registration/resolving look like any traditional data structure usage. It removes a pain point in injecting functions that could have prevented you from trying this technique 😏.

Written by twittemb | Mobile Architect in Montreal
Published by HackerNoon on 2020/08/20