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

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

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

Проблема

Допустим, у вас есть объект, которому требуется поставщик изображений, поэтому вы пишете его init примерно так:

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

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

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

  • рефакторинг ваших звонков
  • добавить для них новые переменные
  • напишите один и тот же шаблон для каждого объекта

например через несколько месяцев этот объект может иметь 3 зависимости:

class FooViewModel {
  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 или Координаторе потока.

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

Мы хотим, чтобы обслуживание было простым, но при этом мы получали все преимущества прямой и простой инъекции кода.

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

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

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

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

Поменяйте местами {Dependency} на имя вашего объекта

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

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

В вашем контроллере приложения или координаторе потока (что бы ни использовалось для создания новых сущностей) вы можете хранить все зависимости в единой структуре контейнера:

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
    }
}

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

например a FooViewModel может потребоваться ImageProvider, и наши FlowCoordinator передают реальную AppDependency структуру, обработка типов Swift заботится только о предоставлении нам доступа к ImageProvider

Если в дальнейшем потребуется больше зависимостей, например зависимости от PersistanceProvider, единственное, что нам нужно изменить в нашей кодовой базе, - это настроить псевдонимы типов:

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

И мы закончили.

Такой подход дает следующие преимущества:

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

Поддержите мое письмо, став Patreon

Первоначально опубликовано на merowing.info.