Swift Codable with Generics,保存响应中的公共数据

Swift Codable with Generics, holding common data from responses

我会尽量以最好的方式解释我想做的事情,就像我在过去几天谷歌搜索时所做的那样。

我的应用程序正在与几个不同的 API 进行通信,但让我们首先考虑来自一个 API 的响应。来自每个端点的响应包含一些'common parameters',例如状态或错误消息,以及我们最感兴趣的单个对象或对象数组,它带来了重要数据,我们可能想对其进行编码、存储,放到Realm、CoreData等

例如,单个对象的响应:

{
    "status": "success",
    "response_code": 200,
    "messages": [
        "message1",
        "message2"
    ]
    "data": {
        OBJECT we're interested in.
    }
}

或者,用对象数组响应:

{
    "status": "success",
    "response_code": 200,
    "messages": [
        "message1",
        "message2"
    ]
    "data": [
        {
            OBJECT we're interested in.
        },
        {
            OBJECT we're interested in.
        }
    ]
}

好的。就是这么简单,容易理解。

现在,我想写一个 "Root" 对象,它将容纳 'common parameters' 或 statusresponse_codemessages 并有另一个, 属性 对于我们感兴趣的特定对象(或对象数组)。


继承
第一种方法是创建一个根对象,如下所示:

class Root: Codable {
    let status: String
    let response_code: Int
    let messages: [String]?

    private enum CodingKeys: String, CodingKey {
        case status, response_code, messages
    }

    required public init(from decoder: Decoder) throws {
        let container = try? decoder.container(keyedBy: CodingKeys.self)
        status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
        response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
        messages = try container?.decodeIfPresent([String].self, forKey: .key)
    }

    public func encode(to encoder: Encoder) throws {}
}

一旦我有了这个 Root 对象,我就可以创建从这个 Root 对象继承的特定对象,并将我的特定对象传递给 JSONDecoder,这样,我就有了一个很好的解决方案。但是,这个解决方案对于数组 是失败的。 也许对于某些人来说它没有,但我怎么强调我都不想制作额外的 'plural' 对象,该对象只存在于保存对象数组 ,喜欢:

class Objects: Root {
    let objects: [Object]
    // Code that decodes array of "Object" from "data" key
}

struct Object: Codable {
    let property1
    let property2
    let property3
    // Code that decodes all properties of Object
}

它看起来不干净,它需要单独的对象来简单地保存一个数组,它在某些情况下由于继承而在存储到 Realm 时产生问题,它 最重要的是 产生可读性较差的代码。


泛型
我的第二个想法是尝试使用泛型,所以我做了 a little something like this:

struct Root<T: Codable>: Codable  {
    let status: String
    let response_code: Int
    let messages: [String]?
    let data: T?

    private enum CodingKeys: String, CodingKey {
        case status, response_code, messages, data
    }

    required public init(from decoder: Decoder) throws {
        let container = try? decoder.container(keyedBy: CodingKeys.self)
        status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
        response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
        messages = try container?.decodeIfPresent([String].self, forKey: .key)
        data = try container.decodeIfPresent(T.self, forKey: .data)
    }

    public func encode(to encoder: Encoder) throws {}
}

有了这个,我可以像这样将单个对象和对象数组传递给 JSON 解码器:

let decodedValue = try JSONDecoder().decode(Root<Object>.self, from: data)
// or
let decodedValue = try JSONDecoder().decode(Root<[Object]>.self, from: data)

这很不错。我可以在 Root 结构的 .data 属性 中获取我需要的结构,并按我喜欢的方式使用它,作为单个对象或对象数组。我可以轻松地存储它,随心所欲地操作它,继承带来了上例。
这个想法对我来说失败的地方是当我想在某个不确定 T 设置的地方访问 'common properties' 时。
这是对我的应用程序中实际发生的事情的简化解释,我将对其进行一些扩展以解释此通用解决方案对我不起作用的地方,最后问我的问题。


问题与疑问
如顶部所述,应用程序使用 3 个 API,所有 3 个 API 都有不同的 Root 结构,当然还有很多不同的 "sub-structs" - 到给他们起名字。我在应用程序中有一个单独的地方,单个 APIResponse 对象可以返回到应用程序的 UI 部分,我在其中从 decoded value 中提取了 1 个可读错误,decoded value 就是那个'sub-struct',是我的 "specific Objects"、CarDogHousePhone
中的任何一个 使用 Inheritance 解决方案,我可以做这样的事情:

struct APIResponse <T> {
    var value: T? {
        didSet {
            extractErrorDescription()
        }
    }
    var errorDescription: String? = "Oops."

    func extractErrorDescription() {
        if let responseValue = value as? Root1, let error = responseValue.errors.first {
            self.errorDescription = error
        }
        else if let responseValue = value as? Root2 {
            self.errorDescription = responseValue.error
        }
        else if let responseValue = value as? Root3 {
            self.errorDescription = responseValue.message
        }
    }
}

但是使用 Generics 解决方案,我无法做到这一点。如果我尝试使用 Root1Root2Root3 编写相同的代码,如 Generics 示例所示:

func extractErrorDescription() {
    if let responseValue = value as? Root1, let error = responseValue.errors.first {
        self.errorDescription = error
    }
}

我会收到错误提示 Generic parameter 'T' could not be inferred in cast to 'Root1',在这里,我试图提取错误,但我不知道哪个子结构被传递给了 Root1。是 Root1<Dog> 还是 Root1<Phone> 还是 Root1<Car> - 我不知道怎么弄清楚,我显然需要知道才能确定值是 Root1 还是Root2Root3

我正在寻找的解决方案是一种可以让我区分 Root 对象与上面显示的泛型解决方案的解决方案,或者是一种允许我在一些完全不同的架构中解码的解决方案方式,记住我写的所有内容,尤其是避免 'plural' objects

的能力

*如果JSON没有通过JSON验证器,请无视,手写就是为了这个问题
**如果编写的代码没有运行,请忽略,这更多的是一个架构问题,而不是如何编译一些代码。

您在这里寻找的是协议。

protocol ErrorProviding {
    var error: String? { get }
}

我有意将 errorDescription 更改为 error,因为这似乎是您的根类型中的内容(但您绝对可以在此处重命名)。

然后 APIResponse 要求:

struct APIResponse<T: ErrorProviding> {
    var value: T?
    var error: String? { value?.error }
}

然后每个具有特殊处理的根类型实现协议:

extension Root1: ErrorProviding {
    var error: String? { errors.first }
}

但是已经具有正确形状的简单根类型可以声明符合性,不需要额外的实现。

extension Root2: ErrorProviding {}

假设您想要的不仅仅是 error,您可以将其设为 APIPayload 而不是 ErrorProviding 并添加任何其他常见要求。

附带说明一下,如果您只使用 Decodable 而不是使用带有空 encode 方法的 Codable,您的代码会更简单。如果一个类型不能真正被编码,它就不应该符合 Encodable。