Implementing the Service Locator Pattern using a Property Wrapper in Swift 5.1

The Services Problem

In every app there is typically a few instances of types (what we will now refer to as a service) that you want to access throughout the app, such as an APIClient or NSPersistentContainer.

If you were developing a small app, you might decide to pass these services around as you navigate through the app.

Either through initialisers:

let detailViewController = DetailViewController(apiClient: apiClient, persistentContainer: NSPersistentContainer)

Or lazily using properties (especially when dealing with UIViewController subclasses):

let detailViewController = DetailViewController()
detailViewController.apiClient = apiClient
persistentContainer.persistentContainer = persistentContainer

While this works fine, it gets a bit cumbersome when you need to add a new service, such as analytics, as it means you need to add it everywhere throughout your app. Moreover intermediate objects end up referencing services they don’t even care about, just because another object further on in the app might take use of it.

Another approach will be to make these types singletons. The major problem with singletons is testability, as you have to reference a concrete type and not a protocol, which makes it impossible to swap them out for mocking during tests.

How I solved this previously was to pass around a container with all of my services, note that I am referring to my APIClient by the API protocol that it implements:

struct Container {
    var apiClient: API
    var persistentContainer: NSPersistentContainer
}

I then pass this around my app, preferably through initializers but sometimes as a property:

let detailViewController = DetailViewController(container: Container)

This avoids the problem when adding a new services and also reduces amount of boiler plate code, but its still not super elegant.

Property Wrappers

When property wrappers were announced, I thought would it be nice if I could just refer to my service using one, such as:

class DetailViewController: UIViewController {
    @Service var apiClient: API
    @Service var persistentContainer: NSPersistentContainer
}

But how do we get there?

Service Locator Pattern

I have looked at a lot of dependency injection implementations in Swift and in other languages, but often found them to be over-engineered, but I concluded at this point I was essentially looking to implement the service locator pattern.

From Wikipedia

The service locator pattern is a design pattern or anti-pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the “service locator”, which on request returns the information necessary to perform a certain task.

API Design

I am going to keep the API design discussion limited to the interface, but you can grab the implementation at the end of the article.

We are going to start off with a ServiceRegistry to register our services with:

var registry: ServiceRegistry = ServiceRegistry()

We can register a service using an instance of a given type:

registry.register(apiClient)

So the service locator can work well with testing and mocking, we also want to be able register a service but refer to it via a protocol that it implements:

registry.register(apiClient as: API.self)

And you can register a service but resolve it later:

registry.register {
    return APIClient()
}

And you can also support referring to the lazily created service by a protocol that it implements, by specifying the protocol as the return type:

registry.register { () -> API in
    return APIClient()
}

This means we end up with a public API for the Service Registry of:

public struct ServiceRegistry {
    public mutating func register<Service>(_ service: Service)
    public mutating func register<Service>(_ service: Service, as serviceType: Service.Type)
    public mutating func register<Service>(_ block: @escaping (() -> Service))
}

Once we have registered our services with our registry we then want to create our service locator:

let locator = ServiceLocator(registry: registry)

We then want the locator (via generics) to return the service back:

var apiClient: API = locator.make()
var persistentContainer: NSPersistentContainer = locator.make()

This means we end up with a public API for the Service Locator of:

public struct ServiceLocator {    
    public let registry: ServiceRegistry
    public init(registry: ServiceRegistry)
    public func make<Service>(_ serviceType: Service.Type) throws -> Service
}

Now we have this setup we want to make our property wrapper.

The logical first step in creating a property wrapper would be to pass in a ServiceLocator:

class DetailViewController: UIViewController {
    @Service(locator: serviceLocator) var apiClient: API
    @Service(locator: serviceLocator) var persistentContainer: NSPersistentContainer
}

This isn’t very DRY, and also how do we get the service locator there?

To clean this up we are going to add the concept of a shared Service Locator, which we can set on app launch:

ServiceLocator.shared = locator

This will give us global access to our services

We will then use a property wrapper to make this nice an clean.

This means to access a service we can no just use @Service in front of a variable and let are property wrapper and service locator do the work:

class DetailViewController: UIViewController {
    @Service var apiClient: API
    @Service var persistentContainer: NSPersistentContainer
}

This means we end up with a public API for the Property Wrapper of:

@propertyWrapper
public struct Service<ServiceType> {
    public let locator: ServiceLocator

    public var wrappedValue: ServiceType {
        return try! locator.make(ServiceType.self)
    }

    public init(locator: ServiceLocator) {
        self.locator = locator
    }

    public init() {
        self.locator = ServiceLocator.shared
    }
}

Implementation

The implementation is less than 100 lines of code. The only extra implementation detail is my internal ServiceFactory to handle all of resolution approaches. An improvement would be to add a cache to the service locator, so that services only had to be resolved once, but that would of made this example overly complex.

public struct ServiceRegistry {
    public enum Error: Swift.Error {
        case notRegistered(Any.Type)
    }

    private struct ServiceFactory<Service> {
        let block: (() -> Service)

        init(block: @escaping (() -> Service)) {
            self.block = block
        }

        func make() -> Service {
            return block()
        }
    }

    private var factories: [Any] = []

    public init() {

    }

    public init(registry: ServiceRegistry) {
        self.factories = registry.factories
    }

    public mutating func register<Service>(_ service: Service) {
        register(service, as: type(of: service))
    }

    public mutating func register<Service>(_ service: Service, as serviceType: Service.Type) {
        let factory = ServiceFactory { () -> Service in
            return service
        }

        factories.append(factory)
    }

    public mutating func register<Service>(_ block: @escaping (() -> Service)) {
        let factory = ServiceFactory(block: block)
        factories.append(factory)
    }

    internal func make<Service>(_ serviceType: Service.Type) throws -> Service {
        if let factory = factories.first(where: {($0 is ServiceFactory<Service>)}) {
            let service = (factory as! ServiceFactory<Service>).make()
            return service
        }

        throw Error.notRegistered(Service.self)
    }
}

public struct ServiceLocator {
    public static var shared: ServiceLocator = ServiceLocator()

    public let registry: ServiceRegistry

    public init() {
        let registry = ServiceRegistry()
        self.init(registry: registry)
    }

    public init(registry: ServiceRegistry) {
        self.registry = registry
    }

    public func make<Service>(_ serviceType: Service.Type) throws -> Service {
        let service = try registry.make(Service.self)
        return service
    }
}

@propertyWrapper
public struct Service<ServiceType> {
    public let locator: ServiceLocator

    public var wrappedValue: ServiceType {
        return try! locator.make(ServiceType.self)
    }

    public init(locator: ServiceLocator) {
        self.locator = locator
    }

    public init() {
        self.locator = ServiceLocator.shared
    }
}