Один из способов внедрения был разобран в предыдущей главе, а именно внедрение через метод инициализации. Несмотря на то, что данный метод внедрения является самым простым и наилучшим, не стоит упускать из виду и два других способа: через свойства или через методы. Эти два способа позволяют внедрять зависимости в объекты, которые были уже созданы кем-то другим, в частности ViewController-ом и создавать циклические зависимости. Помимо этого одним из плюсов внедрение через свойство является - возможность указывать имя как модификатор для получения объекта, в отличие от других способов.
Второе по частоте использования - это внедрение через свойства.
Почему этот способ столь популярен у Apple систем? Потому что в части случаев ViewController-ы создается из storyboard, а значит, библиотека не имеет доступ к методу инициализации.
Внедрение через свойства обладает недостаток - в этом случае эти свойства становятся изменяемыми и доступные другим классов. То есть нарушается инкапсуляция, что не есть хорошо.
Давайте рассмотрим, как это делается с помощью библиотеки. Для внедрения через свойство, как и через методы, есть ключевая функция injection при регистрации компонента:
container.register(Home.init)
.injection { home, dog in home.animals.append(dog as Dog) }
.injection { $0.animals.append($1 as Cat) }
.injection { $0.hamster = $1 }Как видим ничего сложного во внедрение через свойства нету, более того у него работает ассист, который будет вам помогать. У этого способа активно используется автоматический вывод типов, что позволяет писать короткий код, к которому быстро привыкаешь, и даже без указания имён переменных становится все понятно. Более того указание типа почти не нужно, как в 3 варианте - он может быть выведен автоматически. Порядок внедрения совпадает с порядком объявления, за исключением наличия циклических ссылок, о которых речь пойдет ниже.
Давайте посмотрим на более сложные ситуации - если нам надо получить объект по имени, тегу, или множество всех, то есть когда используются модификаторы:
container.register(Home.init)
.injection(name: "dog") { $0.animals.append($1 as Animal) }
.injection { $0.animals.append(contentsOf: many($0) as [Animal]) }
.injection { $0.hamster = by(tag: Hamster.self, on: $1) as Animal }Во всех 3 случаях мы создали экземпляр или экземпляры класса Animal и что-то с ними сделали. Но в первом случае мы получили экземпляр с использованием имени, во втором мы получи все зарегистрированные в приложении классы, которые использует в качестве сервиса тип Animal с любым видом модификаторов, в третьем мы получаем по типу и тегу.
Если посмотреть то синтаксис получается достаточно понятным, несмотря на его излишества. Те, кто используют библиотеку давно, скажут, что раньше такого не было, но эта версия была направлена на то, чтобы библиотека могла построить полный граф связей в голове, и данное решение было вынужденным. Единственный кто отличается - это внедрение с указанием имени. Строка не может быть определена на этапе компиляции в отличие от типов. По этой причине во внедрении через метод инициализации или обычный, отсутствует такая возможность - передавать массив имен отдельно будет выглядеть, мягко говоря, не красиво.
Те, кто пользуются библиотекой давно, или перешли на эту библиотеку с других, возможно, возмутятся - зачем нам беспокоится о циклах! И будут правы - на самом деле программа может сама отследить циклы и разрешить их, но вопрос лишь в том, сколько ей понадобится на это время. Старый способ разрешал зависимости достаточно изощренным способом, который работал, но приводил к проблемам, если использовать didSet у свойств. Чтобы избежать подобных проблем и повысить производительность, наличие циклов надо указывать явно:
class Cat {
init(home: Home) { ... }
}
class Home {
var animals: [Animal] = []
}
...
container.register(Cat.init)
.lifetime(.objectGraph)
container.register(Home.init)
.injection(cycle: true) { $0.animals.append($1 as Cat) }
.lifetime(.objectGraph)Но не думайте что вас бросили и вам придётся лишний раз думать :) Библиотека позаботилась, функция valid() у контейнера, на самом деле сообщит вам и о том, что есть циклы которые надо разорвать.
Выше была написано что внедрение через свойства с указанием циклов работает чуточку иначе - эти свойства внедряются только после того как все остальные объекты будут инициализированы, так как мы не можем внедрить свойство которое ссылает на объект внутри инициализации которого мы находимся. То есть тем самым мы разрываем цикл.
Но и на этом дело не закончилось – раньше, было время жизни perDependency которое является аналогом нового objectGraph, но по умолчанию ставится prototype. Отличие этих времен жизни можно почитать в специальной главе, а тут, лишь заострим внимание на том, что любой объект, который находится в цикле и с которого может начинаться инициализация должен быть помечен не как prototype. На самом деле можно выучить более просто правило - все объекты в цикле стоит помечать не как prototype, и хотя бы одно внедрение через свойство в цикле должно быть помечено.
Ситуации, в которых стоит иметь prototype объекты в цикле настолько редки, что не будет описаны, дабы не загромождать и так большую документацию лишней информацией. Скорей всего если у вас возникнет подобная ситуация, ваш уровень владения библиотекой будет настолько высок, что вы сможете это распознать сами.
В случае если у вас в цикле все объекты помечены как prototype библиотека выкинет ошибку при валидации, если же есть хотя бы один не prototype и есть хотя бы один prototype, то будет просто предупреждение.
На самом деле библиотека способна сама расставить правильно и время жизни и найти точки разрыва, но вопрос лишь в целесообразности данных действий. Чтобы все это сделать требуется хороший анализ графа зависимостей, так как бывают циклы через 3, 4, 5 связей, а еще хуже, если через одну переменную проходят по несколько циклов разной длины и там могут быть еще циклы и т.д. Для автоматического решения всех проблем требуется затратить много времени, а так как DI работает на старте приложения и это время очень ценное, то было принято решение - лучше сообщить один раз, чем выстраивать каждый раз.
в swift4 стала доступна новая возможность - keyPath. Она позволяет записать внедрение зависимостей через свойства более компактно, и главное намного меньше портит инкапсуляция. Но так как это всеголишь синтаксический сахар (который я рекомендую использовать), то описывать сам механизм большого смысла нету - он поддерживает ровно тоже самое что было описано выше, но с другим синтаксисом:
class Home {
private(set) var hamster: Hamster!
private(set) var cat: Cat!
private(set) var dog: Dog!
private(set) var cycleObj: Obj!
private(set) var animals: [Animal]!
}
container.register(Home.init)
.injection(\Home.hamster)
.injection(name: "felix", \.cat)
.injection(\.dog) { by(tag: MyDogTag.self, on: $0) }
.injection(cycle: true, \.cycleObj)
.injection(\.animals) { many($0) }Обращаю внимание, на одну важную особенность - для такого синтаксиса достаточно только знать о существовании свойства, но не обязательно иметь доступ на запись.
В принципе если вы пишите регистрацию в томже файле где и объект, то достаточно и fileprivate, но если регистрация и объявление класса разнесены, то можно использовать private(set) и в отличии от предыдущего синтаксиса это будет работать.