Должен ли я использовать опцию для свойств объектных моделей, которые будут обрабатываться 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
Есть три способа сделать это:
-
Всегда отправляйте все данные JSON и оставляйте свои свойства не факультативными.
-
Сделайте все свойства дополнительными.
-
Внесите все свойства, не являющиеся необязательными, и напишите свой собственный метод 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 решений:
- Отредактируйте мою
init func
из JSON
для object
, запустите с объектами по умолчанию для всех реквизитов (id = -1, email = ''
), затем прочитайте JSON с дополнительной проверкой. - Создайте новый
class/struct
для этого конкретного случая.
Ответ 8
Предпосылка:
Частичное представление является общей моделью в REST. Означает ли это, что все свойства в Swift должны быть дополнительными? Например, клиенту может потребоваться только список идентификаторов пользователей для представления. Означает ли это, что все остальные свойства (имя, адрес электронной почты и т.д.) Должны быть помечены как необязательные? Это хорошая практика в Свифт?
Свойства маркировки, необязательные в модели, указывают только на то, что ключ может появиться или не появится. Это позволяет читателю узнать некоторые вещи о модели в самом первом виде.
Если вы поддерживаете только одну общую модель для различных структур ответа API и сделать все свойства необязательны, что ли хорошая практика или нет очень спорно.
Я сделал это, и он кусает. Иногда это прекрасно, иногда это просто недостаточно ясно.
Сохранение одной модели для нескольких API-интерфейсов - это как создание одного ViewController
со многими элементами пользовательского интерфейса и в зависимости от конкретных случаев, определение того, какой элемент UI должен отображаться или нет.
Это увеличивает кривую обучения для новых разработчиков, поскольку в ней задействовано больше понимания.
Мои 2 цента на это:
Предполагая, что мы будем продвигать Swift Codable
для моделей кодирования/декодирования, я бы разложил их на отдельные модели, а не поддерживал общую модель со всеми параметрами и/или значениями по умолчанию.
Причинами моего решения являются:
-
Ясность разделения
- Каждая модель для конкретной цели
- Объем чистых пользовательских декодеров
- Полезно, когда json-структура нуждается в небольшой предварительной обработке
-
Рассмотрение дополнительных ключей API, которые могут появиться позже.
- Что делать, если этот API-интерфейс списка пользователей является единственным, требующим больше таких ключей, как, скажем, количество друзей или какая-либо другая статистика?
- Должен ли я продолжать загружать одну модель для поддержки различных случаев с дополнительными ключами, которые входят только в один ответ API, но не другой?
- Что делать, если третий API предназначен для получения пользовательской информации, но на этот раз с несколько иной целью? Должен ли я перегружать ту же модель с еще большим количеством клавиш?
- С одной моделью, по мере того, как проект продолжает развиваться, все может стать беспорядочным, поскольку ключевая доступность в настоящее время очень основана на API-интерфейсе. Со всеми возможными вариантами у нас будет много дополнительных привязок и, возможно, некоторых коротких коллапсов, которые мы могли бы избежать с помощью выделенных моделей в первую очередь.
- Написание модели дешево, но сохранение дел - нет.
Однако, если бы я был ленив, и у меня было сильное чувство, сумасшедшие изменения не появлялись впереди, я бы просто сделал все варианты ключей и нести связанные с этим расходы.
Ответ 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 для нестандартных значений по умолчанию.