В чем глобальная проблема паттерна Singleton
Что проверяют
Понимаете ли вы, что проблема синглтона не в единственности экземпляра, а в глобальном доступе к нему. Следом обычно спрашивают, как бы вы убрали синглтон из существующего кода и почему Apple сама пользуется URLSession.shared.
Как отвечать
Главное: синглтон это глобальная переменная с красивым именем. Из этого следует остальное:
- скрытые зависимости: по сигнатуре
init()не видно, что класс ходит вNetworkManager.shared. Зависимость спрятана в методах, и граф связей виден только при чтении всего кода; - общее изменяемое состояние: любой экран может поменять данные, которые читает другой, и порядок изменений трудно отследить;
- тесты: подменить
sharedна заглушку без хаков нельзя, состояние перетекает между тестами, и они начинают зависеть от порядка запуска; - многопоточность:
static let shared = Manager()в Swift инициализируется лениво и потокобезопасно, дубля не будет. Но это касается только создания, свойства внутри защищать всё равно вам.
Стоит сказать про Swift 6. В режиме строгой конкурентности компилятор отвергает static var как небезопасное глобальное состояние, а заодно и static let с не-Sendable типом. Приходится решать явно: пометить класс @MainActor, сделать его actor или обеспечить Sendable. Язык сам заставляет увидеть ту проблему, о которой спрашивают.
Что вместо: внедрение зависимостей через инициализатор с протоколом. Переходный шаг для большого проекта: init(api: APIClient = APIClient.shared), где синглтон остаётся значением по умолчанию, а тест передаёт заглушку. Дальше можно подключить контейнер или @Environment в SwiftUI.
Про Apple: URLSession.shared, FileManager.default, UserDefaults.standard оправданы, они обёртывают системный ресурс, который действительно один. Проблемы начинаются, когда синглтоном делают бизнес-логику.
Где спотыкаются
Главным минусом называют гонку при создании экземпляра. Для static let в Swift это неверно.
Говорят «синглтоны зло» и не могут назвать ни одного оправданного случая.
Не предлагают способа уйти от синглтона, кроме «переписать всё на DI». Вариант с параметром по умолчанию показывает, что вы делали это в живом коде.