# iOS-Инкубатор. Нюансы и антипаттерны DI. Что не так с Service Locator? 15 ноября 2022 ## Одиночество Посмотрите на этот скриншот, мы видим, что тут вызывается в запись в лог через одиночку. И как видно в коде масса таких мест, обратите внимание на мини-карту. А теперь представьте, что будет при необходимости замены системы логирования. ![](https://i.imgur.com/K01lmj8.png) - Нарушает _принцип единственной ответственности класса_. - Маскирует плохой дизайн. - Проблемы мультипоточности. - Проблемы при юнит-тестировании. Зависимость обычного класса от синглтона не видна в публичном контракте класса! Одиночку нужно передавать как зависимость через DI: ```swift /// Сборщик модуля Actualization final class ActualizationAssembly: IActualizationAssembly { func makeModule(coordinator: IMainCoordinator) -> ActualizationViewController { let viewController = ActualizationViewController() viewController.presenter = ActualizationPresenter( coordinator: coordinator, hardwareHelper: ApplicationCoreServicesContainer.shared.hardwareHelper, appGroupManager: ApplicationCoreServicesContainer.shared.appGroupManager, taskManager: ApplicationCoreServicesContainer.taskManager, logger: Logger.shared, dialogController: ApplicationCoreServicesContainer.shared.dialogController, bannerController: ApplicationCoreServicesContainer.shared.bannerController ) return viewController } } ``` И еще лучше, закрывать зависимости протоколами: ```swift init( coordinator: IMainCoordinator, hardwareHelper: IHardwareHelper, appGroupManager: SMAppGroupManager, taskManager: ITaskManager, logger: ILogger, dialogController: IDialogController, bannerController: IBannerController ) ``` Но ни в коем случае не делать со значениями по умолчанию, так как это тоже не видно в публичном контракте класса: ```swift init( coordinator: IMainCoordinator, hardwareHelper: IHardwareHelper = ApplicationCoreServicesContainer.shared.hardwareHelper, appGroupManager: SMAppGroupManager = ApplicationCoreServicesContainer.shared.appGroupManager, taskManager: ITaskManager = ApplicationCoreServicesContainer.taskManager, logger: ILogger = Logger.shared, dialogController: IDialogController = ApplicationCoreServicesContainer.shared.dialogController, bannerController: IBannerController = ApplicationCoreServicesContainer.shared.bannerController ) ``` Используй Одиночку только при наличии веских аргументов для наличия единственного экземпляра класса. ## DI Container Давайте рассмотрим одну из реализаций DI контейнера, который поддерживает кэширование: ```swift /// Контейнер зависимостей: сервисов или мэнеджеров final class DIContainer: IDIContainer { // MARK: - Private Properties private var cachedServices: [String: Any] = [:] // MARK: - IDIContainer implementation func resolve<T>(_ make: @autoclosure () -> T, onMake: ((T) -> Void)? = nil) -> T { if let cachedService = getCachedService(T.self) { return cachedService } else { let newService = make() onMake?(newService) cache(newService) return newService } } } // MARK: - Private Methods private extension DIContainer { func cache<T>(_ newService: T) { cachedServices[String(describing: T.self)] = newService } func getCachedService<T>(_: T.Type) -> T? { let key = String(describing: T.self) if let service = cachedServices[key] as? T { return service } else { return nil } } } ``` ```swift // Assembly для зависимостей приложения protocol IAppDependenciesAssembly { /// Сервис генерации ссылок на изображения товаров func resolve(_: IImageUrlGenerator.Protocol) -> IImageUrlGenerator /// Сервис для хранения настроек func resolve(_: IUserDefaultsService.Protocol) -> IUserDefaultsService /// Менеджер событий приложения func resolve(_: IAppEventsManager.Protocol) -> IAppEventsManager /// Обработчик событий приложения func resolve(_: IAppEventsHandler.Protocol) -> IAppEventsHandler } extension IAppDependenciesAssembly where Self: IAssembly { func resolve(_: IImageUrlGenerator.Protocol) -> IImageUrlGenerator { let imageUrlGenerator = ImageUrlGenerator() return container.resolve(imageUrlGenerator) } func resolve(_: IUserDefaultsService.Protocol) -> IUserDefaultsService { return container.resolve(UserDefaultsService()) } func resolve(_: IAppEventsHandler.Protocol) -> IAppEventsHandler { container.resolve(AppEventsManager()) as AppEventsManager } func resolve(_: IAppEventsManager.Protocol) -> IAppEventsManager { container.resolve(AppEventsManager()) as AppEventsManager } } ``` Пример использования такого контейнера: ```swift final class SearchScreenAssembly: ISearchScreenAssembly { func makeModule( assembly: IAssembly, mainFlowAssembly: IMainFlowAssembly, coordinator: IMainCoordinator ) -> UIViewController { let presenter = SearchPresenter( wareService: mainFlowAssembly.resolve(IWareService.self), hardwareService: assembly.resolve(IHardwareService.self), appContext: mainFlowAssembly.resolve(IAppContext.self), imageUrlGenerator: assembly.resolve(IImageUrlGenerator.self) ) let viewController = SearchViewController.instantiate() viewController.setup( appContext: mainFlowAssembly.resolve(IAppContext.self), coordinator: coordinator, presenter: presenter ) presenter.setup(view: viewController) return viewController } } ``` Я вляется ли DI Container антипаттерном? - Да, если его передавать в качестве зависимости, откуда потом класс сам добывает свои зависимости. - Да, ясли он не обеспечивает безопасности типов. - Нет, если его применять внутри Root Composition. ## Антипаттерн ServiceLocator Основная идея **Service Locator** заключается в том, что в программе имеется объект, знающий, как получить все зависимости (сервисы), которые могут потребоваться. ```swift class ServiceLocator { var services: [Any: Any]() func register<T>(_: T.Self) { ... } func get<T>() -> T { ... } } ``` ![](https://i.imgur.com/ELwSues.png) Этот шаблон используется для регистрации сервисов для какого-то класса. Вы регистрируете их в словаре и теперь, можно выдать любой класс. Но при таком подходе теряется безопасность типов, так как теперь на запрос сервиса разработчик получает неизвестно что. Лучше используйте абстрактную фабрику. ## Антипаттерн Context Context Anti-pattern – это глобальные переменные среды выполнения приложения. Значительно похож на синглтон, но с изменяемым состоянием. Он имеет общий глобальный экземпляр, который может быть заменен в процессе работы. Описание нашего контекста: ```swift struct Context { /// login текущего авторизованного пользователя. var userName: () -> String = { "" } /// Номер магазина в котором работает устройство. var shopNumber: () -> Int = { 0 } /// Торговая сеть в котором работает устройство. var shopRetailer: () -> Retailer = { .some } } ``` Объявление в AppDelegate.swift: ```swift #if DEBUG var context = Context() #else let context = Context() #endif ``` Замена на необходимые значения для разработки: ```swift ... context.userName = { "leonovka" } context.shopNumber = { 1 } ... ``` Его использование проникает глубоко в код, как и одиночка. Затрудняет тестирование. ![](https://i.imgur.com/1RC8K5J.png) ## @Environment как Антипаттерн Это Property Wrappers от _SwiftUI_. ```swift import SwiftUI struct SomeView: View { let title: String ... @Environment var time: Date ... } ``` Такой код скомпилируется даже без установки time, но во время выполнение в этих условиях произойдет падение. Мы теряем безопасность типов. ## Стрелка Антипаттерн Стрелка - это когда в коде много Bool флажков и мы вынуждены их все проверять, изза чего наш код выглядит как стрелки. Также это бывает, когда у нас асинхронные запросы и они вкладываются друг в друга. ![](https://i.imgur.com/8PvGQGq.png) Решение - Не писать так. Стрелку можно развернуть из середины, вынося код в отдельные методы. - Можно применяя протоколы подсовывать нужную реализацию нашей бизнес-логики. Например использовать шаблон Стратегия. ## Полезные ссылки - https://www.pointfree.co/blog/posts/21-how-to-control-the-world - https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ - https://blog.ploeh.dk/2010/11/01/PatternRecognitionAbstractFactoryorServiceLocator/