简体   繁体   中英

How can I use Type Erasure with a protocol using associated type

I am working on a project that has a network client that basically follows the below pattern.

protocol EndpointType {
    var baseURL: String { get }
}

enum ProfilesAPI {
    case fetchProfileForUser(id: String)
}

extension ProfilesAPI: EndpointType {
    var baseURL: String {
        return "https://foo.bar"
    }
}

protocol ClientType: class {
    associatedtype T: EndpointType
    func request(_ request: T) -> Void
}

class Client<T: EndpointType>: ClientType {
    func request(_ request: T) -> Void {
        print(request.baseURL)
    }
}

let client = Client<ProfilesAPI>()

client.request(.fetchProfileForUser(id: "123"))

As part of tidying up this project and writing tests I have found the it is not possible to inject a client when conforming to the ClientType protocol.

let client: ClientType = Client<ProfilesAPI>() produces an error:

error: member 'request' cannot be used on value of protocol type 'ClientType'; use a generic constraint instead

I would like to maintain the current pattern ... = Client<ProfilesAPI>()

Is it possible to achieve this using type erasure? I have been reading but am not sure how to make this work.

To your actual question, the type eraser is straight-forward:

final class AnyClient<T: EndpointType>: ClientType {
    let _request: (T) -> Void
    func request(_ request: T) { _request(request) }

    init<Client: ClientType>(_ client: Client) where Client.T == T {
        _request = client.request
    }
}

You'll need one of these _func/func pairs for each requirement in the protocol. You can use it this way:

let client = AnyClient(Client<ProfilesAPI>())

And then you can create a testing harness like:

class RecordingClient<T: EndpointType>: ClientType {
    var requests: [T] = []
    func request(_ request: T) -> Void {
        requests.append(request)
        print("recording: \(request.baseURL)")
    }
}

And use that one instead:

let client = AnyClient(RecordingClient<ProfilesAPI>())

But I don't really recommend this approach if you can avoid it. Type erasers are a headache. Instead, I would look inside of Client , and extract the non-generic part into a ClientEngine protocol that doesn't require T . Then make that swappable when you construct the Client . Then you don't need type erasers, and you don't have to expose an extra protocol to the callers (just EndpointType).

For example, the engine part:

protocol ClientEngine: class {
    func request(_ request: String) -> Void
}

class StandardClientEngine: ClientEngine {
    func request(_ request: String) -> Void {
        print(request)
    }
}

The client that holds an engine. Notice how it uses a default parameter so that callers don't have to change anything.

class Client<T: EndpointType> {
    let engine: ClientEngine
    init(engine: ClientEngine = StandardClientEngine()) { self.engine = engine }

    func request(_ request: T) -> Void {
        engine.request(request.baseURL)
    }
}

let client = Client<ProfilesAPI>()

And again, a recording version:

class RecordingClientEngine: ClientEngine {
    var requests: [String] = []
    func request(_ request: String) -> Void {
        requests.append(request)
        print("recording: \(request)")
    }
}

let client = Client<ProfilesAPI>(engine: RecordingClientEngine())

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM