Разработка приложений для iOS
Простая инъекция зависимостей с помощью оберток свойств в Swift
Другой подход к DI в Swift 5.1+ с Property Wrappers.
Если вы разработчик, то наверняка знаете, что внедрение зависимостей требуется в любом достойном проекте. Вы не можете жить без этого, поскольку это помогает сделать ваш проект тестируемым, помогает ослабить напряженность между классами и дает много других преимуществ.
Но также вы можете знать, что DI заставляет вас писать дополнительный код, а методы инициализации принимают до смешного огромное количество параметров. Представьте, что вам нужно создать ImageDownloader
, который имеет зависимости от NetworkManager
(для загрузки изображения), FileManager
(для кэширования изображения), NotificationCenter
(для публикации уведомления при загрузке изображения) и AnalyticsManager
(для регистрации в случае возникновения какой-либо ошибки). В этом случае у вас будет что-то вроде этого:
Ваши пользовательские классы, такие как
networkManager
,FileManager
иAnalyticsManager
, скрыты за протоколами, поэтому вы можете имитировать их в своих модульных тестах.
Хотя мы все, вероятно, привыкли писать этот код, это многовато.
Короче говоря, есть подход, который может облегчить вам жизнь! Спасибо, Property wrappers, отличная функция в Swift (5.1+), которая здесь пригодится.
Если вас это интересует, вот как может выглядеть ваш код:
Вам не нужно помещать все эти зависимости в init
метод вашего класса. И вам даже не нужно указывать тип для каждой переменной. Тем не менее, ваш код все еще поддается тестированию.
Итак, как нам это сделать? Давайте напишем код для реализации этого подхода.
Инъекция
Во-первых, нам нужен преобразователь, который будет обеспечивать правильную зависимость свойства. Вот как это будет выглядеть:
Если вы хотите проверить, как это работает, вам необходимо создать и реализовать эти классы и протоколы. Или вы можете просто использовать мой код:
Мы сохраним эти классы очень простыми. Теперь давайте реализуем оболочку свойств.
Property Wrapper
Создайте новый файл Swift с именем Injected.swift
. Вот код:
Если вы не знакомы с Property Wrappers в Swift 5.1, в этом нет ничего страшного:
- Мы создали
struct
; - Добавлено
@propertyWrapper
перед его объявлением; - Каждый объект Property Wrapper должен иметь
wrappedValue
. В нашем случае это универсальный типT
; init
получает один параметр:keyPath
переменной вInjectionResolver
;wrappedValue
получает значение из переменнойInjectionResolver
.
ImageDownloader
Теперь мы добавим метод downloadImage
к нашему ImageDownloader
:
Пора проверить, работает ли это. Добавьте этот код где-нибудь в свой проект:
Когда вы запустите этот код, вы получите следующий вывод консоли:
Will init ImageDownloader() NetworkManager init FileManager init AnalyticsManager init Will call downloadImage() real fetch write file logged: Image is downloaded Did call downloadImage()
Как видите, экземпляры NetworkManager
, FileManager
и AnalyticsManager
не инициализируются, пока вы не создадите экземпляр ImageDownloader()
. Это происходит потому, что ваши свойства в InjectionResolver
загружаются лениво. В результате они запускаются только при первом использовании.
После того, как вы позвонили downloadImage()
, вы получили три сообщения от ваших зависимостей: real fetch
, write file
, logged: Image is downloaded
. Но что произойдет, если вы захотите протестировать этот класс? Например, вы не хотите полагаться на свое сетевое соединение. Таким образом, вместо NetworkManager
вы должны использовать NetworkManagerMock
, который имитирует поведение NetworkManager
.
Добавьте InjectionResolver.shared.networkManager = NetworkManagerMock()
перед инициализацией ImageDownloader
:
Если вы запустите этот код еще раз, вы получите следующий результат:
NetworkManagerMock init Will init ImageDownloader() FileManager init AnalyticsManager init Will call downloadImage() mock fetch write file logged: Image is downloaded Did call downloadImage()
Как видите, теперь ImageDownloader
вводит NetworkManagerMock
. И вместо real fetch
мы видим mock fetch
.
Вы можете заменить все необходимые зависимости на mock перед тестированием класса (или структуры, или чего-либо еще) в вашем приложении.
Теперь вы можете тестировать свое приложение так же, как и раньше, но вам не нужны огромные методы инициализации для ваших классов.
Это может быть связано с тем, что один класс (InjectionResolver
) связан со всеми системами вашего приложения. С другой стороны, он инициализирует только необходимые объекты, и это поведение в основном такое же, как и с одиночными объектами. Но в этой реализации ваши синглтоны можно легко заменить фиктивными классами, и весь ваш код останется тестируемым.
Есть еще одна хорошая реализация DI с Property Wrappers, написанная Иларио Салатино, которая может быть вам интересна.
Идея с keyPaths взята из SwiftUI Property Wrapper @Environment
, который позволяет вам вводить значения Environment в представления SwiftUI:
@Environment(\.managedObjectContext) private var managedObjectContext
Если вы хотите узнать больше о SwiftUI, ознакомьтесь с моей серией руководств: