Dependency Injection with Service Locator

March 3, 2017    Swift Unit Testing Dependency Injection Blog

Here at WeltN24’s iOS team we like to test. We like it so much that we build our code to be almost 100% testable. We are able to achieve this by injecting all dependencies through each class’ initializer. One technic to do this is to make use of Swift’s default values.

The Swift Programming Language

You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

func hello(name: String = "Alex Salom") -> String {
  return "Hello \(name)"
}

hello() // Hello Alex Salom
hello(name: "Nick Cave") // Hello Nick Cave

Let’s say that we have an object called PeopleManager who’s purpose is to, well, manage people. Let’s imagine that it can fetch people from the network and save people in a database. Since we like to follow the Single Responsibility Principle, we want these two functionalities to be encapsulated in different objects, Fetcher and Database.

protocol Database {
  func save(person: Person)
}
final class DatabaseImpl: Database { ... }

protocol Fetcher {
  func fetch() -> Person
}
final class FetcherImpl: Fetcher { ... }

Using Swift’s default values we could inject those dependencies into PeopleManager like this:

final class PeopleManager {
  private let database: Database
  private let fetcher: Fetcher

  init(database: Database = DatabaseImpl(), fetcher: Fetcher = FetcherImpl()) {
    self.database = database
    self.fetcher = fetcher
  }

  func person() -> Person {
    return fetcher.fetch()
  }

  func addPerson() {
    let person = Person(name: "Alex Salom")
    database.save(person: person)
  }
}

This is very nice as it allows us to initialize PeopleManager through its empty initializer from the production code PeopleManager() but giving us the possibility to inject mocked versions of the dependencies from the tests PeopleManager(database: DatabaseMock(), fetcher: FetcherMock()).

However we did find one issue with this technic. When an object has quite a few dependencies, the initializer can get out of hands having long signatures. That’s why we came up with something called ServiceLocator. We can think of a ServiceLocator as a registry of dependencies for a given object. The idea is that each dependency will declare its own locator so other objects can find a way to initialize those dependencies. Let’s see an example with our Database.

protocol Database { ... }
final class DatabaseImpl: Database { ... }

protocol DatabaseLocator {
  func database() -> Database
}

extension DatabaseLocator {
  func database() -> Database {
    return DatabaseImpl()
  }
}

We declared a protocol DatabaseLocator with a function that will provide us with an instance of Database. We also declared a protocol extension with a default implementation of that function. Now imagine we did the same for Fetcher and we now have a FetcherLocator as well as a DatabaseLocator. With those in place let’s revisit PeopleManger’s dependency injection.

final class PeopleManager {
  typealias ServiceLocator = DatabaseLocator & FetcherLocator
  final class ServiceLocatorImpl: ServiceLocator {}

  private let database: Database
  private let fetcher: Fetcher

  init(serviceLocator: ServiceLocator = ServiceLocatorImpl()) {
    self.database = serviceLocator.database()
    self.fetcher = serviceLocator.fetcher()
  }

  func person() -> Person {
    return fetcher.fetch()
  }

  func addPerson() {
    let person = Person(name: "Nick Cave")
    database.save(person: person)
  }
}

We start by declaring a typealias ServiceLocator with all the dependencies of this class. This is very convenient because only by looking at this line we see all the objects PeopleManager depends on. We then inject the ServiceLocator into the initializer init(serviceLocator: ServiceLocator = ServiceLocatorImpl()) using a default value of ServiceLocatorImpl. ServiceLocatorImpl doesn’t need to provide any implementation because each one of the Locators have provided an extension to every method they declare. We just found a way to inject as many dependencies as we want by declaring only one variable at PeopleManager’s initializer. Since all the dependencies use protocols and the locators are protocols themselves we could now build our own version of ServiceLocator that returns mocked objects and inject those from the tests.

Schönes Wochenende!

Get the code here.