С регистрации начинается работа с контейнером внедрения зависимостей. Суть регистрации - указать какие есть сущности в программе, и как эти сущности связанны друг с другом. В случае с библиотекой DITranquillity регистрация выглядит как еще одно объявление сущности, записанное в правилах библиотеки. По этим причинам такой способ называется "декларативный контейнер", так при его использовании, вы как бы декларируете, что у вас есть.
В коде регистрация сущности всегда начинается с ключевого слова register
:
let container = DIContainer()
container.register(MyClass.init)
В круглых скобках указывается тип, который нужно будет создать. Так как создание типа это действие, то надо оставлять информацию не о самом типе, а о методе инициализации этого типа. Для этого можно использовать один из двух вариантов синтаксиса:
container.register(MyClass.init)
container.register { MyClass() }
Более подробно про отличия этих записей можно почитать в главе Внедрение/Внедрение через метод инициализации.
Существует третья запись
container.register(MyClass.self)
которая говорит, что надо зарегистрировать тип, но создание объекта будет не обязанностью DI. Такой способ записи используется при работе со Storyboard, или каких-то специфичных кейсов в вашем коде.
Но процесс регистрации, состоит не только с указания сущности, но и еще из нескольких опциональных этапов:
- Указание сервисов
- Указание времени жизни
- Указание зависимостей
- Указание действий после создания объекта
- Указание "по умолчанию"
Часто наша сущность, в особенности классы реализуют протоколы, и иногда даже больше одного. И случается так, что протокол известен другим частям программы, а вот конкретный класс скрыт. Для таких случаев существует указание сервисов. Оно позволяет указать, какие протоколы реализует класс, и тем самым в будущем получить экземпляр класса по этим протоколам.
Синтаксически указание сервисов происходит во время регистрации, с использованием ключевого слова as
:
container.register(Cat.init)
.as(Animal.self)
.as(Mammal.self)
.as(Pet.self)
Данная запись будет означать, что животное можно будет получить, по любом из трех описанных типов, то есть:
let cat: Cat = container.resolve()
let animal: Animal = container.resolve()
let mammal: Mammal = container.resolve()
let pet: Pet = container.resolve()
Все четыре записи создадут кошку. Возникает резонный вопрос, а почему бы не написать так:
container.register { Cat() }
container.register { Cat() as Animal }
container.register { Cat() as Mammal }
container.register { Cat() as Pet }
Ведь и при такой записи также будут создаваться кошки, по всем 4 типам, но при этом такая запись не требует специфичного синтаксиса.
Причина кроется в том, что в сочетании с временем жизни, эти записи становятся не эквивалентные. Самый простой пример - предположим мы хотим чтобы на всю программу существовала всего одна кошка. Но из разных мест к этой кошке обращаются по разным протоколам. Тогда в первом случае, возможно, указать время жизни объекта, а во втором случае, даже указав время жизни у объектов, мы получим 4 разных кошки. В прочем в некоторых ситуациях это может быть желаемым поведением.
Одна из опциональных возможностей многих DI контейнеров это возможность задать время жизни объекта. Это позволяет использовать DI не только ради получения объектов, но и контролировать когда объект будет создан и будут ли его копии.
В библиотеке время жизни регистрируемого объекта указывается в момент регистрации, с помощью ключевого слова lifetime
. Более подробно про то какое бывает время жизни, и что оно дает можно почитать в главе Время жизни.
Не всегда возможно или необходимо передавать зависимости в метод инициализации. Возможно, нужно внедрить зависимости в уже существующий объект, или по каким либо другим причинам нет возможности их передать в методе инициализации.
В этом случае на помощью приходит другой синтаксис - синтаксис внедрения зависимостей. Любое внедрение начинается с ключевого слова injection
и имеет большой спектр возможностей и вариантов. Подробно про варианты внедрения зависимостей можно почитать в главе: Внедрение
В редких случаях нужно сделать дополнительные действия после окончания полной инициализации объекта. То есть в тот момент, когда уже точно все зависимости внедрены, и объект проинициализирован. Для такого случая в библиотеке во время регистрации компонента предусмотрена функция postInit
.
Как вариант в этой функции можно проставить делегат на себя:
container.register(Presenter.init)
.injection(\.view)
.injection(\.interactor)
.postInit { presenter in
presenter.interactor.delegate = presenter
}
Или исполнить любой другой код, для исполнения которого нужно точно быть уверенным в наличии всех зависимостей.
В больших приложения возникают ситуации, когда на один сервис появляется много реализаций. Особенно это актуально с учетом, что в DITranquillity есть множественное внедрение. Но, в таких ситуация в случае необходимости получения одной реализации возникает вопрос - а какую выдавать? Тут на помощь приходит два механизма: Модульность, Тэги. Один из них позволяет автоматически определить какой объект внедрять, а второй указать явно с помощью тэга. Более подробно про эти механизмы можно почитать в главах посвященных им. Но существует третий механизм, более простой и понятный - можно задать какой объект будет использоваться по умолчанию. Несмотря на его простоту, есть один нюанс связанных с этим способом - в каждом модуле может быть только один компонент "по умолчанию" с одинаковыми сервисами. Да если у вас есть разные компоненты, каждый реализует разные сервисы, то между ними "по умолчанию" никак не пересекаются.
Небольшой пример зачем, и когда это может быть нужно:
container.register(CrashlyticsLogger.init)
.as(Logger.self)
container.register(FileLogger.init)
.as(Logger.self)
container.register(ConsoleLogger.init)
.as(Logger.self)
container.register { MainLogger(loggers: many($0)) }
.as(Logger.self)
.default()
...
let logger: Logger = container.resolve()
Пример слегка наигранный, но видно из него видно, что один сервис Logger
реализуют 4 класса. Но есть один "основной" логгер, который использует все другие. И при получении одного экземпляра логгера, логично, что стоит отдавать его, а не какой-то конкретный.
Аналогично поведения "по умолчанию" сущестует еще один вариант регистрации - тестовая регистрация. Она имеет наибольший приоритет при выборе, и игнорирует модульности и т.п. Из названия можно догадаться что этот способ регистрации нужен для тестов. Он позволяет уже на существуем графе зависимостей легко подменить одну или несколько зависимостей, без необходимости удалять аналогичные.