Должен ли я использовать опцию для свойств объектных моделей, которые будут обрабатываться JSON?

У моего приложения iOS есть довольно распространенная настройка: он делает HTTP-запросы на сервер API, который отвечает объектами JSON. Затем эти объекты JSON обрабатываются соответствующими объектами Swift.

Первоначально я разделял свойства на требуемые свойства и дополнительные свойства, в основном на основе требований к базе данных API-сервера. Например, id, email и name требуют полей, поэтому они используют необязательные типы. Другие могут быть NULL в базе данных, поэтому они являются необязательными типами.

class User {
  let id: Int
  let email: String
  let profile: String?
  let name: String
  let motive: String?
  let address: String?
  let profilePhotoUrl: String?
}

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

Например, на странице профиля пользователя все эти поля необходимы для правильного отображения представления. Поэтому ответ JSON будет включать все эти поля. Однако для представления, в котором перечислены имена пользователей, мне не нужна email или id, и ответ JSON, вероятно, не должен включать эти свойства. К сожалению, это приведет к ошибке и сбою приложения при анализе ответа JSON на объект Swift, так как приложение ожидает, что id, email, name всегда не ноль.

Я думаю об изменении всех свойств объектов Swift в опции, но это похоже на то, чтобы избавиться от всех преимуществ этой специфической для языка функции. Более того, мне придется писать еще много строк кода, чтобы развернуть все эти опции в другом месте приложения.

С другой стороны, объекты JSON по своей природе не очень совместимы со строгой статической типизацией и проверкой нуля Swift, поэтому было бы лучше просто принять это раздражение.

Должен ли я переходить к моделям со всеми свойствами в качестве опциональных? Или есть лучший способ? Я был бы признателен за любые комментарии здесь.

Ответы

Ответ 1

Есть три способа сделать это:

  1. Всегда отправляйте все данные JSON и оставляйте свои свойства не факультативными.

  2. Сделайте все свойства дополнительными.

  3. Внесите все свойства, не являющиеся необязательными, и напишите свой собственный метод init(from:) чтобы присвоить значения по умолчанию отсутствующим значениям, как описано в этом ответе.

Все это должно работать; который является "лучшим", основан на мнениях и, следовательно, выходит за рамки ответа на переполнение стека. Выберите тот, который наиболее удобен для вашей конкретной потребности.

Ответ 2

Первое, что нужно сделать, это спросить: должен ли элемент "представления, в котором перечислены имена пользователей", быть тем же самым объектом, что и объект модели за "страницей профиля пользователя"? Возможно нет. Возможно, вы должны создать модель специально для списка пользователей:

struct UserList: Decodable {

    struct Item: Decodable {
        var id: Int
        var name: String
    }

    var items: [Item]

}

(Хотя вопрос сказал, что ответ JSON может не включать id, он не выглядит как список пользователей без идентификаторов, что особенно полезно, поэтому я сделал это здесь.)

Если вы действительно хотите, чтобы они были одним и тем же объектом, то, возможно, вы хотите смоделировать пользователя как имеющие основные свойства, которые всегда отправляет сервер, и поле "details", которое может быть nil:

class User: Decodable {
    let id: Int
    let name: String
    let details: Details?

    struct Details: Decodable {
        var email: String
        var profile: String?
        var motive: String?
        var address: String?
        var profilePhotoUrl: String?
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        details = container.contains(.email) ? try Details(from: decoder) : nil
    }

    enum CodingKeys: String, CodingKey {
        case id
        case name

        case email // Used to detect presence of Details
    }
}

Обратите внимание, что я создаю Details, если они присутствуют, используя Details(from: decoder) вместо обычного container.decode(Details.self, forKey:.details). Я делаю это с помощью Details(from: decoder) так что свойства Details выходят из того же объекта JSON, что и свойства User, вместо того, чтобы требовать вложенный объект.

Ответ 3

Если сервер дает значение Null для других свойств, вы можете перейти к дополнительным файлам и безопасному распаку. Или во время развертывания вы можете назначить пустую строку для свойства, если значение равно nil

profile = jsonValue ?? ""

Другой случай, поскольку другие свойства являются строковыми типами данных, вы можете назначить значение по умолчанию в виде пустой строки

class User {
  let id: Int
  let email: String
  let profile: String = ""
  let name: String
  let motive: String = ""
  let address: String = ""
  let profilePhotoUrl: String = ""
}

Ответ 4

Я обычно делаю все некритические свойства необязательными, а затем имею инициализатор с ошибкой. Это позволяет мне лучше обрабатывать любые изменения в формате JSON или неработающие контракты API.

Например:

class User {
  let id: Int
  let email: String
  var profile: String?
  var name: String?
  var motive: String?
  var address: String?
  var profilePhotoUrl: String?
}

Это означает, что у меня никогда не будет объекта пользователя без идентификатора или электронной почты (пусть предполагается, что это те, которые всегда должны быть связаны с пользователем). Если я получаю полезную нагрузку JSON без идентификатора или электронной почты, инициализатор в классе User завершится с ошибкой и не создаст объект пользователя. Затем я обрабатываю ошибки для неудачных инициализаторов.

Я бы предпочел бы быстрый класс с дополнительными свойствами, чем пучок свойств с пустым строковым значением.

Ответ 5

Да, вы должны использовать опцию, если свойство не требуется в API, и если вы хотите, чтобы какое-то значение в обязательном свойстве присваивало пустое значение:

class User {
  let id: Int?
  let email: String? = ""
  let profile: String?
  let name: String? = ""
  let motive: String?
  let address: String?
  let profilePhotoUrl: String?
}

Ответ 6

Я рекомендую сохранить все non-scalar properties как необязательные, scalar как необязательные (с некоторыми исключениями) путем назначения значения по умолчанию и наборов с пустым массивом. например,

class User {
    var id: Int = 0
    var name: String?
    var friends: [User] = []
}

Это гарантирует вам бесплатное приложение, независимо от того, что происходит с сервером. Я бы предпочел ненормальное поведение над крахом.

Ответ 7

По моему мнению, я выберу 1 из 2 решений:

  1. Отредактируйте мою init func из JSON для object, запустите с объектами по умолчанию для всех реквизитов (id = -1, email = ''), затем прочитайте JSON с дополнительной проверкой.
  2. Создайте новый class/struct для этого конкретного случая.

Ответ 8

Предпосылка:

Частичное представление является общей моделью в REST. Означает ли это, что все свойства в Swift должны быть дополнительными? Например, клиенту может потребоваться только список идентификаторов пользователей для представления. Означает ли это, что все остальные свойства (имя, адрес электронной почты и т.д.) Должны быть помечены как необязательные? Это хорошая практика в Свифт?

Свойства маркировки, необязательные в модели, указывают только на то, что ключ может появиться или не появится. Это позволяет читателю узнать некоторые вещи о модели в самом первом виде.
Если вы поддерживаете только одну общую модель для различных структур ответа API и сделать все свойства необязательны, что ли хорошая практика или нет очень спорно.
Я сделал это, и он кусает. Иногда это прекрасно, иногда это просто недостаточно ясно.

Сохранение одной модели для нескольких API-интерфейсов - это как создание одного ViewController со многими элементами пользовательского интерфейса и в зависимости от конкретных случаев, определение того, какой элемент UI должен отображаться или нет.
Это увеличивает кривую обучения для новых разработчиков, поскольку в ней задействовано больше понимания.


Мои 2 цента на это:

Предполагая, что мы будем продвигать Swift Codable для моделей кодирования/декодирования, я бы разложил их на отдельные модели, а не поддерживал общую модель со всеми параметрами и/или значениями по умолчанию.

Причинами моего решения являются:

  1. Ясность разделения

    • Каждая модель для конкретной цели
    • Объем чистых пользовательских декодеров
      • Полезно, когда json-структура нуждается в небольшой предварительной обработке
  2. Рассмотрение дополнительных ключей API, которые могут появиться позже.

    • Что делать, если этот API-интерфейс списка пользователей является единственным, требующим больше таких ключей, как, скажем, количество друзей или какая-либо другая статистика?
      • Должен ли я продолжать загружать одну модель для поддержки различных случаев с дополнительными ключами, которые входят только в один ответ API, но не другой?
      • Что делать, если третий API предназначен для получения пользовательской информации, но на этот раз с несколько иной целью? Должен ли я перегружать ту же модель с еще большим количеством клавиш?
    • С одной моделью, по мере того, как проект продолжает развиваться, все может стать беспорядочным, поскольку ключевая доступность в настоящее время очень основана на API-интерфейсе. Со всеми возможными вариантами у нас будет много дополнительных привязок и, возможно, некоторых коротких коллапсов, которые мы могли бы избежать с помощью выделенных моделей в первую очередь.
  3. Написание модели дешево, но сохранение дел - нет.

Однако, если бы я был ленив, и у меня было сильное чувство, сумасшедшие изменения не появлялись впереди, я бы просто сделал все варианты ключей и нести связанные с этим расходы.

Ответ 9

Это зависит от того, как вы обрабатываете свои данные. Если вы обрабатываете свои данные с помощью класса "Codable", вам нужно написать собственный декодер, чтобы создать исключение, когда вы не получите определенных ожидаемых значений. Вот так:

 class User: Codable {
    let id: Int
    let email: String
    let profile: String?
    let name: String
    let motive: String?
    let address: String?
    let profilePhotoUrl: String?

     //other methods (such as init, encoder, and decoder) need to be added below.
    }

Поскольку я знаю, что мне нужно будет вернуть ошибку, если я не получу минимально необходимые параметры, вам понадобится что-то вроде перечисления Error:

    enum UserCodableError: Error {
         case missingNeededParameters
         //and so on with more cases
    }

Вы должны использовать ключи для кодирования, чтобы сохранить совместимость с сервером. Способ сделать это внутри пользовательского объекта будет таким:

    fileprivate enum CodingKeys: String, CodingKey {
       case id = "YOUR JSON SERVER KEYS GO HERE"
       case email
       case profile
       case name
       case motive
       case address
       case profilePhotoUrl
    }

Затем вам нужно написать свой декодер. Способ сделать это будет следующим:

    required init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    guard let id = try? values.decode(Int.self, forKey: .id), let email = try? values.decode(String.self, forKey: .email), let name = try? values.decode(String.self, forKey: .name) else {
        throw UserCodableError.missingNeededParameters
    }

    self.id = id
    self.email = email
    self.name = name

    //now simply try to decode the optionals
    self.profile = try? values.decode(String.self, forKey: .profile)
    self.motive = try? values.decode(String.self, forKey: .motive)
    self.address = try? values.decode(String.self, forKey: .address)
    self.profilePhotoUrl = try? values.decode(String.self, forKey: .profilePhotoUrl)
}

ЧТО-ТО, ЧТОБЫ ЗАМЕЧАТЬ: Вы должны написать свой собственный кодировщик, чтобы оставаться последовательным.

Все это может пойти, просто к приятному заявлению о вызове:

    if let user = try? JSONDecoder().decode(User.self, from: jsonData) {
        //do stuff with user
    }

Это, вероятно, самый безопасный, быстрый и ориентированный на большинство способ решения этой проблемы.

Ответ 10

Я бы предпочел использовать отказоустойчивый инициализатор в своем опрятном сравнении с другими параметрами.

Поэтому сохраните требуемые свойства как необязательные и создайте объект, только если они присутствуют в ответе (вы можете использовать if-let или gaurd-let, чтобы проверить это в ответ), иначе не удалось создать объект.

Используя этот подход, мы избегаем делать необязательные варианты как дополнительные и иметь боль, чтобы обрабатывать их во всей программе.

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

Ответ 11

Я предпочел бы дополнительные свойства, потому что вы не можете обещать значения JSON, чтобы быть там все время, и любое изменение имени свойства ответа приведет к сбою вашего приложения.

Если вы не используете необязательные значения, вам нужно управлять параметрами во время разбора и добавлять значение по умолчанию, если вы хотите использовать приложение, не требующее краха. И вы не знаете, была ли это ниль или пустая строка с сервера.

Дополнительные ценности - ваши лучшие друзья.

object mapper для изменяемых и не изменяемых свойств.

realm-swift для нестандартных значений по умолчанию.