Композиция протоколов для инъекции зависимостей

от автора

Мне нравится использовать композицию и инъекцию зависимостей, но когда каждая сущность начинает инъектится несколькими зависимости, получается некое нагромождение.

Проект растет и приходится инъектить все больше зависимостей в объекты, рефакторить методы помногу раз, Xcode не особо с этим помогает, как мы знаем.

Но есть более управляемый способ.

Статья будет полезна тем, кто только начал использовать DI или вообще его не использует, и еще не особо знаком с IoC. Подход применим и к другим языкам, но т.к автор пишет на Swift, все примеры будут на нем (прим. пер.)

Проблема

Предположим, есть объект, которому вдруг становится нужен провайдер ImageProvider, напишем что-то вроде этого:

class FooCoordinator {   let imageProvider: ImageProvider   init(..., imageProvider: ImageProvider)   ///... }

Довольно просто и удобно + это позволит подменить провайдер в тестах.

По мере роста кодовой базы, появляется все больше зависимостей, каждая из которых заставляет:

  • рефакторить место, где она используется
  • добавлять новую переменную
  • писать некоторое количество бойлерплейта

например, по прошествии нескольких месяцев у объекта может появится 3 зависимости:

class FooCoordinator {   let imageProvider: ImageProvider   let articleProvider: ArticleProvider   let persistanceProvider: PersistanceProvider    init(..., imageProvider: ImageProvider, articleProvider: ArticleProvider, persistanceProvider: PersistanceProvider) {       self.imageProvider = imageProvider       self.articleProvider = articleProvider       self.persistanceProvider = persistanceProvider       ///...   }   ///... }

Поскольку, обычно в проекте больше чем один класс, то тот же самый паттерн повторяется множество раз.

И надо не забывать, что нужно где-то хранить ссылки на все эти зависимости, например, в AppController или Flow Coordinator.

Такой подход неминуемо приводит к боли. Боль может мотивировать использовать не совсем правильные решения, Синглтоны например.

Но нам же нужна простая и легкая поддержка кода, со всеми преимуществами инъекции кода.

Альтернатива

Мы можем использовать композицию протоколов (интерфейсов), чтобы повысить читаемость и качество обслуживания кода.

Давайте опишем базовый контейнер протокола для любой зависимости которая у нас будет:

protocol Has{Dependency} {     var {dependency}: {Dependency} { get } }

Меняем {Dependency} на имя объекта

Например для ImageProvider это будет выглядеть так:

protocol HasImageProvider {     var imageProvider: ImageProvider { get } }

Swift позволяет составлять композицию из протоколов используя оператор &, это означает, что наши сущности теперь могут содержать просто одно хранилище зависимостей:

class FooCoordinator {     typealias Dependencies = HasImageProvider & HasArticleProvider      let dependencies: Dependencies      init(..., dependencies: Dependencies) }

Теперь в AppController или Flow Coordinator (Или что там используется для создания сущностей) можно спрятать все зависимости под одним контейнером в виде структуры:

struct AppDependency: HasImageProvider, HasArticleProvider, HasPersistanceProvider {   let imageProvider: ImageProvider   let articleProvider: ArticlesProvider   let persistanceProvider: PersistenceProvider }

И все зависимости приложения теперь будут хранится в контейнере, который не имеет какой либо логики или чего-то магического, просто структура.

Этот подход повышает читабельность, так же все зависимости хранятся вместе, но что более важно — это то, что код конфигурации всегда один и тот же, не зависимо какие из зависимостей нужны вашему объекту:

class FlowCoordinator {     let dependencies: AppDependency      func configureViewController(vc: ViewController) {         vc.dependencies = dependencies     } }

Каждый объект описывает только те зависимости которые ему реально нужны, и он получит только их.

Например, если нашему FooCoordinator понадобится ImageProvider, то он прокинет AppDependency структуру, а проверка типов в Swift обеспечит доступ только к ImageProvider

Если в дальнейшем понадобится больше зависимостей, например PersistanceProvider, то просто нужно добавить её в наш typealias:

class FooCoordinator {     typealias Dependencies = HasImageProvider & HasArticleProvider & HasPersistanceProvider }

На этом всё.

У такого подхода есть ряд преимуществ:

  • Зависимости четко определены и всегда консистентны, в любом объекте, по всему проекту
  • Когда зависимости объекта меняются, нужно поменять только typealias
  • Не нужно трогать ни инициализатор ни функции конфигурации.
  • Любой из объектов, благодаря системе проверки типов в Swift, получает только те зависимости, которые ему нужны.

ссылка на оригинал статьи https://habrahabr.ru/post/326712/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *