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


Проект растет и приходится инъектить все больше зависимостей в объекты, рефакторить методы помногу раз, 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, получает только те зависимости, которые ему нужны.
Поделиться с друзьями
-->

Комментарии (8)


  1. Deosis
    18.04.2017 06:33

    Как это работает с зависимостями зависимостей?
    Т.е. если ImageProvider понадобится StorageProvider?


    1. JiLiZART
      18.04.2017 12:02

      Написал небольшой gist как это можно решить https://gist.github.com/JiLiZART/d072b3cc9a8d181234f4f374bb5eda09, по сути нужно создавать отдельную структуру для ImageProvider.


      По правильному, используя например Swinject, это выглядело бы так:


      class AmazonImageProvider {
          init(container: Container) {
              self.storage = container.resolve(StorageProvider.self)!
          }
      }
      
      let container = Container()
      
      container.register(StorageProvider.self) { _ in FileStorageProvider() }
      container.register(ImageProvider.self) { c in
          AmazonImageProvider(container: c)
      }


  1. oxidmod
    18.04.2017 17:15
    +1

    class AmazonImageProvider {
        init(container: Container) {
            self.storage = container.resolve(StorageProvider.self)!
        }
    }
    



    Попахивает сервис локатором


    1. JiLiZART
      18.04.2017 17:23

      Ну, это он и есть, частный случай реализации IoC


      1. oxidmod
        18.04.2017 17:30
        +1

        Имхо, это антипаттерн, поскольку:
        1. Привязывает бизнес-логику к инфраструктуре приложения (вы зависите от конкретного контейнера в худшем случае или от интерфейса в лучшем, но всеравно этому не место на уровне бизнесс-логики)
        2. Вносите неявные зависимости между вашими сервисами. Чтобы однозначно понять от чего зависит ваш сервис недостаточно его контракта, нужно читать код


  1. dima0o
    19.04.2017 11:40

    В таком случае мы не сможем разделить реализацию компонентов ImageProvider и ArticleProvider


  1. 4FreeD
    19.04.2017 13:36
    -1

    Желательно добавить источник:
    http://merowing.info/2017/04/using-protocol-compositon-for-dependency-injection/


    1. JiLiZART
      19.04.2017 13:38

      т.е вот эта плашка вам ни о чем не говорит? image