Search code examples
iosswiftdesign-patternsswift2service-locator

Service Locator pattern in Swift


I'm interested in a flexible universal Service Locator design pattern implementation in Swift.

A naive approach may be as follows:

// Services declaration

protocol S1 {
    func f1() -> String
}

protocol S2 {
    func f2() -> String
}

// Service Locator declaration
// Type-safe and completely rigid.

protocol ServiceLocator {
    var s1: S1? { get }
    var s2: S2? { get }
}

final class NaiveServiceLocator: ServiceLocator {
    var s1: S1?
    var s2: S2?
}

// Services imlementation

class S1Impl: S1 {
    func f1() -> String {
        return "S1 OK"
    }
}

class S2Impl: S2 {
    func f2() -> String {
        return "S2 OK"
    }
}

// Service Locator initialization

let sl: ServiceLocator = {
    let sl = NaiveServiceLocator()
    sl.s1 = S1Impl()
    sl.s2 = S2Impl()
    return sl
}()

// Test run

print(sl.s1?.f1() ?? "S1 NOT FOUND") // S1 OK
print(sl.s2?.f2() ?? "S2 NOT FOUND") // S2 OK

But it would be much better if the Service Locator will be able to handle any type of service without changing its code. How this can be achieved in Swift?

Note: the Service Locator is a pretty controversial design pattern (even called an anti-pattern sometimes), but please let's avoid this topic here.


Solution

  • Actually, we can exploit Swift's type inference abilities to get a flexible universal and type-safe Service Locator. Here is the basic implementation (gist):

    protocol ServiceLocator {
        func getService<T>() -> T?
    }
    
    final class BasicServiceLocator: ServiceLocator {
    
        // Service registry
        private lazy var reg: Dictionary<String, Any> = [:]
    
        private func typeName(some: Any) -> String {
            return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)"
        }
    
        func addService<T>(service: T) {
            let key = typeName(T)
            reg[key] = service
            //print("Service added: \(key) / \(typeName(service))")
        }
    
        func getService<T>() -> T? {
            let key = typeName(T)
            return reg[key] as? T
        }
    
    }
    

    It then can be used as follows:

    // Services declaration
    
    protocol S1 {
        func f1() -> String
    }
    
    protocol S2 {
        func f2() -> String
    }
    
    // Services imlementation
    
    class S1Impl: S1 {
        func f1() -> String {
            return "S1 OK"
        }
    }
    
    class S2Impl: S2 {
        func f2() -> String {
            return "S2 OK"
        }
    }
    
    // Service Locator initialization
    
    let sl: ServiceLocator = {
        let sl = BasicServiceLocator()
        sl.addService(S1Impl() as S1)
        sl.addService(S2Impl() as S2)
        return sl
    }()
    
    // Test run
    
    let s1: S1? = sl.getService()
    let s2: S2? = sl.getService()
    
    print(s1?.f1() ?? "S1 NOT FOUND") // S1 OK
    print(s2?.f2() ?? "S2 NOT FOUND") // S2 OK
    

    This is already a usable implementation, but it would be also useful to allow lazy services initialization. Going one step further we'll have this (gist):

    protocol ServiceLocator {
        func getService<T>() -> T?
    }
    
    final class LazyServiceLocator: ServiceLocator {
    
        /// Registry record
        enum RegistryRec {
    
            case Instance(Any)
            case Recipe(() -> Any)
    
            func unwrap() -> Any {
                switch self {
                    case .Instance(let instance):
                        return instance
                    case .Recipe(let recipe):
                        return recipe()
                }
            }
    
        }
    
        /// Service registry
        private lazy var reg: Dictionary<String, RegistryRec> = [:]
    
        private func typeName(some: Any) -> String {
            return (some is Any.Type) ? "\(some)" : "\(some.dynamicType)"
        }
    
        func addService<T>(recipe: () -> T) {
            let key = typeName(T)
            reg[key] = .Recipe(recipe)
        }
    
        func addService<T>(instance: T) {
            let key = typeName(T)
            reg[key] = .Instance(instance)
            //print("Service added: \(key) / \(typeName(instance))")
        }
    
        func getService<T>() -> T? {
            let key = typeName(T)
            var instance: T? = nil
            if let registryRec = reg[key] {
                instance = registryRec.unwrap() as? T
                // Replace the recipe with the produced instance if this is the case
                switch registryRec {
                    case .Recipe:
                        if let instance = instance {
                            addService(instance)
                        }
                    default:
                        break
                }
            }
            return instance
        }
    
    }
    

    It can be used in the following way:

    // Services declaration
    
    protocol S1 {
        func f1() -> String
    }
    
    protocol S2 {
        func f2() -> String
    }
    
    // Services imlementation
    
    class S1Impl: S1 {
        let s2: S2
        init(s2: S2) {
            self.s2 = s2
        }
        func f1() -> String {
            return "S1 OK"
        }
    }
    
    class S2Impl: S2 {
        func f2() -> String {
            return "S2 OK"
        }
    }
    
    // Service Locator initialization
    
    let sl: ServiceLocator = {
        let sl = LazyServiceLocator()
        sl.addService { S1Impl(s2: sl.getService()!) as S1 }
        sl.addService { S2Impl() as S2 }
        return sl
    }()
    
    // Test run
    
    let s1: S1? = sl.getService()
    let s2: S2? = sl.getService()
    //let s2_: S2? = sl.getService()
    
    print(s1?.f1() ?? "S1 NOT FOUND") // S1 OK
    print(s2?.f2() ?? "S2 NOT FOUND") // S2 OK
    

    Pretty neat, isn't it? And I think that using a Service Locator in conjunction with Dependency Injection lets avoid some cons of the former pattern.