Мне нравится использовать композицию и инъекцию зависимостей, но когда каждая сущность начинает инъектится несколькими зависимости, получается некое нагромождение.
Проект растет и приходится инъектить все больше зависимостей в объекты, рефакторить методы помногу раз, 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)
oxidmod
18.04.2017 17:15+1class AmazonImageProvider { init(container: Container) { self.storage = container.resolve(StorageProvider.self)! } }
Попахивает сервис локаторомJiLiZART
18.04.2017 17:23Ну, это он и есть, частный случай реализации IoC
oxidmod
18.04.2017 17:30+1Имхо, это антипаттерн, поскольку:
1. Привязывает бизнес-логику к инфраструктуре приложения (вы зависите от конкретного контейнера в худшем случае или от интерфейса в лучшем, но всеравно этому не место на уровне бизнесс-логики)
2. Вносите неявные зависимости между вашими сервисами. Чтобы однозначно понять от чего зависит ваш сервис недостаточно его контракта, нужно читать код
dima0o
19.04.2017 11:40В таком случае мы не сможем разделить реализацию компонентов ImageProvider и ArticleProvider
Deosis
Как это работает с зависимостями зависимостей?
Т.е. если ImageProvider понадобится StorageProvider?
JiLiZART
Написал небольшой gist как это можно решить https://gist.github.com/JiLiZART/d072b3cc9a8d181234f4f374bb5eda09, по сути нужно создавать отдельную структуру для
ImageProvider
.По правильному, используя например Swinject, это выглядело бы так: