0

In a protocol, I'd like to create a single instance from functions so I use a container to store the static instances like this:

protocol MyProtocol {
    func networkService() -> NetworkService
}

extension MyProtocol {

    func networkService() -> NetworkService {
        if Singletons.networkService == nil {
            Singletons.networkService = NetworkService(abc: 123)
        }

        return Singletons.networkService!
    }
}

private enum Singletons {
    static var networkService: NetworkService?
}

Later on, a type can conform to it and replace the default implementation, but also requires a single instance:

struct MyType: MyProtocol {
    private static var networkService: NetworkService?

    func networkService() -> NetworkService {
        if Self.networkService == nil {
            Self.networkService = NetworkService(abc: 555)
        }

        return Self.networkService!
    }
}

What I'm hoping is to encapsulate this ceremony of creating the singleton by using a Property Wrapper, but on the type. I'd like to do something like this:

protocol MyProtocol {
    func networkService() -> NetworkService
}

extension MyProtocol {

    func networkService() -> NetworkService {
        @Singleton
        NetworkService(abc: 123)
    }
}

////

struct MyType: MyProtocol {

    func networkService() -> NetworkService {
        @Singleton
        NetworkService(abc: 555)
    }
}

Is there a way to achieve this or something similar?

TruMan1
  • 33,665
  • 59
  • 184
  • 335
  • Property wrappers wraps properties, but from the "I'd like to do something like this" bit, it seems like you want to wrap an _expression_ with a property wrapper? – Sweeper Feb 05 '21 at 06:42
  • Yes that's a good observation, I'd like to encapsulate the expression generically for any type. My example just shows NetworkService, but MyProtocol would be a container of multiple functions providing instances of different types back. – TruMan1 Feb 05 '21 at 06:52
  • "Property" Wrappers as you point out doesn't fit.. but you got me thinking maybe something like this would make more sense: `Singleton { NetworkService(abc: 555) }` – TruMan1 Feb 05 '21 at 06:56
  • 1
    I just finished writing up a solution, when I realised, you just want `lazy`, don't you? – Sweeper Feb 05 '21 at 07:10
  • 1
    But `static` already implies `lazy`, so... the question is kind of moot. `static` already encapsulates the "if nil then initialise, otherwise return the thing" behaviour that you want. – Sweeper Feb 05 '21 at 07:13
  • FYI This is called the "service locator" pattern, for further reading – Alexander Feb 05 '21 at 07:57
  • It is lazy and static does handle this, but how can the protocol have a default implementation then implementors override it, since protocols can’t have static values. This is why the protocol needs to have functions that’s optionally implemented by implementors unless there’s a better way to do that. – TruMan1 Feb 05 '21 at 12:28

1 Answers1

0

Here is my first attempt:

struct Single {
    private static var instances = [String: Any]()

    static func make<T>(_ instance: () -> T) -> T {
        let key = String(describing: type(of: T.self))

        guard let value = instances[key] as? T else {
            let resolved = instance()
            instances[key] = resolved
            return resolved
        }

        return value
    }
}

protocol NetworkService {}
struct NetworkDefaultService: NetworkService {
    let id = UUID().uuidString

    init() {
        print("Network Default: \(id)")
    }
}

struct NetworkMockService: NetworkService {
    let id = UUID().uuidString

    init() {
        print("Network Mock: \(id)")
    }
}

protocol LocationService {}
class LocationDefaultService: LocationService {
    let id = UUID().uuidString

    init() {
        print("Location Default: \(id)")
    }
}

protocol NonSingleService {}
struct NonSingleDefaultService: NonSingleService {
    let id = UUID().uuidString

    init() {
        print("Non-Single Default: \(id)")
    }
}

protocol Context {
    func networkService() -> NetworkService
    func locationService() -> LocationService
    func nonSingleService() -> NonSingleService
}

extension Context {

    func networkService() -> NetworkService {
        Single.make {
            NetworkDefaultService()
        }
    }

    func locationService() -> LocationService {
        Single.make {
            LocationDefaultService()
        }
    }
}

struct AppContext: Context {

    func networkService() -> NetworkService {
        Single.make {
            NetworkMockService()
        }
    }

    func nonSingleService() -> NonSingleService {
        NonSingleDefaultService()
    }
}

let context = AppContext()
context.networkService()
context.networkService()
context.locationService()
context.locationService()
context.nonSingleService()
context.nonSingleService()

This prints:

Network Mock: 48CBDE3A-26D2-4767-A6AA-F846F8863A52
Location Default: 4846953B-93F6-4025-A970-DA5B47470652
Non-Single Default: 957979D8-9F3E-428E-BD87-B9F45D56B755
Non-Single Default: 816D2886-D606-4558-A842-295C833AE4C8
TruMan1
  • 33,665
  • 59
  • 184
  • 335