Skip to content

Latest commit

 

History

History
150 lines (130 loc) · 12 KB

scope_and_lifetime.md

File metadata and controls

150 lines (130 loc) · 12 KB

Время жизни

Во многих библиотеках внедрения зависимости можно указать время жизни, или это еще называется scope (не знаю адекватного перевода на русский). Оно позволяет указать, сколько будет жить объект, и как будет создаваться.

В моей библиотеке есть разные времени жизни, и можно даже указать "гибкий" вариант - когда управление временем жизни передается из библиотеки вам.

Все они настраиваются с помощью одно перечисления. Давайте познакомимся с каждым из них.

Всегда новый (prototype)

Самое простое время жизни. Каждый раз будет создаваться новый объект, не зависимо ни от чего. Объявляется время жизни так:

container.register(Cat.init)
  .lifetime(.prototype)

И если мы несколько раз попробуем получить экземпляр кошки, то это будет каждый раз разный:

let cat1: Cat = container.resolve()
let cat2: Cat = container.resolve()
cat1 === cat2 // false

Единственный в графе (objectGraph)

Наиболее сложное в понимании время жизни. Зайдем издалека. При создании объекта, создаются и другие зависимости. То есть, начав создавать объект 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.

Один на контейнер (perContainer)

При таком времени жизни объект будет создаваться единожды на каждый контейнер. В случае если вы не используете больше одного DIContainer-а, то это время жизни будет эквивалентно времени жизни perRun. Так как эти два времени жизни очень похожи, то разбор их принципа действия будет в 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 так как во время создания объекта, его нужно держать сильной ссылкой.

Одиночка (single)

Это синглетон. По своему принципу почти полный аналог perRun(.strong), но с одним нюансом - он создается сразу-же. Хотя стоять - что значит сразу же? Да конечно объект сразу же сам создаться не может, поэтому в DIContainer-е есть специальная функция - она создает все объекты одиночки разово, и после этого они больше не будут создаваться. функция называется: initializeSingletonObjects() и есть у каждого container-а. Замечу, что если у вас много контейнеров, и у каждого контейнера есть одинаковые регистрации. И у каждого контейнера будет вызвана эта функция, то объект создастся единожды! Этим можно иногда пользоваться - например, в одном контейнере зарегистрировать все зависимости нужные для синглетон объекта. Вызвать initializeSingletonObjects(). А в другом контейнере можно зарегистрировать только этот это один единственный синглетон, без всех его зависимостей. И объект будет каждый раз успешно создаваться. Правда, функция валидации графа будет возмущаться.

Пользовательский (custom)

В редких сценариях может не хватить указанных времен жизни. Например, захочется чтобы каждый третий объект создавался заново, но каждый три объекта были одни и те же. Для сложных сценариев есть возможность создать свой 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, что приводит к необходимости пересоздать объект. А после пересоздания объекта, он снова запишется в кэш.

По умолчанию (default)

Если у компоненты не указано время жизни, то используется prototype. Но это можно изменить в настройках:

DISetting.Defaults.lifeTime = .objectGraph

Этот код сделает так, чтобы по умолчанию у всех объектов было время жизни один на граф.