I am experimenting with the ServiceLocator pattern. I'd like to support lazy loading of items.

protocol Resolver {
func resolve<T> () -> T
}

protocol Register {
func register<T>(instance: T)
func register<T>(reference: @escaping () -> T)
}

final class LazyServiceLocator {

enum ObjectRegistry {
case instance(Any)
case reference(() -> Any)

func unwrap() -> Any {
switch self {
case let .instance(instance): return instance
case let .reference(reference): return reference()
}
}
}

private lazy var store: Dictionary<ObjectIdentifier, ObjectRegistry> = [:]
}

extension LazyServiceLocator: Resolver {
func resolve<T>() -> T {
let key = ObjectIdentifier(type(of: T.self))
if let item = store[key], let instance = item.unwrap() as? T {
switch item {
case .reference: register(instance: instance)
default: break
}
return instance
} else {
preconditionFailure("Could not resolve service for \(type(of: T.self))")
}
}
}

extension LazyServiceLocator: Register {

func register<T>(instance: T) {
let key = ObjectIdentifier(type(of: T.self))
store[key] = .instance(instance)
}

func register<T>(reference: @escaping () -> T) {
let key = ObjectIdentifier(type(of: T.self))
store[key] = .reference(reference)
}
}


This can run with the following in a playground


let foo = LazyServiceLocator()

class Test {
init() {
print("Test")
}
}

class OtherTest {
init(foo: Test) {
print("OtherTest")
}
}

foo.register(instance: Test())
foo.register(reference: {
return OtherTest(foo: foo.resolve())
})

let bar: Test = foo.resolve()
let boo: OtherTest = foo.resolve()



Commenting out let bar: Test = foo.resolve() still runs the print statement in it's init method, so I can see that works.

Commenting out let boo: OtherTest = foo.resolve() does not run the print statement, so I believe this also works.

I am very new to the pattern and Swift, so would appreciate some feedback on where this can be improved or what I have missed, being new to the language.

This is mostly good, but it can be improved.

You might want to constrain the Resolver and Register protocols to classes. Since you want to share instances this won’t work with structs.

protocol Register: class { ... }


Your store property doesn’t have to be lazy. Initializing an empty dictionary is cheap, so you can do that directly when your LazyServiceLocator is initialized.

The biggest issue I found is the ObjectIdentifier you use as the dictionary key. You don’t store the type of T (which would be T.self) but the type of the type of T - it’s metatype. I’m not quite sure if this can lead to problems, but it is unnecessary. Just use ObjectIdentifier(T.self).

The other thing you might want to consider is renaming your register(reference:) method to register(factory:). Calling something that creates an object reference is rather uncommon.

And finally you might want to consider adding an overload for resolve that takes the requested type as a parameter:

extension Resolver {
func resolve<T>(_ type: T.Type) -> T {
return resolve()
}
}


With that you can write let x = resolve(Foo.self) instead of let x: Foo = resolve(). IMHO this is a bit more readable.

• Thank you so much, very helpful. Jun 21, 2020 at 10:43