So I have a little dependency injection library I wrote called Factory and I was working on a few examples of how to use it and I ran into a problem.
This is, however, going to take a little setup.
All of us, at one point or another, have probably defined a protocol and associated services something like the following:
protocol AccountLoading {
func load() -> [Account]
}
struct AccountLoader: AccountLoading {
func load() -> [Account] {
return [Account()]
}
}
Note that my protocol and loader return straight, non-async values just to keep the code simple.
With Factory I can register a service that uses both of these for later injection.
extension Container {
static let accountLoader = Factory<AccountLoading> { AccountLoader() }
}
Now, with the protocol in place, I can also create a mock version and use that for previews, mocking, and testing.
struct MockAccountLoader: AccountLoading {
func load() -> [Account] {
return [Account()]
}
}
func setupMocks() {
Container.accountLoader.register { MockAccountLoader() }
}
This is all good so far. I have my protocol. I have my service. I have mocks. But...
It would be nice if, instead of manually creating loaders and mock handlers, I could use some spiffy generic service providers. (This is where the problems begin....)
Let's say I have a couple of services like the following:
struct NetworkLoader<T> {
let path: String
func load() -> T {
fatalError()
}
}
struct MockLoader<T> {
let data: T
func load() -> T {
return data
}
}
That, in turn should let me revise my Factory and do...
extension Container {
static let accountLoader = Factory<AccountLoading> {
NetworkLoader<[Account]>(path: "/api/accounts")
}
}
But I get an error about how NetworkLoader doesn't conform to AccountLoading... because, well, it doesn't. I get that. I have a load() -> [Account] function, but that doesn't automatically make it conform to the protocol.
So let's add that conformance to the loader and to the mock loader as well.
extension NetworkLoader<[Account]>: AccountLoading {}
extension MockLoader<[Account]>: AccountLoading {}
Success! But... this is a little clunky. I can use my generic services, but I still have to do the following for each and every type I want to work with.
protocol AccountLoading {
func load() -> [Account]
}
extension NetworkLoader<[Account]>: AccountLoading {}
extension MockLoader<[Account]>: AccountLoading {}
It would be nice if I could bypass the need for all of that. Maybe a generic protocol is in order......
protocol TypeLoading {
associatedtype T
func load() -> T
}
extension NetworkLoader: TypeLoading {}
extension MockLoader: TypeLoading {}
And then create my Factory using the generic type...
extension Container {
static let accountLoader = Factory<TypeLoading<[Account]>> {
NetworkLoader<[Account]>(path: "/api/accounts")
}
}
But sadness. Swift doesn't approve of this and tells me that I can't specialize protocol type TypeLoading.
Bummer.
Now, I could do the following...
protocol NewAccountLoading: TypeLoading where T == [Account] {}
extension NetworkLoader<[Account]>: NewAccountLoading {}
extension MockLoader<[Account]>: NewAccountLoading {}
extension Container {
static let accountLoader = Factory<any NewAccountLoading> {
NetworkLoader<[Account]>(path: "/api/accounts")
}
}
But, similar to how things worked earlier, I still have to define the NewXXXLoading protocol and extensions for each and every type I want to use.
The code I need to write is getting smaller, but that's still boilerplate code I'd simply prefer to avoid.
I could ignore protocols altogether and go classic OOP...
class AbstractLoader<T> {
private init() {}
func load() -> T {
fatalError()
}
}
class NetworkLoader<T>: AbstractLoader <T> {
// implementation
}
class MockLoader<T>: AbstractLoader <T> {
// implementation
}
Which at least gives me a clean Factory specification without the need for extra protocol and extension definitions.
extension Container {
static let accountLoader = Factory<AbstractLoader<[Account]>> {
NetworLoader<[Account]>(path: "/api/accounts")
}
}
But in this day and age that feels... well... old. And not very Swifty.
Which brings me, finally, to my question. Is something like the following even possible given the state of Swift generics?
extension Container {
static let accountLoader = Factory<TypeLoading<[Account]>> {
NetworkLoader<[Account]>(path: "/api/accounts")
}
}
What am I missing?
That's all I want for Christmas. A simple definition without extra protocols and extensions.
Well. maybe that and the pony....