Одним из важнейших аспектов внедрения зависимостей — управление жизненным циклом объектов. В .NET жизненный цикл объектов очень прост: объект создается, используется и уничтожается механизмом сборки мусора. Наличие интерфейса IDisposable немного усложняет эту картину, но сам жизненный цикл не становится сложнее.
Объекты подпадают под сборку мусора, когда они становятся неиспользуемыми. И наоборот, они существуют, пока на них стоят ссылки. Как только потребитель завершает работу, это же происходит и с зависимостью.
Иногда когда потребитель завершает работу, зависимость может продолжить существование, если другие объекты сохраняют ссылку на нее.
Компоновщик принимает решение о времени создания экземпляров, и в зависимости от реализованного в нем правила совместного использования экземпляров между потребителями определяет, перестает ли зависимость использоваться, когда завершает работу единственный потребитель или все потребители должны закончить работу, прежде чем зависимость может быть удалена.
Компоновщик лучше, чем кто бы то ни было, знает, когда он создает одноразовый экземпляр, поэтому он знает и то, что этот экземпляр должен быть уничтожен. Компоновщик может легко сохранить ссылку на одноразовый экземпляр и вызвать его метод Dispose в нужное время.
Вызвать в нужное время по другому называется жизненные стили. Давайте рассмотрим основные жизненные стили и разберем подробнее каждый жизненный стиль
Singleton
Всякий раз, когда потребитель запрашивает компонент, подается один и тот же экземпляр.
Потребитель не может получить через статический член доступ к зависимости, находящейся в области видимости Singleton, и если запросим экземпляры у двух разных компоновщиков, получим два разных экземпляра.
Когда применяется
Используйте стиль Singleton где только возможно. Главная причина отказа от его применения — потоковая небезопасность компонента. Поскольку Singleton-экземпляр может совместно использоваться между большим количеством потребителей, он должен быть способен обрабатывать параллельный доступ.
Резюме
Жизненный стиль Singleton — один из самых простых для реализации. Все, что для этого требуется, — хранить ссылку на объект и предоставлять один и тот же объект по каждому запросу. Этот экземпляр существует, пока существует экземпляр компоновщика. Когда компоновщик завершает свою работу, он должен уничтожить и сам объект.
Transient
Жизненный стиль Transient (Кратковременный) предполагает возврат нового экземпляра при каждом запросе потребителя.
Когда применяется
Данный жизненный стиль является самым безопасным, но и наименее эффективным из всех стилей, потому что он приводит к созданию множество экземпляров, впоследствии требующих сборки мусора. Но если сомневаетесь относительно потоковой безопасности самих компонентов, Transient обеспечит вам требуемую безопасность на уровне приложения, поскольку каждый потребитель получает свой собственный экземпляр зависимости. При использовании Transient, когда много компонентов требует одну и ту же зависимость, каждый из них получает независимый экземпляр.
Резюме
Когда зависимость представляет собой изменяемый ресурс и каждому потребителю должно быть установлено свое приватное значение, то корректным жизненным стилем будет Transient, который гарантирует, что экземпляры никогда не будут использоваться совместно.
Per Graph
Singleton является наиболее эффективным жизненным стилем, а Transient — наиболее безопасным, а можно ли создать стиль, который объединял бы достоинства их обоих?
Хотя этого нельзя сделать в полной мере, в ряде случаев имеет смысл разделять один экземпляр в пределах одного создаваемого графа. Можем считать такую схему разновидностью Singleton c локальной областью видимости. Можно получить экземпляр, совместно используемый в пределах одного графа, и не разделять его с другими графами.
Каждый раз, когда создаем граф объекта, создаем только один экземпляр каждой зависимости. Если эту зависимость использует несколько потребителей, они разделяют один и тот же экземпляр; но когда создаем новый граф объекта, мы создаем новый экземпляр.
Когда применяется
Жизненный стиль Per Graph применяется в основном там, где также может использоваться и Transient. Обычно предполагается, что поток, создающий граф объекта, является единственным потребителем этого графа. Даже когда зависимость в запросе не является потокобезопасной, можно использовать жизненный стиль Per Graph, поскольку совместно применяемый экземпляр разделяется только потребителями, запущенными в том же потоке.
По сравнению с Transient, стиль Per Graph не добавляет никаких дополнительных издержек и часто может служить заменой для Transient. Но хотя издержки и отсутствуют, он не гарантирует и каких-либо дополнительных преимуществ. Мы получаем некоторый выигрыш в эффективности, только если один граф объекта содержит много потребителей одной и той же зависимости. В этом случае мы можем совместно использовать экземпляр между этими потребителями, но если разделяемые зависимости отсутствуют, то нечего будет разделять и никаких преимуществ не будет.
Резюме
Вместо того, чтобы создавать отдельные экземпляры для каждого потребителя, можно создать один экземпляр, который совместно используется между всеми потребителями.
Web Request Context (Контекст веб-запроса)
Чтобы решить проблему ожидания пользователей, веб-приложения обрабатывают запросы параллельно. Инфраструктура .NET защищает нас, обеспечивая выполнение каждого запроса в собственном контексте и со своими экземплярами контроллеров (Controller, если вы используете ASP.NET MVC). При параллельной обработке зависимости, которые не являются потокобезопасными, не могут использоваться как Singleton. С другой стороны, применение их как Transient может оказаться неэффективным или даже совершенно проблематичным, если нам требуется совместно использовать зависимость между разными потребителями в одном и том же запросе. Из этого следует, что если можно совместно использовать зависимость только в пределах одного запроса, то потоковая безопасность не станет проблемой.
Резюме
Зависимости ведут себя как Singleton в пределах одного запроса, но не разделяются между запросами. Каждый запрос содержит свой собственный набор ассоциированных с ним зависимостей.
ПРИМЕЧАНИЕ
Если создавать только один граф объектов на веб-запрос, то стили Web Request Context и Per Graph оказываются функционально эквивалентными.
Не все контейнеры внедрения зависимостей поддерживают этот жизненный стиль, поэтому, очевидно, мы можем использовать его, только если он доступен.
Pooled (Пулированный, реализованный как пул)
Иногда создание компонентов оказывается довольно затратной операцией. Распространенное решение в таком случае — создание пула заранее подготовленных компонентов, легко доступных для последующего использования.
Как и в случае жизненного стиля Web Request Context, следует воздерживаться от собственных реализаций пулов объектов и использовать пулы, предоставляемые типовыми контейнерами внедрения зависимостей. Не все контейнеры предоставляют жизненный стиль Pooled, поэтому, естественно, применять его можно, только если он поддерживается контейнером.
Когда применяется
Жизненный цикл Pooled может применяться, когда имеются специфические компоненты, которые часто используются, но являются затратными на этапе создания. Даже если компонент требует много ресурсов при создании, следует все же выбирать жизненный стиль Singleton, когда только это возможно. Так сможем обходиться одним экземпляром и расплачиваться за его создание лишь однажды.
Резюме
Обязательным требованием является то, что запрашиваемый компонент должен быть переиспользуемым. Если он имеет естественный жизненный цикл, исключающий повторное использование элементов, пул применять нельзя.
Lazy (Отложенный, задержанный)
Жизненный стиль Lazy, или Delayed — это виртуальный посредник для более затратной зависимости. Идея заключается в том, что если у нас имеется затратная зависимость, которую мы не хотим использовать часто, мы можем отложить ее создание вплоть до момента, когда она окажется необходимой.
В потребителя внедряется упрощенный дублер вместо реальной, более затратной реализации. Когда потребитель, наконец, нуждается в зависимости, вызывается метод создания экземпляра реальной зависимости. Вплоть до этого момента экземпляр не создается. После того как реальный экземпляр будет создан, все последующие вызовы будут делегироваться ему.
Резюме
Данный жизненный стиль следует использовать, только если потребитель использует энергозатратную зависимость в течение краткого периода своего жизненного цикла или если достаточно вероятно, что пройдет значительный период времени, прежде чем зависимость будет применена.
Вывод
Когда применяем инверсию управления к зависимостям, мы инвертируем управление не только над выбором типов, но и над управлением временем жизни. Когда потребитель более не создает собственные экземпляры зависимостей, он не может решить, когда зависимость должна быть создана или будет ли она разделяться с другими потребителями. Компоновщики решают, позволить ли нескольким потребителям совместно использовать единственный экземпляр или предоставлять каждому потребителю собственный.
Зависимости могут выйти за границу области видимости, после чего они будут утилизированы сборщиком мусора. Но остается необходимость явно управлять компонентами, реализующими интерфейс IDisposable, потому что должны гарантировать, что все неуправляемые ресурсы также очищаются, иначе в приложении скоро возникнут утечки памяти.
Самым безопасным стилем является Transient, поскольку экземпляры никогда не используются одновременно несколькими элементами. От также наименее эффективный, поскольку предполагается, что много экземпляров одного и того же типа будет находиться в памяти. Наиболее эффективный жизненный стиль — Singleton, поскольку лишь один экземпляр находится в памяти. Но требуется, чтобы компонент был потокобезопасным, поэтому данный жизненный стиль можно применять не всегда.
Стили Web Request Context и Pooled являются хорошими альтернативами стилям Singleton и Transient, но только в более ограниченных сценариях.
Ну и давайте нарисуем итоговую таблицу рассмотренных жизненных стилей
Singleton | Единственный экземпляр, всегда разделяется (используется повторно) |
Transient | Всегда создаются новые экземпляры |
Per Graph | В каждом графе используется один экземпляр |
Web request context | На каждый веб запрос подается 1 экземпляр каждого типа |
Pooled | Экземпляры берутся из пула предварительно созданных объектов |
Lazy | Энергозатратная зависимость создается и обслуживается в отложенный момент времени |