Last active
July 16, 2022 18:39
-
-
Save davibe/983aaba09df540c949a886a03b4201d6 to your computer and use it in GitHub Desktop.
Using protocol composition (intersection types) as simple DI in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Providers | |
class Provider1 { } | |
protocol HasProvider1 { var provider1: Provider1 { get } } | |
class Provider2 { } | |
protocol HasProvider2 { var provider2: Provider2 { get } } | |
// a Provider that depends on the other two needs to be configured | |
protocol Configurable { | |
func configure(providerBag: HasProviders) | |
} | |
class SubProvider : Configurable { | |
typealias Dependencies = HasProvider1 & HasProvider2 | |
var deps: Dependencies? = nil | |
func configure(providerBag: HasProviders) { | |
self.deps = providerBag | |
} | |
func doWork() { | |
guard let deps = deps else { return } // always check we're configured first | |
} | |
} | |
protocol HasSubProvider { var subProvider: SubProvider { get } } | |
typealias HasProviders = HasProvider1 & HasProvider2 & HasSubProvider | |
// some view models depending on Providers | |
class ViewModel1 : Configurable { | |
typealias Dependencies = HasProvider1 | |
var deps: Dependencies? = nil | |
func configure(providerBag: HasProviders) { | |
self.deps = providerBag | |
} | |
} | |
class ViewModel2 : Configurable { | |
typealias Dependencies = HasProvider2 & HasSubProvider | |
var deps: Dependencies? = nil | |
func configure(providerBag: HasProviders) { | |
self.deps = providerBag | |
} | |
} | |
// compose a bag for holding all dependencies | |
struct ProviderBag: HasProvider1 & HasProvider2 & HasSubProvider { | |
let provider1 = Provider1() | |
let provider2 = Provider2() | |
let subProvider = SubProvider() | |
func bootstrap() { | |
[provider1, provider2, subProvider] | |
.compactMap { $0 as? Configurable } | |
.map { $0.configure(providerBag: self) } | |
} | |
func teardown() { | |
// ... | |
} | |
} | |
class FlowController { | |
// let invocationParameters = InvocationParameters() | |
// let localSettings = LocalSettings() | |
// let Environment = Environment() | |
// let currentStatus = CurrentStatus() | |
let providerBag = ProviderBag() | |
init() { providerBag.bootstrap() } | |
deinit { providerBag.teardown() } | |
func atSomePoint() { | |
let _ = ViewModel2().configure(providerBag: providerBag) | |
} | |
} | |
let flowController = FlowController() | |
flowController.atSomePoint() | |
// Very flexible | |
// - can depend on one or more components | |
// - can depend on protocols which may help separating concerns | |
// - can setup providers based on environment/context | |
// - can support event/reactive based communication between components | |
// by wiring them up during bootstrap | |
// Drawbacks | |
// - While very flexible this architecture ALLOWS having circular | |
// dependencies between Configurable providers | |
// - Configurable components always need to unwrap their `deps` basically | |
// supporting the case they're not configured (disabled) | |
// - Kotlin does not have intersection types so this is not immediately appliable |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment