Программирование SwiftОбработка ошибокРазработчик iOS среднего уровня

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

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

При этом признак «повторить можно» не означает «повторить всегда»: политика должна учитывать идемпотентность операции, лимит попыток и задержку между ними.

Исторический контекст

Механизм throws сообщает о неуспешном завершении, но сам по себе не задаёт смысл ошибки. Протокол Error также не навязывает универсальные поля вроде кода, категории или признака повторяемости.

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

Постановка проблемы

Слой синхронизации может получить тайм-аут, отказ в авторизации или конфликт версии данных. Если определять повтор по тексту ошибки, изменение локализации, формулировки или версии библиотеки сломает поведение приложения.

Без явной семантики возможны две противоположные ошибки: приложение будет бесконечно повторять необратимый сбой или прекратит попытки при временной проблеме. Особенно опасен автоматический повтор неидемпотентной операции, например повторное создание платежа.

Подробное решение

Добавьте в доменный тип ошибки свойство, описывающее возможность автоматического повтора. Например:

import Foundation enum SyncError: Error { case timeout case unauthorized case conflict } extension SyncError { var isRetryable: Bool { switch self { case .timeout: true case .unauthorized, .conflict: false } } } func canRetry(_ error: Error) -> Bool { (error as? SyncError)?.isRetryable == true }

Здесь проверяется не строка, а структурированная семантика доменной ошибки. Ошибка нижнего уровня сначала преобразуется в SyncError, после чего политика синхронизации работает только с доменной моделью.

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

Если одной булевой характеристики недостаточно, вместо неё можно моделировать политику: например, never, afterDelay или afterRefreshToken. Это точнее, но увеличивает связанность модели с механизмом выполнения повторов, поэтому простое свойство предпочтительнее, пока требований немного.

Для библиотеки с несколькими независимыми типами ошибок возможен протокол с таким свойством. Однако приведение к протоколу через as? требует, чтобы каждый relevantный тип явно реализовал контракт; перечисление удобнее, когда ошибки принадлежат одному домену и набор причин контролируется централизованно.

Ситуация из практики

Сервис синхронизации получает ошибку HTTP. Вариант с проверкой текста или числового кода транспорта прост, но протекает деталями HTTP в бизнес-логику и становится хрупким при смене клиента. Вариант с повтором всех сетевых ошибок проще всего реализовать, однако он опасен для ошибок авторизации, конфликтов и неидемпотентных запросов.

Выбранное решение — преобразовать транспортную ошибку в SyncError, хранить в доменном типе категорию повторяемости, а сам повтор выполнять отдельной политикой. В результате замена сетевой библиотеки не меняет код синхронизации, временные сбои обрабатываются автоматически, а необратимые причины не приводят к опасному циклу повторов.

Что кандидаты часто упускают

1. Достаточно ли свойства isRetryable, чтобы безопасно повторить любую такую ошибку?

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

2. Где должен находиться лимит попыток и интервал между ними?

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

3. Нужно ли помечать как повторяемые все тайм-ауты?

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