【问题标题】:Swift - Protocol can only be used as a generic constraint because it has Self or associated type requirementsSwift - Protocol 只能用作通用约束,因为它具有 Self 或关联的类型要求
【发布时间】:2018-06-20 09:59:30
【问题描述】:

我正在开发一个需要查询多个 API 的应用。我已经为每个 API 提供者提供了类(在更极端的情况下,每个特定 API 端点都有一个类)。这是因为每个 API 查询都应该返回一个非常严格类型的响应,所以如果一个 API 可以,例如,返回用户个人资料和个人资料图片,我只想要一个特定于其中任何一个的响应。

我大致是这样实现的:

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 的类进行单元测试,所以我需要能够动态注入该个人资料图片依赖项。默认情况下,它将使用SomeProfilePictureAPI,但在运行测试时,我可以将其替换为MockProfilePictureAPI,它仍将遵循ProfilePictureMicroserviceProvider

因为我使用的是关联类型,所以我需要创建依赖于ProfilePictureMicroserviceProvider 的类泛型。

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

class SomeClass {
    var profilePicProvider: ProfilePictureMicroserviceProvider
}

但这只是导致了令人沮丧的著名'Protocol ProfilePictureMicroserviceProvider 只能用作通用约束,因为它具有 Self 或关联的类型要求'编译时错误。

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

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

但即使这样我也会收到以下错误:

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

即使 T 被限制为 ProfilePictureMicroserviceProvider 协议,并且 SomeProfilePictureAPI 遵守它...

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

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

我也看过类型擦除。但在我看来,这似乎很古怪,而且对于在许多方面看起来都错了的事情付出了很大的努力。

所以基本上我的问题有两个:如何强制我的微服务定义自己的响应类型?以及如何在依赖它们的类中通过模拟微服务轻松替换它们?

【问题讨论】:

  • 这里有个问题,你找到解决方法了吗?
  • @TuanDo 我做了,并最终用它写了一个 Cocoapod / SPM,看看:github.com/MrSkwiggs/Netswift。我会尝试根据这个写一个答案
  • 谢谢。期待您的回答
  • @TuanDo 已发布答案。看看,如果你需要什么,请随时告诉我:)

标签: swift swift-protocols associated-types


【解决方案1】:

您必须扭转这些要求;

您应该编写一个通用的微服务“连接器”协议,而不是向每个请求中注入一个 MicroServiceProvider,该协议应该定义它期望从每个请求中得到什么,以及每个请求期望它返回什么。

然后您可以编写一个符合此协议的 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、请求标头、请求正文等的方法)。

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

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(带有额外的示例类变量):

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)
    }
}

如果您想要发送个人资料图片请求,那么您需要做的就是(您需要一个符合 Connector 的具体类型,但由于 Connector 协议具有默认实现,该具体类型大多为空在本例中: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。它以更加结构化的方式扩展了这个示例。

【讨论】:

  • 非常感谢您的详细解释。这很容易理解。继续努力(y)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多