gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 09:18:23 25 4
gpt4 key购买 nike

我正在开发一个需要查询多个 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 的类进行单元测试,所以我需要能够动态注入(inject)该配置文件图片依赖项。默认使用 SomeProfilePictureAPI但是在运行测试时,我可以将其替换为 MockProfilePictureAPI仍将遵守 ProfilePictureMicroserviceProvider .

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

起初,我天真地尝试像这样编写我的 View Controller
class SomeClass {
var profilePicProvider: ProfilePictureMicroserviceProvider
}

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

现在我在过去的几天里一直在阅读这个问题,试图把我的头脑包裹在具有关联类型的协议(protocol)( PATS )上,并认为我会采取这样的通用类的路线:
class SomeClass<T: ProfilePictureMicroserviceProvider> {
var profilePicProfider: T = SomeProfilePictureAPI()
}

但即便如此,我也会收到以下错误:
Cannot convert value of type 'SomeProfilePictureAPI' to specified type 'T'
即使有 T被限制在 ProfilePictureMicroserviceProvider协议(protocol),并具有 SomeProfilePictureAPI坚持...

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

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

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

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

最佳答案

你必须扭转这些要求;

与其将 MicroServiceProvider 注入(inject)每个请求,不如编写一个通用的 MicroService“连接器”协议(protocol),该协议(protocol)应该定义它对每个请求的期望,以及每个请求期望它返回的内容。

然后,您可以编写一个符合此协议(protocol)的 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)
}

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

最好的部分是,您可以为您的协议(protocol)编写默认实现。这删除了很多样板代码!所以对于一个实际的连接器,你可以这样做:
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 协议(protocol)具有默认实现,因此该具体类型在此示例中大部分为空: 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我写。它以更加结构化的方式扩展了这个示例。

关于Swift - 协议(protocol)只能用作通用约束,因为它具有 Self 或关联的类型要求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50945446/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com