Protocols with associated types is confusing:
// Lets say I have two possible type of responses
struct OtpResponse {}
struct SsoResponse {}
// A simple protocol to mandate the return of token from respective concrete type
protocol AuthenticationProvider {
associatedtype ResponseType
func getToken(completion: @escaping (ResponseType?, NSError?) -> Void)
}
// A type of auth provider
struct OtpBasedAuthProvider:AuthenticationProvider {
typealias ResponseType = OtpResponse
func getToken(completion: @escaping (OtpResponse?, NSError?) -> Void) {
let otpResponse = OtpResponse()
completion(otpResponse, nil)
}
}
// Another type of auth provider
struct SsoBasedAuthProvider: AuthenticationProvider {
typealias ResponseType = SsoResponse
func getToken(completion: @escaping (SsoResponse?, NSError?) -> Void) {
let ssoResponse = SsoResponse()
completion(ssoResponse, nil)
}
}
// There is some external logic to decide which type of auth provider to be used
func getProviderTypeFromSomeLogicOtherLogic() -> Int{
return 1 // simply for dummy
}
// Factory to return a concrete implementaton of auth provider
class AuthProviderFactory {
func getAuthProvider<T: AuthenticationProvider>(type:Int) -> T {
if type == 1 {
return SsoBasedAuthProvider() as! T
}
else {
return OtpBasedAuthProvider() as! T
}
}
}
Now to use the code above, I want to do something like this:
func executeNetworkCall() -> Void {
let factory = AuthProviderFactory() // 1
let authProvider = factory.getAuthProvider(type: getProviderTypeFromSomeLogicOtherLogic()) // 2
authProvider.getToken{ (resp, error) in // 3
// some code
}
}
In the above, line number 2 where I am trying to get provider type from factory is giving me error as :
Generic parameter 'T' could not be inferred.
I know I can get rid of compilation error by doing something like this :
let authProvider:SsoBasedAuthProvider = factory.getAuthProvider(type: getProviderTypeFromSomeLogicOtherLogic())
But thats not the point, I dont know which provider will be returned and I want to call .getToken from that provider.
Protocols with associatedtype
can’t be used in form of composition, which is a drawback and definitely irritating sometimes. But, you can create your own Type Erasure
class to make this work.
You can study more about type erasure from this link: https://www.donnywals.com/understanding-type-erasure-in-swift/. You can find many more on Google.
This is how Apple has implemented it internally, by making few changes we can make it work our way.
Below is the code I came up with:
//Let's say I have two possible type of responses
struct OtpResponse{}
struct SsoResponse{}
//A simple protocol to mandate the return of token from respective concrete type
protocol AuthenticationProvider{
associatedtype ResponseType
func getToken(completion: @escaping(ResponseType?, NSError?) -> Void)
}
//A type of auth provider
struct OtpBasedAuthProvider:AuthenticationProvider{
func getToken(completion: @escaping (OtpResponse?, NSError?) -> Void) {
let otpResponse = OtpResponse()
completion(otpResponse,nil)
}
}
//Another type of auth provider
struct SsoBasedAuthProvider:AuthenticationProvider{
func getToken(completion: @escaping (SsoResponse?, NSError?) -> Void) {
let ssoResponse = SsoResponse()
completion(ssoResponse,nil)
}
}
// there is some external logic to decide which type of auth provider to be used
func getProviderTypeFromSomeLogicOtherLogic() -> Int{
return 1//simply for dummy
}
Type Erasure:
class _AnyCacheBox<Storage>:AuthenticationProvider{
func getToken(completion: @escaping (Storage?, NSError?) -> Void) {
fatalError("Never to be called")
}
}
final class _CacheBox<C:AuthenticationProvider>: _AnyCacheBox<C.ResponseType>{
private var _base:C
init(base:C) {
self._base = base
}
override func getToken(completion: @escaping (C.ResponseType?, NSError?) -> Void) {
_base.getToken(completion: completion)
}
}
struct AnyCache<Storage>:AuthenticationProvider{
private let _box: _AnyCacheBox<Storage>
init<C:AuthenticationProvider>(cache:C) where C.ResponseType == Storage {
_box = _CacheBox(base: cache)
}
func getToken(completion: @escaping (Storage?, NSError?) -> Void) {
_box.getToken(completion: completion)
}
}
//Factory to return a concrete implementaton of auth provider
class AuthProviderFactory{
func getOTPAuthProvider() -> AnyCache<OtpResponse>{
let obj : AnyCache = AnyCache(cache: OtpBasedAuthProvider())
return obj
}
func getSSoAuthProvider() -> AnyCache<SsoResponse>{
let obj : AnyCache = AnyCache(cache: SsoBasedAuthProvider())
return obj
}
}
Below is how client can invoke methods in Factory-:
func executeNetworkCall() -> Void{
let factory = AuthProviderFactory()
let authProvider = factory.getOTPAuthProvider()
authProvider.getToken{(resp,error) in
//some code
print(resp)
}
}
It’s a bit involving and could take time to understand.