Swift Codable с Generics, содержит общие данные из ответов - PullRequest
0 голосов
/ 08 ноября 2019

Я постараюсь объяснить, что я хочу сделать, как можно лучше, точно так же, как я пытался в течение последних нескольких дней во время поиска в Google.

Мое приложение взаимодействует с несколькими разными 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 структуры Root и использовать ее как угодно, как отдельный объект или как массив объектов. Я могу легко его хранить, манипулировать, как хочу, без ограничений наследование приводит в верхнем примере.
В случае, когда эта идея не подходит для моего случая, я хочу получить доступ к «общим свойствам» в каком-то месте, в котором не уверен, какой T был установленк.
Это упрощенное объяснение того, что на самом деле происходит в моем приложении, я немного расширю его, чтобы объяснить, где это универсальное решение не работает для меня, и, наконец, задам мой вопрос.


Проблема и вопрос
Как упоминалось выше, приложение работает с 3 API, и все 3 API имеют различные структуры Root, и, конечно, многоразличных "подструктур" - чтобы назвать их. У меня есть одно место, один APIResponse объект в приложении, который восходит к пользовательской части приложения, в которой я извлекаю 1 читаемую ошибку из decoded value, decoded value, являющейся этой «подструктурой», являющейся любым из моих«конкретные объекты», Car, Dog, House, Phone.
Благодаря решению 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 я не могу этого сделать. Если я попытаюсь написать этот же код с использованием 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.

Решение, которое я ищу, - это решение, которое позволило бы мне различать Root объекты с помощью решения Generics, показанного выше, или решение, которое позволяет мне декодировать архитектуру совершенно другим способом, учитывая всеЯ написал, особенно возможность избегать «множественных» объектов

* Если JSON не проходит валидатор JSON, пожалуйста, не обращайте внимания, он был написан от руки только ради этого вопроса
** Если написанный код не запускается, пожалуйста, не обращайте внимания, это скорее архитектурный вопрос, чем компиляция некоторого фрагмента кода.

1 Ответ

2 голосов
/ 08 ноября 2019

Здесь вы ищете протокол.

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, а не Codable с пустыми encode методами. Тип не должен соответствовать Encodable, если он действительно не может быть закодирован.

...