Почему сервис стоит создавать глобально, а не локально в методе в iOS
Что проверяют
Понимаете ли вы время жизни объектов в ARC и что происходит с сервисом, созданным прямо в методе, когда метод завершился. Это вопрос не про красоту архитектуры, а про конкретный баг: локально созданный клиент успевает умереть раньше, чем придёт ответ.
Как отвечать
Назвать три причины по убыванию важности.
- Время жизни. Объект, на который держит ссылку только локальная переменная, освобождается на выходе из метода. Если сервис работает через делегата (
URLSessionс делегатом, менеджер геопозиции, монитор сети), удерживать делегата некому, и колбэк не придёт. Проявляется плавающе: под отладчиком иногда успевает. - Состояние. У сервиса есть то, что имеет смысл переиспользовать: кэш, токен, очередь, соединения внутри
URLSession. Создание в методе обнуляет это на каждый вызов, и вместо переиспользования соединения получаются новые рукопожатия TCP и TLS. - Конфигурация. Заголовки, таймауты, перехватчики задаются один раз при сборке, а не повторяются в каждом методе.
Но правильная формулировка не «глобально», а «один экземпляр, который кто-то держит, и его передают явно». Сервис лежит свойством у владельца и приходит через инициализатор:
final class ProfileViewModel {
private let api: ProfileServicing
init(api: ProfileServicing) { self.api = api }
}Так экземпляр живёт ровно столько, сколько живёт владелец, а в тестах на его место встаёт заглушка.
Где спотыкаются
Ставят знак равенства между «глобально» и static let shared. Синглтон это глобальное изменяемое состояние: скрытая зависимость, непредсказуемый порядок инициализации, общий кэш, который течёт из теста в тест.
Говорят, что глобальный сервис упрощает тестирование. Упрощают протокол и внедрение зависимости, а не глобальность. Сервис, вшитый в код обращением к shared, подменить нечем.
Третье: не думают про потоки. Один экземпляр на всё приложение означает конкурентный доступ к его состоянию из разных очередей, так что изменяемые поля нужно закрывать актором или собственной очередью.