A Swifty ServiceLocator implementation


Next week we’ll start a new project at WeltN24, and we spent last week setting up the infrastructure (Git, Fastlane, Jenkins) and the overall architecture (mostly a VIPER adaptation).

One of the topics that came up was how to do dependency injection. I won’t explain what dependency injection is in general, for that I redirect you to objc.io as a great source of information.

Where we were coming from

For the Welt Edition app we used the simplest form of DI: we injected the dependencies in the init func of every class. Since we wrote the app in Swift, we could at least use default values for the arguments, that just happened to be the default implementation of every protocol we injected into the objects.

This worked well for us because we didn’t create a complex container-like infrastructure, we could opt-in or out of this behavior when we liked, and we could of course inject fake dependencies for our tests, or custom dependencies just for some of our objects.

The biggest downside was that we needed to write something like this:

init(myDependency: MyDependencyInterface = MyDependencyImpl(), ...) {
   self.myDependency = myDependency
   ...
}

for every object, thus:

  • basically writing 3 times the same thing, for each dependency (argument name, interface name, implementation default value)
  • repeating the same mantra everywhere, so that if we wanted to change the default implementation of a given interface this would result very difficult
  • having huge initializers, for complex objects with up to 10 dependencies
  • not having a clear overview of what dependencies were needed by which object

Our new approach

For the new project we decided to move a little away from the poor man’s dependency injection approach and get closer to the Service Locator pattern.

A Service Locator is an object whose responsibility is to provide dependencies to other objects. We implemented our own infrastructure around this idea that leverages Swift’s protocol oriented programming.

The main concept is that we define a protocol for every dependency that can be injected and a protocol for every specific service locator that will inject dependencies into another object. Then, we extensively use protocol oriented programming in order to gain several benefits that I will list in a minute.

A code example

A contrived but complete example would look like the following:

// ArticleRepositoryInterface.swift

// This is a dependency we want to inject into one or more objects later
protocol ArticleRepositoryInterface {
  // Just for the sake of simplicity
  func articlesList() -> [Article]
}

// Contextually with the definition of a dependency interface we also define a protocol that will locate objects conforming to this interface
protocol ArticleRepositoryLocator {
  func articleRepository() -> ArticleRepositoryInterface
}
// ArticleRepository.swift

// Now we define an implementation of the ArticleRepositoryInterface protocol. 
class ArticleRepository: ArticleRepositoryInterface {
  func articlesList() -> [Article] {
    // implementation goes here...
    return []
  }
}

// In 95% of the cases, we'll always use this implementation when locating ArticlePersistenceInterfaces in the main app. For the tests, we'll provide a different implementation anyway.
// So, contextually with the definition of the main class conforming to the protocol, we also extend the locator to provide this implementation
extension ArticleRepositoryLocator {
  func articleRepository() -> ArticleRepositoryInterface {
    return ArticleRepository()
  }
}

Now we get to the usage of this dependency:

// ArticleInteractor.swift

// Since in this class we'll use other objects, we define a typealias at the beginning of the file that will have 2 functions: 
// 1) It will provide an interface for service locators that want to locate dependencies for this object, type-safe, not more and not less
// 2) It will show in a readable way what dependencies this object needs

typealias ArticleInteractorServiceLocatorInterface = protocol<ArticleRepositoryLocator>

// Then we define a dummy class implementing this protocol
// Please note that the implementation is empty since we want to use the one we get for free through the protocol extensions
class ArticleInteractorServiceLocator: ArticleInteractorServiceLocatorInterface {} 

// Then we define the client code

class ArticleInteractor {
  // dependencies
  let repository: ArticleRepositoryInterface // Always code against interfaces!
  
  init(serviceLocator: ArticleInteractorServiceLocatorInterface = ArticleInteractorServiceLocator()) {
    self.repository = serviceLocator.articleRepository() // We don't know which implementation we'll get, we just care about the API
  }
}

Let’s try to understand a bit more what do we get with this approach:

  • We clearly define the dependencies of every object, at the beginning of the file, with a readable one-liner
  • We don’t pollute the init of every object using dependencies, and we don’t have to change it every time we add a new dependency
  • We have a type-safe service locator that can’t be abused by the client by calling methods that locate dependencies the object won’t use
  • We specify in a single place in the app which dependency we want to use as default, as opposed to the original approach where we specify it in every init
  • We are forced to code against interfaces rather than against implementations (that’s a good thing!)

Let’s now dig a bit deeper, and let’s see how we can add another dependency to the ArticleInteractor:

// ReachabilityInterface.swift

protocol ReachabilityInterface {
  func isConnected() -> Bool
}

protocol ReachabilityLocator {
  func reachability() -> ReachabilityInterface
}
// Reachability.swift

class Reachability: ReachabilityInterface {
  func isConnected() -> Bool {
    return true
  }
}

extension ReachabilityInterfaceLocator {
  func reachability() -> ReachabilityInterface {
    return Reachability()
  }
}

As you can see, leaving out the comments I added in the first example, the “boilerplate” is not so much. The ArticleInteractor code now becomes:

// ArticleInteractor.swift

typealias ArticleInteractorServiceLocatorInterface = protocol<ArticleRepositoryLocator, ReachabilityLocator> // We declare the new dependency here!

class ArticleInteractorServiceLocator: ArticleInteractorServiceLocatorInterface {} // No change in this implementation!

class ArticleInteractor {
  let repository: ArticleRepositoryInterface
  let reachability: ReachabilityInterface
  
  init(serviceLocator: ArticleInteractorServiceLocatorInterface = ArticleInteractorServiceLocator()) {
    self.repository = serviceLocator.articleRepository() 
    self.reachability = serviceLocator.reachability() // The init is left unchanged!
  }
}

So adding new dependencies is a walk in the park. Let’s see now what happens if we have a second implementation of an interface, and we want to provide this to a specific client instead of the default one:

//OfflineSimulator.swift

class OfflineSimulator: ReachabilityInterface {
  func isConnected() -> Bool {
    return false 
  }
}

// We can't provide a second default implementation for the ReachabilityInterfaceLocator, which is good because this would be a developer error that is now catched by the compiler. Also, we don't want this implementation to be the default ;)

Let’s go back to our beloved ArticleInteractor

// ArticleInteractor.swift

typealias ArticleInteractorServiceLocatorInterface = protocol<ArticleRepositoryLocator, ReachabilityLocator>

class ArticleInteractorServiceLocator: ArticleInteractorServiceLocatorInterface {
  // We can just override the default implementation in this specific service locator!
  func reachability() -> ReachabilityInterface {
    return OfflineSimulator()
  }
}

class ArticleInteractor {
  let repository: ArticleRepositoryInterface
  let reachability: ReachabilityInterface
  
  init(serviceLocator: ArticleInteractorServiceLocatorInterface = ArticleInteractorServiceLocator()) {
    self.repository = serviceLocator.articleRepository() 
    self.reachability = serviceLocator.reachability() // No change in the client!
  }
}

Testing

When it comes to testing our code, we want to inject fakes in our subjects under test, so that we don’t mistakenly end up testing the dependencies together with them.

Let’s see how we can do that:

// ArticleInteractorTests.swift

// As in the app case, we define our own implementation of the ArticleInteractorServiceLocatorInterface
class ArticleInteractorServiceLocatorTestImpl: ArticleInteractorServiceLocatorInterface {
  lazy var fakeReachability = FakeReachability()
  lazy var fakeArticleRepository = FakeArticleRepository()

  func articleRepository() -> ArticleRepositoryInterface {
    return fakeArticleRepository
  }

  func reachability() -> ReachabilityInterface {
    return fakeReachability
  }
}

// We use Quick & Nimble, but it doesn't matter except for the syntax
class ArticleInteractorTests: QuickSpec {
  override func spec() {
    describe("An article interactor") {
      var sut: ArticleInteractor!
      var serviceLocator: ArticleInteractorServiceLocatorTestImpl!
      
      beforeEach {
        serviceLocator = ArticleInteractorServiceLocatorTestImpl()
        
        // We now use our own service locator
        sut = ArticleInteractor(serviceLocator: serviceLocator)
      }
      
      context("when offline") {
        beforeEach {
        	// We can operate on our fakes
        	serviceLocator.fakeReachability.isOffline = true
        }
        
        it("should do something") {
          //test something
          //...
        }
      }
    }
  }
}

Conclusions

As we’ve seen, there are many benefits to this approach and as far as we’ve seen until today, the only downside could be the verbosity of the approach. IMHO the benefits are worth the verbosity, especially the fact that one always codes against protocols and that all the dependencies an object needs are defined in a single place (in a single line!).

We don’t see yet the benefits of a full-fledged container approach, like the ones used in Typhoon (Objective-C) or Dip (Swift).

If you have questions about this approach or would like to say something against it in favour of proper DI or Container-Based infrastructures, please leave a comment or mention me on Twitter!

I hope this article will be useful for you who are still unsure about if and how to introduce a DI-approach into your codebase!

For your reading convenience, I published all the code included in this article on Github as a gist.