Как решить проблему тесной связанности Singleton с помощью протоколов и Dependency Injection
Что проверяют
Понимаете ли вы, что проблема не в самом shared, а в том, что класс сам достаёт зависимость из глобального состояния. Следом обычно просят показать, как это протестировать и как внедрять зависимости в UIViewController или SwiftUI-представление.
Как отвечать
Решение складывается из двух частей:
- протокол как контракт:
protocol AnalyticsTracking { func track(_ event: Event) }. Потребителю нужен только этот набор методов, а не весьAnalyticsService. Протокол держите узким, под задачу потребителя; - внедрение через инициализатор:
init(tracker: AnalyticsTracking). Класс больше не знает, откуда пришла реализация: в приложении это синглтон, в тесте мок.
Для постепенного перехода удобен аргумент по умолчанию: init(tracker: AnalyticsTracking = AnalyticsService.shared). Старые места вызова компилируются без изменений, а тесты уже подставляют своё. Сам синглтон может остаться: один экземпляр на приложение это нормально, плохо только скрытое обращение к нему изнутри.
Собирать зависимости лучше в одной точке, composition root: обычно это координатор или фабрика модуля. Для экранов из сториборда, где свой init не задать, есть instantiateViewController(identifier:creator:) (iOS 13+) или внедрение через свойство. В SwiftUI зависимости передают через инициализатор или @Environment с собственным ключом. Контейнеры вроде Swinject или Factory уместны в большом проекте, но это инструмент, а не суть подхода.
Swift 6 добавляет ещё довод: синглтон с изменяемым состоянием и типом без Sendable компилятор в строгом режиме параллелизма не пропустит, его придётся сделать actor или привязать к @MainActor.
Где спотыкаются
Вводят протокол, но внутри класса по-прежнему пишут AnalyticsService.shared.track(...). Связанность осталась, просто добавился лишний файл.
Делают один огромный протокол, копию всего класса. Мок приходится писать на двадцать методов ради одного теста.
Используют DI-контейнер как глобальный реестр и вызывают container.resolve() в любом месте. Это тот же синглтон, только зависимость спрятана ещё глубже.