Разработка приложений для 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, ознакомьтесь с моей серией руководств: