Swift Combine:无法重构重复代码
Swift Combine: Cannot refactor repetitive code
我的APIreturn这个格式,其中data
可以包含各种回复。
{
status: // http status
error?: // error handle
data?: // your response data
meta?: // meta data, eg. pagination
debug?: // debuging infos
}
我已经制作了一个 Codable
响应类型,其中包含可选数据的泛型,我们不知道其类型。
struct MyResponse<T: Codable>: Codable {
let status: Int
let error: String?
let data: T?
let meta: Paging?
let debug: String?
}
我现在正在尝试编写 API 便捷方法 尽可能简洁 。因此,我有一个 return 通用发布者的功能,我可以将其用于所有这些响应,即 pre-parses 响应并预先捕获任何错误的发布者。
首先,我得到一个 dataTaskPublisher
来处理参数输入(如果有的话)。 Endpoint
只是方便 String
enum
我的端点, Method
是相似的。 MyRequest
return 一个 URLRequest
和一些必要的 headers 等等
注意我定义参数的方式:params: [String:T]
。这是标准的 JSON,因此它可以是字符串、数字等
这 T
似乎是问题所在。。
static fileprivate func publisher<T: Encodable>(
_ path: Endpoint,
method: Method,
params: [String:T] = [:]) throws
-> URLSession.DataTaskPublisher
{
let url = API.baseURL.appendingPathComponent(path.rawValue)
var request = API.MyRequest(url: url)
if method == .POST && params.count > 0 {
request.httpMethod = method.rawValue
do {
let data = try JSONEncoder().encode(params)
request.httpBody = data
return URLSession.shared.dataTaskPublisher(for: request)
}
catch let err {
throw MyError.encoding(description: String(describing: err))
}
}
return URLSession.shared.dataTaskPublisher(for: request)
}
接下来,我正在解析响应。
static func myPublisher<T: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: [String:T] = [:])
-> AnyPublisher<MyResponse<R>, MyError>
{
do {
return try publisher(path, method: method, params: params)
.map(\.data)
.mapError { MyError.network(description: "\([=14=])")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { MyError.encoding(description: "\([=14=])")} //(2)
.tryMap {
if [=14=].status > 204 {
throw MyError.network(description: "\([=14=].status): \([=14=].error!)")
}
else {
return [=14=] // returns a MyResponse
}
}
.mapError { [=14=] as! MyError }
//(1)
.eraseToAnyPublisher()
}
catch let err {
return Fail<MyResponse<R>,MyError>(error: err as? MyError ??
MyError.undefined(description: "\(err)"))
.eraseToAnyPublisher()
}
}
现在我可以轻松编写端点方法了。这里有两个例子。
static func documents() -> AnyPublisher<[Document], MyError> {
return myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: [=15=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<[Document], MyError>
}
和
static func user() -> AnyPublisher<User, MyError> {
return myPublisher(.user)
.map(\.data!)
.mapError { MyError.network(description: [=16=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<User, MyError>
}
这一切都运作良好。请注意,每次我都必须指定我的确切 return 类型两次。我想我可以接受。
我应该能够对此进行简化,这样我就不必每次都以完全相同的方式重复相同的三个运算符(map、mapError、receive)。
但是当我在上面的 //(1)
位置插入 .map(\.data!)
时,我在 //(2)
.
位置收到错误 Generic parameter T could not be inferred.
这真是令人困惑。为什么 input 参数中的泛型类型在这里起作用?这必须与上面对 .decode
运算符的调用有关,其中所讨论的泛型称为 R
,而不是 T
.
你能解释一下吗?我如何在上游重构这些运算符?
这段代码有一些小问题。没错,一个是 [String: T]
。这意味着对于给定的一组参数,所有值都必须属于同一类型。那不是“JSON”。这将接受 [String: String]
或 [String: Int]
,但如果这样做,您不能在同一个字典中同时拥有 Int 和 String 值。而且它也会接受 [String: Document]
,而且你似乎并不真的想要那个。
我建议将其切换为 Encodable,如果方便的话,这将允许您传递结构,如果方便的话,也可以传递字典:
func publisher<Params: Encodable>(
_ path: Endpoint,
method: Method,
params: Params?) throws
-> URLSession.DataTaskPublisher
func myPublisher<Params: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: Params?)
-> AnyPublisher<MyResponse<R>, MyError>
然后修改您的 params.count
以检查 nil。
请注意,我没有将 params = nil
设为默认参数。那是因为这会重现您遇到的第二个问题。 T
(和Params)在默认情况下无法推断。对于 = [:]
,T
是什么? Swift 必须知道,即使它是空的。因此,您使用重载而不是默认值:
func myPublisher<R: Decodable>(
_ path: Endpoint,
method: Method = .GET)
-> AnyPublisher<MyResponse<R>, MyError> {
let params: String? = nil // This should be `Never?`, see https://twitter.com/cocoaphony/status/1184470123899478017
return myPublisher(path, method: method, params: params)
}
现在,当您不传递任何参数时,Params 自动变为 String。
所以现在你的代码没问题了,你不需要最后的 as
func documents() -> AnyPublisher<[Document], MyError> {
myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: [=12=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() // <== Removed `as ...`
}
现在,那个 .map(\.data!)
让我很难过。如果您从服务器取回损坏的数据,应用程序将崩溃。应用程序崩溃有很多充分的理由;糟糕的服务器数据从来都不是其中之一。但是解决这个问题与这个问题并没有真正的关系(并且有点复杂,因为除了 Error 之外的 Failure 类型目前使事情变得困难),所以我暂时离开它。我的一般建议是使用 Error 作为您的失败类型,并允许意外错误出现而不是将它们包装在 .undefined
案例中。如果你无论如何都需要一些 catch-all "other",你最好用类型("is")而不是额外的枚举大小写(它只是将 "is" 移动到一个开关)。至少,我会尽可能晚地移动 Error->MyError 映射,这将使处理起来更容易。
再做一些调整,让后面的事情更通用一些,我怀疑 MyResponse 只需要是可解码的,而不是可编码的(其余的都可以,但它使它更灵活一点):
struct MyResponse<T: Decodable>: Decodable { ... }
对于您最初提出的如何使其可重用的问题,您现在可以提取一个通用函数:
func fetch<DataType, Params>(_: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET,
params: Params?) -> AnyPublisher<DataType, MyError>
where DataType: Decodable, Params: Encodable
{
myPublisher(endpoint, method: method, params: params)
.map(\.data!)
.mapError { MyError.network(description: [=14=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// Overload to handle no parameters
func fetch<DataType>(_ dataType: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET) -> AnyPublisher<DataType, MyError>
where DataType: Decodable
{
fetch(dataType, from: endpoint, method: method, params: nil as String?)
}
func documents() -> AnyPublisher<[Document], MyError> {
fetch([Document].self, from: .documents)
}
我的APIreturn这个格式,其中data
可以包含各种回复。
{
status: // http status
error?: // error handle
data?: // your response data
meta?: // meta data, eg. pagination
debug?: // debuging infos
}
我已经制作了一个 Codable
响应类型,其中包含可选数据的泛型,我们不知道其类型。
struct MyResponse<T: Codable>: Codable {
let status: Int
let error: String?
let data: T?
let meta: Paging?
let debug: String?
}
我现在正在尝试编写 API 便捷方法 尽可能简洁 。因此,我有一个 return 通用发布者的功能,我可以将其用于所有这些响应,即 pre-parses 响应并预先捕获任何错误的发布者。
首先,我得到一个 dataTaskPublisher
来处理参数输入(如果有的话)。 Endpoint
只是方便 String
enum
我的端点, Method
是相似的。 MyRequest
return 一个 URLRequest
和一些必要的 headers 等等
注意我定义参数的方式:params: [String:T]
。这是标准的 JSON,因此它可以是字符串、数字等
这 T
似乎是问题所在。。
static fileprivate func publisher<T: Encodable>(
_ path: Endpoint,
method: Method,
params: [String:T] = [:]) throws
-> URLSession.DataTaskPublisher
{
let url = API.baseURL.appendingPathComponent(path.rawValue)
var request = API.MyRequest(url: url)
if method == .POST && params.count > 0 {
request.httpMethod = method.rawValue
do {
let data = try JSONEncoder().encode(params)
request.httpBody = data
return URLSession.shared.dataTaskPublisher(for: request)
}
catch let err {
throw MyError.encoding(description: String(describing: err))
}
}
return URLSession.shared.dataTaskPublisher(for: request)
}
接下来,我正在解析响应。
static func myPublisher<T: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: [String:T] = [:])
-> AnyPublisher<MyResponse<R>, MyError>
{
do {
return try publisher(path, method: method, params: params)
.map(\.data)
.mapError { MyError.network(description: "\([=14=])")}
.decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
.mapError { MyError.encoding(description: "\([=14=])")} //(2)
.tryMap {
if [=14=].status > 204 {
throw MyError.network(description: "\([=14=].status): \([=14=].error!)")
}
else {
return [=14=] // returns a MyResponse
}
}
.mapError { [=14=] as! MyError }
//(1)
.eraseToAnyPublisher()
}
catch let err {
return Fail<MyResponse<R>,MyError>(error: err as? MyError ??
MyError.undefined(description: "\(err)"))
.eraseToAnyPublisher()
}
}
现在我可以轻松编写端点方法了。这里有两个例子。
static func documents() -> AnyPublisher<[Document], MyError> {
return myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: [=15=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<[Document], MyError>
}
和
static func user() -> AnyPublisher<User, MyError> {
return myPublisher(.user)
.map(\.data!)
.mapError { MyError.network(description: [=16=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() as AnyPublisher<User, MyError>
}
这一切都运作良好。请注意,每次我都必须指定我的确切 return 类型两次。我想我可以接受。
我应该能够对此进行简化,这样我就不必每次都以完全相同的方式重复相同的三个运算符(map、mapError、receive)。
但是当我在上面的 //(1)
位置插入 .map(\.data!)
时,我在 //(2)
.
Generic parameter T could not be inferred.
这真是令人困惑。为什么 input 参数中的泛型类型在这里起作用?这必须与上面对 .decode
运算符的调用有关,其中所讨论的泛型称为 R
,而不是 T
.
你能解释一下吗?我如何在上游重构这些运算符?
这段代码有一些小问题。没错,一个是 [String: T]
。这意味着对于给定的一组参数,所有值都必须属于同一类型。那不是“JSON”。这将接受 [String: String]
或 [String: Int]
,但如果这样做,您不能在同一个字典中同时拥有 Int 和 String 值。而且它也会接受 [String: Document]
,而且你似乎并不真的想要那个。
我建议将其切换为 Encodable,如果方便的话,这将允许您传递结构,如果方便的话,也可以传递字典:
func publisher<Params: Encodable>(
_ path: Endpoint,
method: Method,
params: Params?) throws
-> URLSession.DataTaskPublisher
func myPublisher<Params: Encodable, R: Decodable>(
_ path: Endpoint,
method: Method = .GET,
params: Params?)
-> AnyPublisher<MyResponse<R>, MyError>
然后修改您的 params.count
以检查 nil。
请注意,我没有将 params = nil
设为默认参数。那是因为这会重现您遇到的第二个问题。 T
(和Params)在默认情况下无法推断。对于 = [:]
,T
是什么? Swift 必须知道,即使它是空的。因此,您使用重载而不是默认值:
func myPublisher<R: Decodable>(
_ path: Endpoint,
method: Method = .GET)
-> AnyPublisher<MyResponse<R>, MyError> {
let params: String? = nil // This should be `Never?`, see https://twitter.com/cocoaphony/status/1184470123899478017
return myPublisher(path, method: method, params: params)
}
现在,当您不传递任何参数时,Params 自动变为 String。
所以现在你的代码没问题了,你不需要最后的 as
func documents() -> AnyPublisher<[Document], MyError> {
myPublisher(.documents)
.map(\.data!)
.mapError { MyError.network(description: [=12=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher() // <== Removed `as ...`
}
现在,那个 .map(\.data!)
让我很难过。如果您从服务器取回损坏的数据,应用程序将崩溃。应用程序崩溃有很多充分的理由;糟糕的服务器数据从来都不是其中之一。但是解决这个问题与这个问题并没有真正的关系(并且有点复杂,因为除了 Error 之外的 Failure 类型目前使事情变得困难),所以我暂时离开它。我的一般建议是使用 Error 作为您的失败类型,并允许意外错误出现而不是将它们包装在 .undefined
案例中。如果你无论如何都需要一些 catch-all "other",你最好用类型("is")而不是额外的枚举大小写(它只是将 "is" 移动到一个开关)。至少,我会尽可能晚地移动 Error->MyError 映射,这将使处理起来更容易。
再做一些调整,让后面的事情更通用一些,我怀疑 MyResponse 只需要是可解码的,而不是可编码的(其余的都可以,但它使它更灵活一点):
struct MyResponse<T: Decodable>: Decodable { ... }
对于您最初提出的如何使其可重用的问题,您现在可以提取一个通用函数:
func fetch<DataType, Params>(_: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET,
params: Params?) -> AnyPublisher<DataType, MyError>
where DataType: Decodable, Params: Encodable
{
myPublisher(endpoint, method: method, params: params)
.map(\.data!)
.mapError { MyError.network(description: [=14=].errorDescription) }
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
// Overload to handle no parameters
func fetch<DataType>(_ dataType: DataType.Type,
from endpoint: Endpoint,
method: Method = .GET) -> AnyPublisher<DataType, MyError>
where DataType: Decodable
{
fetch(dataType, from: endpoint, method: method, params: nil as String?)
}
func documents() -> AnyPublisher<[Document], MyError> {
fetch([Document].self, from: .documents)
}