Внедрение зависимостей бывает трех типов: через метод инициализации, через свойства и через любой другой метод. Библиотека поддерживает все 3 варианта внедрения зависимостей. Хорошим стилем считается внедрение зависимостей через метод инициализации. Другим способом стоит пользоваться в редких случаях, таких как - циклические зависимости или отсутствие возможности создавать объект самим.
Объявление внедрения через метод инициализации происходит при регистрации нового компонента. Рассмотрим пример:
/// объявляем классы и протоколы
protocol Engine {}
protocol Wheel {}
protocol Body {}
class Car {
private let engine: Engine
private let wheel: Wheel
private let body: Body
init(engine: Engine, wheel: Wheel, body: Body) {
self.engine = engine
self.wheel = wheel
self.body = body
}
}
/// регистрация в контейнер.
container.register(Car.init)
container.register(Car.init(engine:wheel:body:))
container.register {
return Car(engine: $0, wheel: $1, body: $2)
}
Все три записи регистрации компонента равносильны, но стоит рассмотреть отличия между ними:
Простой способ регистрации. Работает если у класса есть единственный метод инициализации. Таким способом стоит пользоваться в двух случаях:
- Класс активно меняется, и обновлять регистрацию на каждое изменение метода инициализации не хочется.
- Хочется упросить написание кода. Но это усложняет чтение кода, так как придется переходить в класс для просмотра зависимостей.
Предпочтительный способ регистрации. Работает в большинстве ситуаций, и не усложняет чтение кода. Такой способ регистрации не поддерживает модификаторы. Для этого есть третий.
Универсальный способ регистрации. Работает во всех ситуациях, легко читаем, и хорошо поддерживает auto completion. Но имеет недостаток, в виде необходимости писать цифры, что не всегда может быть удобным. Основное достоинство - поддерживает работу с модификаторами, а также позволяет внедрять объекты, не зарегистрированные в контейнере. Пример использования, с многими возможностями:
container.register {
/// Внедряем:
/// двигатель не зарегистрированный в DI контейнере
/// все колеса зарегистрированные в контейнере и соответствующие типу: Wheel
/// каркас, соответствующий тегу BMWBody
Car(engine: BMWEngine(), wheels: many($0), body: by(tag: BMWBody.self, $1))
}
Способ регистрации, на случай если вам нужно в качестве первого аргумента внедрить не просто зависимость, а зависимость с модификатором. Такое способ предполагается в первую очередь для использования для внедрения аргумента, но может быть использован и для других целей. Служит для упрощения и сокращения кода, если нужно внедрение с одной модификацией. Пример использования:
/// Внедряем:
/// двигатель не зарегистрированный в DI контейнере, и который нужно передать снаружи.
/// колеса и каркас, которые до этого где-то были зарегистрированы в DI контейнере.
container.register(Car.init) { arg($0) }
// Теперь можно создать машину, указав один аргумент, и он автоматически подставится при создании
let car: Car = container.resolve(args: BMWEngine())
}
Если внедрение через метод инициализации не подходит, то стоит использовать внедрение через свойства. Такое может быть по следующим причинам:
- Идеологическим - на проекте не принято внедрять через метод инициализации
- Историческим - уже написано много кода, который долго править
- Синтаксическим - наличие циклических зависимостей, или ViewController-ов создаваемых из xib/storyboard не дает возможности делать все с использованием методов инициализации.
- Временным - написать конструктор и прописать присвоении переменных занимает больше времени, чем написать свойство.
При этом внедрение через свойство достаточно богато на синтаксис. Давайте рассмотрим все варианты:
class Car {
var engine: Engine!
var wheels: [Wheel] = []
}
container.register(Car.init)
/// При создании зависимостей ручками
.injection { car in car.engine = BMWEngine() }
.injection { $0.engine = BMWEngine() }
/// При получении из DI контейнера
.injection { car, engine in car.engine = engine }
.injection { $0.engine = $1 }
.injection(\Car.engine)
.injection(\.engine)
/// с модификаторами
.injection { car wheel in car.wheels = many(wheel) }
.injection { $0.wheels = many($1) }
.injection(\Car.wheels) { wheel in many(wheel) }
.injection(\.wheels) { many($0) }
/// При необходимости указать другой тип
.injection { $0.engine = $1 as OtherEngine }
.injection(\Car.engine) { $0 as OtherEngine }
/// При наличии цикла все те же варианты, но с добавлением `cycle: true`
.injection(cycle: true) { $0.engine = $1 }
/// С указанием имени, все те же варианты, но с добавлением `name: "..."`
.injection(name: "BMW") { $0.engine = $1 } /// deprecated(Лучше использовать модификаторы)
Скорей всего можно придумать еще комбинации, но все они вытекают из этих вариантов, которые на самом деле распадаются на 3:
.injection(_ closure: @escaping (Impl) -> ())
Вариант, когда внедряемый объект создается на месте. Наиболее простой вариант, и редко используемый. Основное его использование или проставление общих констант (казалось бы, при чем тут DI?), или добавление в уже существующих проект DI. Второй вариант более интересен, так как если проект большой, а на нем нет DI, то скорей всего есть какой-то аналог. И данное внедрение хорошо подходит, для переписывания проекта частями..injection(name: String? = nil, cycle: Bool = false, _ closure: @escaping (Impl, Property) -> ())
Старый вариант внедрения зависимостей, до swift4.0. Позволяет внедрить любой объект зарегистрированный в DI контейнере. Для чего нужен каждый параметр:
name
- является устаревшим способом, замененным на модификаторы. Нужно в случае если для одного и того же типа, надо иметь несколько вариантов компонента. Например:
container.register(Car.init)
.as(Car.self, name: "BMW")
.injection { $0.engine = BMWEngine() }
container.register(Car.init)
.as(Car.self, name: "Eclipse")
.injection { $0.engine = EclipseEngine() }
В этом случае при внедрении, можно указать одно из указанных имен, и получить интересуемый экземпляр машины, но с разными двигателями.
!!! Причины отказа: строковые литералы не являются типо-безопасными, более того они подвержены опечаткам. На смену пришли тэги. Да в отличие от имени они требуют чуть больше действий, но они имеет намного больше возможностей: можно пользоваться при инициализации, типо-безопасны, проверка валидности на стадии валидации, наличие области видимости. Все эти плюсы, в моем понимании, перевешивают их единственный минус - очевидность использования.
cycle
- является указанием, что данный граф зависимостей имеет цикл, и его нужно разорвать. На один цикл достаточно одного указания данного факта - это будет точкой разрыва, чтобы инициализации не ушла в бесконечный цикл. Наличие циклов проверяется при валидации, и если вы забудете указать, то библиотека сообщит об этом и предложит в определенном цикле указать данный факт.
!!! Стоит учесть: Если при регистрации указывается много внедрений через свойства, то все они будут внедряться строго в указанном порядке, за исключением циклических внедрений - их момент внедрения сложно пред угадать. Единственное что можно сказать наверняка - циклические внедрения будут внедряться после всех не циклических и также по порядку.
closure
- метод описывающий способ внедрения. На вход принимает объект в который внедряемся и объект, который будет внедрен. Ваша задача присвоить объект, так как swift до 4 версии не умел это делать каким либо способом.
!!! Lifehack: Не рекомендуется, но на самом деле в closure
можно делать любые другие действия. Но лучше для этих целей использовать отдельный метод postInit
, который исполняется после всех внедрений.
injection(name: String? = nil, cycle: Bool = false, _ keyPath: ReferenceWritableKeyPath<Impl, P>, _ modificator: @escaping (Property) -> P)
Улучшенный вариант предыдущего доступный со swift4.0. улучшение касается как синтаксиса - он становится короче и понятней, так и возможностей: при таком внедрении свойство может иметь область видимости меньше, чем в прошлом случае:
class Car {
/// Прошлое способ не имеет возможности изменить свойство при таком модификаторе доступа
/// Начиная со swift4.0 благодаря keyPath достаточно только знать о свойстве, без возможности модифицировать
private(set) var engine: Engine!
}
Во всех остальных отношениях этот способ эквивалентен предыдущему. Стоит только уточнить про наличие еще одного параметра modificator
:
modificator
нужен для добавления модификаторов. В случае если нам нужно получить объект с использованием модификатора, к примеру many
, то это можно легко дописать в конце, например как это было сделано тут:
.injection(\.wheels) { many($0) }
Последний способ внедрения. Мало популярный, и мало отличающийся от внедрения через свойства, кроме как того что происходит внедрение нескольких зависимостей одновременно. Более того такой способ имеет меньший функционал.
container.register(Car.init)
.injection { $0.setup(engine: $1, wheels: many($2), body: $3) }
Как и с методом инициализации, такой способ не умеет внедрять по имени, и не поддерживает разрыв циклов.
После полной инициализации объекта, с внедрением всех зависимостей, в том числе и циклических, вызывается еще один метод postInit
. По умолчанию он отсутствует, но если вам нужно сделать какие-нибудь дополнительные методы после полной инициализации метода, вы можете его определить:
container.register(Car.init)
.injection(\.engine)
.injection(\.wheels) { many($0) }
.injection(\.body)
.postInit { car in
car.move(to: Moscow.location)
}