【问题标题】:Swift Combine: Cannot refactor repetitive codeSwift Combine:无法重构重复的代码
【发布时间】:2020-08-14 11:59:54
【问题描述】:

我的 API 返回这种格式,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 便利方法。所以我有一个函数可以返回一个通用发布者,我可以将它用于所有这些响应,即预先解析响应并提前捕获任何错误的发布者。

首先,我得到一个处理参数输入(如果有)的dataTaskPublisherEndpoint 只是方便String enum 对于我的端点,Method 是类似的。 MyRequest 返回一个 URLRequest 以及一些必要的标头等。

注意我定义参数的方式: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: "\($0)")}
            .decode(type: MyResponse<R>.self, decoder: self.agent.decoder)
            .mapError { MyError.encoding(description: "\($0)")}             //(2)
            .tryMap {
                if $0.status > 204 {
                    throw MyError.network(description: "\($0.status): \($0.error!)")
                }
                else {
                    return $0 // returns a MyResponse
                }
            }
            .mapError { $0 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: $0.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: $0.errorDescription) }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher() as AnyPublisher<User, MyError>
}

这一切运作良好。请注意,每次,我都必须指定我的确切返回类型两次。我想我可以忍受。

我应该能够简化这一点,这样我就不必每次都以完全相同的方式重复相同的三个运算符(map、mapError、receive)。

但是当我在上面的位置 //(1) 插入 .map(\.data!) 时,我在位置 //(2) 处收到错误 Generic parameter T could not be inferred.

这真是令人困惑。为什么 input 参数中的泛型类型在这里起任何作用?这必须与上面对.decode 运算符的调用有关,其中所讨论的泛型称为R,而不是T

你能解释一下吗?如何在上游重构这些运算符?

【问题讨论】:

  • 不能是什么类型,如果你不使用Combine,你将如何处理这个JSON?
  • T 是 json 允许的所有类型,但本质上是StringIntFloat。我正在按照上面的方式处理它,它工作正常。
  • 哦,我明白了。您只是对错误消息感到困惑。组合总是给出编译错误!与 your 通用无关。有关如何成功编辑组合代码的信息,请参阅我的技巧和提示页面:apeth.com/UnderstandingCombine/tricksandtips.html
  • 但是我不能让它工作......为什么我不能进一步过滤到data 字段?输入应该是一个MyResponse,它有一个可选的data 键路径。完成所有操作(捕获错误,在主队列上接收)后,错误仍然存​​在。下一步是什么?
  • 特别注意提供显式返回类型部分。但请阅读全文。按照我的技巧,你会明白的。

标签: swift dictionary generics combine


【解决方案1】:

这段代码有很多小问题。你是对的,一个是[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(和参数)在默认情况下无法推断。对于= [:]T 是什么?斯威夫特必须知道,即使它是空的。因此,您可以使用重载而不是默认值:

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: $0.errorDescription) }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher() // <== Removed `as ...`
}

现在,.map(\.data!) 让我很难过。如果您从服务器取回损坏的数据,应用程序将崩溃。崩溃应用程序有很多充分的理由;糟糕的服务器数据绝不是其中之一。但是解决这个问题与这个问题并没有真正的关系(并且有点复杂,因为除了 Error 之外的故障类型目前使事情变得困难),所以我现在就留下它。我的一般建议是使用 Error 作为您的失败类型,并允许意外错误冒出而不是将它们包装在 .undefined 案例中。如果您无论如何都需要一些包罗万象的“其他”,那么您最好使用类型(“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: $0.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)
}

【讨论】:

  • 谢谢,这看起来很不错。 1. 我实施了Params? 建议,当然,这更有意义。 2. 我试图通过首先检查nil 并抛出错误来避免data! 的缺点。 3. 如果没有参数,为了方便起见,我已经使用了重载。 4.DataType.Type 传递给函数是个好主意,它使调用API 更加简洁。相当优雅。
猜你喜欢
  • 1970-01-01
  • 2018-04-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多