我试图尽可能使我的API服务更加通用:
首先,最重要的是,这不应该是一个目标。相反,您应该从使用案例开始,并确保您的API服务满足它们。 "尽可能通用" 毫无意义,只会让您在向事物添加 "通用功能" 时进入类型噩梦,这与能够普遍适用于许多用例不同。哪些调用者需要这种灵活性?从调用者开始,然后协议就会跟随。
func send<T>(request: RestRequest) -> T
接下来,这是一个非常糟糕的签名。你不想在返回类型上使用类型推断。这很难以管理。相反,在Swift中处理此问题的标准方式是:
func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType
通过将预期的结果类型作为参数传递,您可以摆脱类型推断带来的烦恼。这种烦恼看起来像这样:
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
编译器怎样知道
stringResponse
应该是一个字符串?这里没有写"String"。所以你必须这样做:
let stringResponse: String = ...
那很丑的 Swift 代码。相反,你可能想要以下代码(但实际上不是):
And that's very ugly Swift. Instead you probably want (but not really):
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
returning: String.self)
“但其实不行”,因为这样做没有好的实现方式。 send
如何知道如何将“我得到的任何响应”翻译成“一个被称为 String 的未知类型”? 那会有什么作用呢?
protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}
这个带关联类型的PAT协议并不是很合理。它说如果一个实例能返回一个ResponseType,那么就可以被解析。但这应该是一个解析器而不是“可以被解析的东西”。
如果要解析某些内容,你需要一个init方法,可以接受一些输入并创建自己。通常最好使用Codable,但你也可以自己创建,例如:
protocol Parseable {
init(parsing data: Data) throws
}
但我更倾向于使用Codable,或者只是传递解析函数(见下文)。
enum RestRequest {}
如果你想要通用性,则枚举可能是不恰当的使用方式。每次创建新的 RestRequest 都需要更新 parse
,这种代码写法不正确。枚举使得添加“所有实例都实现的新内容”很容易,但是添加“新类型的实例”却很困难。结构体(+ 协议)则相反。它们使得添加协议的新类型很容易,但是添加新的协议需求却很困难。在一个通用系统中,特别是请求(Requests),属于后者。你需要经常添加新的请求。而使用枚举就会变得困难。
还有更好且干净的方法来实现吗?
这取决于“this”是什么。你的调用代码是什么样的?你当前的系统是否存在代码重复,需要消除?你的使用情况是什么?“尽可能通用”并不存在。只有可以适应用例的系统。不同的配置轴线导致不同类型的多态,并具有不同的权衡。
你希望你的调用代码是什么样子?
这里只提供一个示例,比如像这样:
final class ApiService {
let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: @escaping (Response?) -> Void) {
urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(nil)
return
}
if let data = data {
let result = try? JSONDecoder().decode(Response.self, from: data)
completion(result)
return
}
completion(nil)
}
}
}
这是非常通用的,可以适用于许多种用例(虽然这种特定形式非常原始)。您可能会发现它并不适用于所有用例,所以您需要扩展它。例如,也许您不想在这里使用 Decodable。您想要一个更通用的解析器。那没问题,使解析器可配置:
func send<Response>(request: URLRequest,
returning: Response.Type,
parsedBy: @escaping (Data) -> Response?,
completion: @escaping (Response?) -> Void) {
urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
completion(nil)
return
}
if let data = data {
let result = parsedBy(data)
completion(result)
return
}
completion(nil)
}
}
也许您想要同时采用这两种方法。没问题,可以在其中一种方法之上构建另一种方法:
func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: @escaping (Response?) -> Void) {
send(request: request,
returning: returning,
parsedBy: { try? JSONDecoder().decode(Response.self, from: $0) },
completion: completion)
}
如果您想深入了解这个主题,您可能会对"Beyond Crusty"感兴趣,该文章包含一个实际例子,展示了如何将您所讨论的解析器串联在一起。它有些过时了,Swift协议现在更加强大,但基本信息没有改变,例如本例中的parsedBy
就是建立在此基础上的。