Swift - 协议只能用作泛型约束,因为它具有 Self 或关联类型要求

Swift - Protocol can only be used as a generic constraint because it has Self or associated type requirements

我正在开发一个需要查询多个 API 的应用程序。我已经为每个 API 提供商提供了 classes(在更极端的情况下,为每个特定的 API 端点提供了 class)。这是因为每个 API 查询都需要 return 非常严格的响应类型,因此如果 API 可以 return 用户个人资料和个人资料图片,我只想针对其中任何一个做出回应。

我大致是这样实现的:

protocol MicroserviceProvider {
    associatedtype Response
}

protocol ProfilePictureMicroserviceProvider: MicroserviceProvider {
    func getPicture(by email: String, _ completion: (Response) -> Void)
}

class SomeProfilePictureAPI: ProfilePictureMicroserviceProvider {
    struct Response {
        let error: Error?
        let picture: UIImage?
    }

    func getPicture(by email: String, _ completion: (Response) -> Void) {
        // some HTTP magic 
        // will eventually call completion(_:) with a Response object 
        // which either holds an error or a UIImage.
    }
}

因为我希望能够对依赖此 API 的 classes 进行单元测试,所以我需要能够动态注入个人资料图片依赖项。默认情况下,它将使用 SomeProfilePictureAPI,但是当 运行 测试时,我将能够用 MockProfilePictureAPI 替换它,它仍然会遵守 ProfilePictureMicroserviceProvider

并且因为我正在使用关联类型,所以我需要使 class 依赖于 ProfilePictureMicroserviceProvider 的类型成为通用类型。

起初,我确实天真地尝试过这样编写我的视图控制器

class SomeClass {
    var profilePicProvider: ProfilePictureMicroserviceProvider
}

但这导致了令人沮丧的著名 'Protocol ProfilePictureMicroserviceProvider can only be used as a generic constraint because it has Self or associated type requirements' 编译时错误。

现在我在过去几天一直在阅读这个问题,试图围绕关联类型协议 (PATS) 思考,并认为我会像这样走通用 classes 的路线:

class SomeClass<T: ProfilePictureMicroserviceProvider> {
    var profilePicProfider: T = SomeProfilePictureAPI() 
}

但即便如此,我仍收到以下错误:

Cannot convert value of type 'SomeProfilePictureAPI' to specified type 'T'

即使 T 受限于 ProfilePictureMicroserviceProvider 协议,并且 SomeProfilePictureAPI 遵守它...

基本上,主要思想是实现 2 个目标:强制执行具有强制响应类型的微服务结构,并使每个微服务可模拟以进行相关 classes 的单元测试。

我现在坚持选择两者之一,因为我似乎无法让它发挥作用。欢迎任何帮助告诉我我做错了什么。

我也看过类型擦除。但对我来说,这似乎很古怪,而且对于在许多方面看起来不对劲的事情来说是相当费力的。

所以基本上我的问题有两个方面:我如何强制我的微服务定义它们自己的响应类型?我如何才能轻松地用 class 中依赖于它们的模拟微服务替换它们?

你必须扭转这些要求;

与其将 MicroServiceProvider 注入到每个请求中,不如编写一个通用的微服务 'Connector' 协议,该协议应定义它对每个请求的期望,以及每个请求对它的期望 return。

然后您可以编写一个符合此协议的 TestConnector,以便您可以完全控制请求的处理方式。最好的部分是,您的请求甚至不需要修改。

考虑以下示例:

protocol Request {
    // What type data you expect to decode and return
    associatedtype Response

    // Turn all the data defined by your concrete type 
    // into a URLRequest that we can natively send out.
    func makeURLRequest() -> URLRequest

    // Once the URLRequest returns, decode its content
    // if it succeeds, you have your actual response object 
    func decode(incomingData: Data?) -> Response?
}

protocol Connector {
    // Take in any type conforming to Request, 
    // do whatever is needed to get back some potential data, 
    // and eventually call the handler with the expected response
    func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void)
}

这些基本上是设置此类框架的最低要求。在现实生活中,您会希望请求协议有更多要求(例如定义 URL、请求 headers、请求 body 等的方法)。

最好的部分是,您可以为您的协议编写默认实现。这删除了很多样板代码!因此,对于实际的连接器,您可以这样做:

extension Connector {
    func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void) {
        // Use a native URLSession
        let session = URLSession()

        // Get our URLRequest
        let urlRequest = request.makeURLRequest()

        // define how our URLRequest is handled
        let task = session.dataTask(with: urlRequest) { data, response, error in
            // Try to decode our expected response object from the request's data
            let responseObject = request.decode(incomingData: data)

            // send back our potential object to the caller's completion block
            handler(responseObject)
        }

        task.resume()
    }
}

现在,有了它,您需要做的就是像这样实现您的 ProfilePictureRequest(额外示例 class 变量):

struct ProfilePictureRequest: Request {
    private let userID: String
    private let useAuthentication: Bool

    /// MARK: Conform to Request
    typealias Response = UIImage

    func makeURLRequest() -> URLRequest {
        // get the url from somewhere
        let url = YourEndpointProvider.profilePictureURL(byUserID: userID)

        // use that URL to instantiate a native URLRequest
        var urlRequest = URLRequest(url: url)

        // example use: Set the http method
        urlRequest.httpMethod = "GET"

        // example use: Modify headers
        if useAuthentication {
            urlRequest.setValue(someAuthenticationToken.rawValue, forHTTPHeaderField: "Authorization")
        }

        // Once the configuration is done, return the urlRequest
        return urlRequest
    }

    func decode(incomingData: Data?) -> Response? {
        // make sure we actually have some data
        guard let data = incomingData else { return nil }

        // use UIImage's native data initializer.
        return UIImage(data: data)
    }
}

如果你想发送个人资料图片请求,那么你需要做的就是(你需要一个符合连接器的具体类型,但由于连接器协议有默认实现,该具体类型是在这个例子中大部分是空的:struct GenericConnector: Connector {}):

// Create an instance of your request with the arguments you desire
let request = ProfilePictureRequest(userID: "JohnDoe", useAuthentication: false)

// perform your request with the desired Connector
GenericConnector().perform(request) { image in 
    guard let image = image else { return }

    // You have your image, you can now use that instance whichever way you'd like
    ProfilePictureViewController.current.update(with: image)
}

最后,要设置您的 TestConnector,您需要做的就是:

struct TestConnector: Connector {

    // define a convenience action for your tests
    enum Behavior {
        // The network call always fails
        case alwaysFail

        // The network call always succeeds with the given response
        case alwaysSucceed(Any)
    }

    // configure this before each request you want to test
    static var behavior: Behavior

    func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void) {
        // since this is a test, you don't need to actually perform any network calls.
        // just check what should be done
        switch Self.behavior {
        case alwaysFail:
            handler(nil)

        case alwaysSucceed(let response):
            handler(response as! T)
        }
    }
}

有了这个,您可以轻松定义请求,它们应该如何配置它们的 URL 操作以及它们如何解码它们自己的响应类型,并且您可以轻松地为您的连接器编写模拟。

当然,请记住,此答案中给出的示例的使用方式非常有限。我强烈建议您看一下我写的 this library。它以更加结构化的方式扩展了这个示例。