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