Search code examples
iosswiftprotocolstype-erasureassociated-types

Swift - implement protocol with generic method for different <T> types


I have a Client protocol that looks like this:

protocol Client {
  func get<T>(_ url: String) -> Promise<T>
}

Now, the problems start when I try to implement it. I want to have 2 types of clients: AlamofireClient and MockClient (maybe in the future more, like ErrorClient etc. but let's start simple)

so I need something like:

final class AlamofireClient: Client {
   func get<T: Decodable>(_ url: String) -> Promise<T> {
        (...)
   }
}

and:

final class MockClient: Client {
   func get<T: Mockable>(_ url: String) -> Promise<T> {
            return Promise { seal in
               seal.fulfill(T.mock())
        }
   }
}

here the simple Mockable interface that every entity in the app will implement:

public protocol Mockable {
    static func mock() -> Self
}

But I always get the error:

Type 'MockClient' does not conform to protocol 'Client'
Type 'AlamofireClient' does not conform to protocol 'Client'

That's because is not the same as <T: Decodable> and <T: Mockable>

Is there an elegant way to solve this issue? I'm not able to find an elegant solution for this problem. Maybe the whole idea is just bad? I also tried solving the problem with PATs but also no luck there.


Solution

  • The way you have it written, by conforming to the protocol Client, you have to implement a function get, with a generic, unconstrained argument T. In the example implementations provided, you added a type constraint to the generic parameter T, which does not match the function in the protocol.

    There's more than one way you can approach a solution to this problem. Keeping in mind that you said all entities will conform to Mockable, the solution that requires the least change to the code you provided is to use protocol composition to enforce that all parameters T conform to both Decodable and Mockable.

    protocol Client {
      func get<T: Decodable & Mockable>(_ url: String) -> Promise<T>
    }
    

    In your clients, you would implement that function exactly as written.

    Now, in your MockClient, you can call T.mock(), and in your real implementations, you can treat T as Decodable as required. Of course, this now requires that even your mock arguments conform to Decodable, but I would assume your mock arguments will be fairly lightweight and thus this wouldn't be a problem.