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。它以更加结构化的方式扩展了这个示例。
我正在开发一个需要查询多个 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。它以更加结构化的方式扩展了这个示例。