← Назад к вопросам

Как оборачивали данные полученные с бэкенда?

1.0 Junior🔥 252 комментариев
#Архитектура и паттерны#Работа с сетью

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектурные подходы к обертыванию данных с бэкенда

В iOS-разработке обертывание данных, полученных с бэкенда — критически важный процесс, который напрямую влияет на стабильность приложения, поддерживаемость кода и производительность. За свою практику я применял различные подходы, эволюционировавшие вместе с экосистемой Apple и сообществом.

Основные принципы и уровни абстракции

Обычно я выстраиваю несколько слоев абстракции между сырыми данными (JSON) и бизнес-логикой приложения:

  1. Модели данных (Data Models) — чистые Swift-структуры или классы, отражающие доменную логику.
  2. Слой декодирования (Decoding Layer) — отвечает за трансформацию Data в модели, обычно с помощью Codable.
  3. Сеть (Network Layer) — инкапсулирует выполнение запросов.
  4. Репозитории/Сервисы (Repositories/Services) — предоставляют чистый API для бизнес-логики, скрывая источник данных (сеть, кэш, базу данных).

1. Использование Codable для декодирования JSON

Стандартный и наиболее эффективный способ — протоколы Codable (объединение Decodable и Encodable). Это позволяет декларировать преобразование JSON ↔ Model на уровне типа.

struct UserResponse: Decodable {
    let id: Int
    let name: String
    let email: String
    let profileImageURL: URL?
    let createdAt: Date

    // Кастомные ключи для преобразования snake_case в camelCase
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case email
        case profileImageURL = "profile_image_url"
        case createdAt = "created_at"
    }
}

Важные практики:

  • Использование проваливающихся инициализаторов или try? для обработки необязательных полей, чтобы сбой в одном поле не приводил к падению всего объекта.
  • Кастомное декодирование дат через JSONDecoder.DateDecodingStrategy (.iso8601, .secondsSince1970, кастомный formatter).
  • Для сложных полей (например, union-типов) — реализация init(from decoder: Decoder) throws вручную.

2. Промежуточные DTO (Data Transfer Objects)

Часто структура ответа API не идеально соответствует модели предметной области. В этом случае я ввожу промежуточный DTO (Data Transfer Object).

// Сетевой DTO
struct NetworkUserDTO: Decodable {
    let user: UserDTO
    let metadata: MetadataDTO
}

struct UserDTO: Decodable {
    let id: String
    let attributes: UserAttributesDTO
}

// Преобразование DTO в доменную модель
extension UserDTO {
    func toDomain() -> User? {
        guard let uuid = UUID(uuidString: id) else { return nil }
        return User(
            id: uuid,
            name: attributes.name,
            email: attributes.email
        )
    }
}

Преимущества: Слой DTO защищает доменные модели от изменений в API, позволяет обрабатывать сложные вложенные структуры и выполнять валидацию/нормализацию данных перед созданием финального объекта.

3. Обертка в общий контейнер ответа (Response Wrapper)

Многие API возвращают данные в стандартизированной обертке (с метаданными, пагинацией, статусом). Я создаю универсальные типы для таких случаев.

struct ApiResponse<Wrapped: Decodable>: Decodable {
    let status: String
    let data: Wrapped
    let pagination: Pagination?
}

struct ApiErrorResponse: Decodable {
    let code: Int
    let message: String
}

// Использование
let decoder = JSONDecoder()
let response = try decoder.decode(ApiResponse<[UserDTO]>.self, from: jsonData)
let users = response.data.compactMap { $0.toDomain() }

4. Работа с optional и default значениями

Явное указание поведения для отсутствующих или некорректных данных — залог устойчивости.

struct Product: Decodable {
    let title: String
    let price: Double
    let discountPrice: Double?
    let tags: [String]
    let isAvailable: Bool

    // Установка дефолтных значений через инициализатор
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        price = try container.decode(Double.self, forKey: .price)
        discountPrice = try container.decodeIfPresent(Double.self, forKey: .discountPrice)
        tags = try container.decodeIfPresent([String].self, forKey: .tags) ?? [] // Дефолтное значение
        isAvailable = try container.decodeIfPresent(Bool.self, forKey: .isAvailable) ?? true
    }
}

5. Интеграция со слоем постоянного хранения (Persistence)

Часто данные не только отображаются, но и кэшируются. Я использую Core Data или Realm как источник истины, а сетевые модели преобразую в управляемые объекты.

// Пример с Core Data
extension UserEntity {
    func update(with dto: UserDTO) {
        self.id = UUID(uuidString: dto.id) ?? UUID()
        self.name = dto.attributes.name
        self.email = dto.attributes.email
        // ... другие поля
    }
}

6. Обработка ошибок сети и декодирования

Все операции обернуты в комплексную обработку ошибок:

enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingFailed(underlyingError: Error)
    case serverError(statusCode: Int, message: String?)
}

func fetchData<T: Decodable>() async throws -> T {
    guard let url = URL(string: endpoint) else {
        throw NetworkError.invalidURL
    }
    
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse else {
        throw NetworkError.noData
    }
    
    guard (200...299).contains(httpResponse.statusCode) else {
        // Попытка декодировать структурированную ошибку от бэкенда
        let errorResponse = try? JSONDecoder().decode(ApiErrorResponse.self, from: data)
        throw NetworkError.serverError(
            statusCode: httpResponse.statusCode,
            message: errorResponse?.message
        )
    }
    
    do {
        return try JSONDecoder().decode(T.self, from: data)
    } catch {
        throw NetworkError.decodingFailed(underlyingError: error)
    }
}

Эволюция подходов

Раньше активно использовались библиотеки вроде ObjectMapper или SwiftyJSON, но с приходом Codable в Swift 4 необходимость в них практически отпала для базовых задач. Однако для сложных случаев с полиморфными типами или нестандартным парсингом иногда применяются кастомные решения или легковесные обертки над Codable.

Ключевой вывод: Главное — последовательное разделение ответственности. Сетевой слой должен возвращать готовые, валидные доменные модели, а не сырые словари или данные. Это позволяет бизнес-логике оставаться чистой, тестируемой и независимой от конкретной реализации бэкенда.