Во многих библиотеках внедрения зависимости можно указать время жизни, или это еще называется scope (не знаю адекватного перевода на русский). Оно позволяет указать, сколько будет жить объект, и как будет создаваться.
В моей библиотеке есть разные времени жизни, и можно даже указать "гибкий" вариант - когда управление временем жизни передается из библиотеки вам.
Все они настраиваются с помощью одно перечисления. Давайте познакомимся с каждым из них.
Самое простое время жизни. Каждый раз будет создаваться новый объект, не зависимо ни от чего. Объявляется время жизни так:
container.register(Cat.init)
.lifetime(.prototype)
И если мы несколько раз попробуем получить экземпляр кошки, то это будет каждый раз разный:
let cat1: Cat = container.resolve()
let cat2: Cat = container.resolve()
cat1 === cat2 // false
Наиболее сложное в понимании время жизни. Зайдем издалека. При создании объекта, создаются и другие зависимости. То есть, начав создавать объект A создается еще B и C. Тем в свою очередь может понадобиться D, E, Z. Вот набор всех этих объектов нужных для создания объекта A называется графом зависимостей A. Обращу внимание - это не дерево, а граф, так как могут быть циклы. Теперь представим вот такой граф зависимостей: A -> B, A -> C, B -> E, B -> D, C -> D, C -> Z. Или визуально: Граф Вроде обычный граф, даже дерево. Но у него есть особенность - на объект D ведут две стрелки. И возникает вопрос - создавать объект D дважды, или единожды. Если время жизни D будет prototype, то он создастся дважды, а если objectGraph то единожды при создании A.
Давайте посмотрим на примере:
container.register(A.init(B:C:))
container.register(B.init(E:D:))
container.register(C.init(D:Z:))
container.register(E.init)
container.register(D.init)
.lifetime(.objectGraph) // .prototype
container.register(Z.init)
Тут время жизни других классов не имеет значения в данном примере, поэтому оно упущено. И что же будет теперь при получении A?
В случае если будет написано время жизни prototype у D:
let a1: A = container.resolve()
let a2: A = container.resolve()
a1.b.d === a1.c.d // false
a1.b.d === a2.b.d // false
a2.b.d === a2.c.d // false
В случае если будет написано время жизни objectGraph у D:
let a1: A = container.resolve()
let a2: A = container.resolve()
a1.b.d === a1.c.d // true
a1.b.d === a2.b.d // false
a2.b.d === a2.c.d // true
Из этого примера видно, что на каждое создание объекта A при objectGraph был создан всего один экземпляр объекта D.
При таком времени жизни объект будет создаваться единожды на каждый контейнер. В случае если вы не используете больше одного DIContainer-а, то это время жизни будет эквивалентно времени жизни perRun. Так как эти два времени жизни очень похожи, то разбор их принципа действия будет в perRun.
При таком времени жизни объект будет создаваться единожды на один запуск программы. Можно сравнить с ленивым синглетоном. Но не все так просто, так как помимо perRun и perContainer у этих времен жизни есть модификаторы: weak, strong. В коде это выглядит так:
container.register(A.init)
.lifetime(.perContainer(.weak))
container.register(B.init)
.lifetime(.perContainer(.strong))
container.register(C.init)
.lifetime(.perRun(.weak))
container.register(D.init)
.lifetime(.perRun(.strong))
И если используется perRun(.strong)
или perContainer(.strong)
то описание: один экземпляр на запуск или один экземпляр на контейнер полностью корректно. DI точно не создаст больше одно экземпляра объекта, не зависимо ни от чего.
А вот с perRun(.weak)
или perContainer(.weak)
все чуть сложнее. В этом случае сам DIContainer ни как не держит объект, и он может быть уничтожен. То есть если программа держит объект сама, то экземпляр будет каждый раз один и тот же, но если программа перестанет держать объект, то он будет создан заново. Если же программа не держит объект вовсе, а только получает его, то это время жизни по поведению становится аналогом objectGraph
. Да именно objectGraph
так как во время создания объекта, его нужно держать сильной ссылкой.
Это синглетон. По своему принципу почти полный аналог perRun(.strong)
, но с одним нюансом - он создается сразу-же. Хотя стоять - что значит сразу же? Да конечно объект сразу же сам создаться не может, поэтому в DIContainer-е есть специальная функция - она создает все объекты одиночки разово, и после этого они больше не будут создаваться. функция называется: initializeSingletonObjects()
и есть у каждого container-а
. Замечу, что если у вас много контейнеров, и у каждого контейнера есть одинаковые регистрации. И у каждого контейнера будет вызвана эта функция, то объект создастся единожды! Этим можно иногда пользоваться - например, в одном контейнере зарегистрировать все зависимости нужные для синглетон объекта. Вызвать initializeSingletonObjects()
. А в другом контейнере можно зарегистрировать только этот это один единственный синглетон, без всех его зависимостей. И объект будет каждый раз успешно создаваться. Правда, функция валидации графа будет возмущаться.
В редких сценариях может не хватить указанных времен жизни. Например, захочется чтобы каждый третий объект создавался заново, но каждый три объекта были одни и те же. Для сложных сценариев есть возможность создать свой scope и передать его:
let yourScope = DIScope(name: "your scope", storage: DICacheStorage())
container.register(Cat.init)
.lifetime(.custom(yourScope))
Данный вариант использует обычный scope с кэширующей политикой хранения - то есть будет храниться по одному экземпляру каждого объекта. Scope обладает возможность указать политику хранения weak
или strong
, которая является аналогом weak/strong для perContainer и perRun области. По умолчанию используется strong
политика.
DIScope(name: "your scope", storage: DICacheStorage(), policy: .weak)
В таком виде scope интересен тем, что можно создать некий аналог perRun/perContainer, но при этом иметь возможность сбросить кэш:
yourScope.clean()
Например, у вас есть объекты которые должны быть в одном экземпляре на одну сессию. Тогда их можно разместить в таком scope, и почистить при разлогинивании.
Возвращаемся к начальной идеи - как сделать так, чтобы каждый третий объект был новым? Для такой цели придётся реализовать свою собственную политику хранения объектов. Сделать это можно отнаследовавшись от протокол DIStorage
:
class YourStorage: DIStorage {
var any: [DIComponentInfo: Any] { cache }
private var cache: [DIComponentInfo: Any] = [:]
func fetch(key: DIComponentInfo) -> Any? {
return cache[key]
}
func save(object: Any, by key: DIComponentInfo) {
cache[key] = object
}
func clean() {
cache.removeAll()
}
}
В данном примере реализован самый простое хранилище - которое хранит все в единственном виде. Давайте его модифицируем:
class YourStorage: DIStorage {
...
private var cache: [DIComponentInfo: (Any, Int)] = [:]
func fetch(key: DIComponentInfo) -> Any? {
if let (object, count) = cache[key] {
cache[key] = (object, count + 1)
if count < 3 {
return object
}
}
return nil
}
func save(object: Any, by key: DIComponentInfo) {
cache[key] = (object, 1)
}
...
}
Такой вариант на каждый третий запрос возвращает nil, что приводит к необходимости пересоздать объект. А после пересоздания объекта, он снова запишется в кэш.
Если у компоненты не указано время жизни, то используется prototype
. Но это можно изменить в настройках:
DISetting.Defaults.lifeTime = .objectGraph
Этот код сделает так, чтобы по умолчанию у всех объектов было время жизни один на граф.