我会尽量用最好的方式解释我想做什么,就像我过去几天在谷歌上搜索时所做的那样。
我的应用程序正在与几个不同的API通信,但让我们首先考虑来自一个API的响应。来自每个端点的响应都包含一些"常见参数",如状态或错误消息,以及我们最感兴趣的单个对象或对象阵列,它带来了重要的数据,我们可能想对其进行编码、存储,并将其放入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.
}
]
}
好的。这很简单,很容易理解。
现在,我想写一个"根"对象,它将包含"公共参数",或status
、response_code
和messages
,并具有我们感兴趣的特定对象(或对象数组)的另一个属性。
继承
第一种方法是创建根对象,如下所示:
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 {}
}
一旦我有了这个根对象,我就可以创建从这个根对象继承的特定对象,并将我的特定对象传递到JSONDecoder中,在那里,我有了一个很好的解决方案。但是,此解决方案对于阵列是失败的。对于某些人来说,这可能不是,,但我再怎么强调也不为过,我多么不想制作额外的"复数"对象,它只存在于一个对象数组中,比如:
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时产生问题,最重要的是,它生成的代码可读性较差。
Generics
我的第二个想法是尝试Generics,所以我做了一个类似的小东西:
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 {}
}
有了这个,我就可以将单个对象和对象数组都传递给JSONDecoder,如下所示:
let decodedValue = try JSONDecoder().decode(Root<Object>.self, from: data)
// or
let decodedValue = try JSONDecoder().decode(Root<[Object]>.self, from: data)
这真是太好了。我可以在根结构的.data属性中获取我需要的结构,并将其用作我喜欢的对象、单个对象或对象数组。我可以很容易地存储它,可以随心所欲地操作继承带来的上例
这个想法在我的案例中失败的地方是,当我想在某个不确定t设置为什么的地方访问"公共属性"时。
这是对我的应用程序中实际发生的事情的简化解释,我将对其进行一点扩展,以解释这个通用解决方案在哪里不适合我,最后问我的问题。
问题
如前所述,应用程序使用3个API,并且所有3个API都有不同的Root
结构,当然还有很多不同的"子结构"(仅举其名)。我在应用程序中有一个位置单一的APIResponse
对象,它可以返回到应用程序的UI部分,在该部分中,我从decoded value
中提取1个可读错误,decoded value
是那个"子结构",是我的任何"特定对象",Car
、Dog
、House
、Phone
使用继承解决方案,我可以执行以下操作:
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解决方案,我无法做到这一点。如果我试图用Root1
、Root2
或Root3
编写相同的代码,如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
、Root2
还是Root3
。
我正在寻找的解决方案是一个可以让我用上面显示的Generics解决方案区分Root
对象的解决方案,或者一个允许我以某种完全不同的方式构建解码的解决方案——记住我写的所有内容,特别是避免"复数"对象的能力
*如果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
,并添加任何其他常见需求。
顺便说一句,如果您只使用Decodeable,而不是使用空encode
方法的Codable,那么您的代码会更简单。如果一个类型不能真正编码,那么它就不应该符合Encodable。