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

Друзья:
//devdigest platform - новости и полезные статьи о дотнете.

Управление жизненным циклом объектов в Castle Windsor

February 21, 2017

В этой статье я хотел бы разобрать вопрос управления временем жизни объектов при использовании Inversion of Control контейнера Castle Windsor.

Мы рассмотрим такие вопросы как:

  • Как Castle Windsor управляет жизненным циклом создаваемых объектов.

  • Можно ли вызывать Dispose() метод у полученных из контейнера объектов.

  • Когда нужно вызывать _container.Release(someObjectInstance).

  • Как работают различные Lifestyle’ы и Lifecycle’ы Castle Windsor.

Castle Windsor и жизненный цикл создаваемых им объектов

Одна из ключевых обязанностей IoC-контейнера - это управление жизненным циклом создаваемых им объектов. Контейнер создает объект, настраивает его и отдаем вам, после чего вы можете им пользоваться, а когда объект становится не нужен - производит действия подготавливающие объект к уничтожению.

Давайте рассмотрим пример:

public class UnitOfWork
{
   public void Init()
   {
      // some initialization logic...
   }
   public void Commit()
   {
      // commit or rollback the UoW
   }
}

Мы имеем класс реализующий функциональность unit of work. После создания его экземпляра и перед его использованием, нам нужно его инициализировать. А после использования зафиксировать всю работу.

Если мы хотим получать экземпляр UnitOfWork из IOC-контейнера, то обязанностью контейнера будет создать экземпляр UnitOfWork, затем вызывать метод Init() - мы не должны этого делать, потому что может быть множество клиентов, завязаных на один экземпляр unit of work, и как определить какой из них должен это сделать?

Если вы сейчас думаете, что пример надуман и метод Init() вообще не нужен, а всю логику инициализации можно поместить в конструктор, то, в принципе, да, но рекомендуется тяжеловесную логику инициализации экземпляра, занимающую длительное время, производяющую сетевые запросы и прочие подобные вещи, выносить из конструктора.

Ответ - никакой. Это не их работа. Они просто должны его использовать. Какой из клиентов должен вызывать Commit() ? Опять же, никакой. Каждый отдельный клиент не знает, когда другие клиенты завершат работу.

Никто из клиентов не отвечает за этот экземпляр UnitOfWork, так как никто из них не создавал его.

Подготовка объекта к уничтожению - это тоже часть ответственности контейнера. Обычно в .NET под этим подразумевается вызов метода Dispose() объекта.

Хочу заметить, что это упрощенный взгляд на unit of work и управление его жизненным циклом. В реальных приложениях, вероятнее всего, жизненным циклом unit of work будет управлять специально созданный компонент, как то связанный с Castle Windsor. Смысл примера в том, что жизненным циклом объекта должен управлять тот, кто ответственнен за его жизненный цикл, а не тот кто использует этот объект.

Ссылки:

  1. Жизненный цикл объектов в Castle Windsor

Должен ли я вызывать Dispose метод на объектах полученных из Castle Windsor?

В .NET фреймворке есть простое правило относительно вызова метода Dispose():

Вызывайте Dispose у того, что вы создали, когда вы закончили использовать его (Dispose what you’ve created, when you’re done using it).

Так как контейнер создал объект, то это его обязанность вызвать dispose метод этого объекта - вы не должны этого делать.

Всегда могут быть исключения в каких-то случаях, например когда вы специально предусматриваете вызов Dispose() из своего кода.

Если вы хотите больше узнать о Dispose объектов, то вот еще несколько ссылок:

  1. Dispose паттерн в .NET

  2. Забавный ответ Эрика Липперта о диспосинге объектов

  3. Первая часть статьи Эрика Липперта о финалайзерах

  4. Вторая часть статьи Эрика Липперта о финалайзерах

Castle Windsor хранит ссылки на создаваемые им объекты

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

Настройки того, как Castle Windsor будет отслеживать создаваемые им объекты, определяются Release Policy. По умолчанию Windsor использует LifecycledComponentReleasePolicy, который отслеживает все создаваемые контейнером объекты, до момента когда они не перестанут быть нужны. Windsor имеет также альтернативную версию release policy, называющуюся NoTrackingReleasePolicy, которая, как подразумевается ее названием, никогда не отслеживает создаваемые контейнером объекты.

Часто люди рассматривают то, что Windsor хранит ссылки на объекты, как причину утечек памяти (так как .NET Garbage Collector не может удалить объекты из памяти, пока на них есть ссылки в контейнере) и решают заменить стандартную политику на NoTrackingReleasePolicy, думая, что это решит все проблемы. Однако, в конечном итоге, это может привести только к большей головной боли, сложному запутанному коду с вызовом Dispose объектов и как результат более серьезным, сложно отлавливаемым ошибкам и проблемам с утечкой памяти.

Нет никаких проблем в том, что Windsor хранит ссылки на объекты, вам просто нужно правильно использовать Castle Windsor.

Ссылки:

  1. Release Policy в Castle Windsor

Когда нужно вызывать Release

Как я упомянул в предыдущем параграфе, Windsor хранит ссылки на созданные объекты до момент, когда они не перестанут быть нужны и некоторые пользователи думают, что в порядке правильного освобождения компонетов, для того чтобы избежать проблем с утечкой памяти, они должны вызывать метод Release() контейнера для всех объектов, которые они получили из него:

   container.Release(myComponent);

Чтобы дать понять контейнеру, что они больше не нуждаются в этих объектах. Давайте остановимся на этом моменте подробнее.

Как Windsor понимает, что вы больше не нуждаетесь в этих объектах

В Castle Windsor каждый компонент имеет Lifestyle ассоциированный с ним. Этот Lifestyle определяет контекст, в котором экземпляр объекта существует. Если вы хотите, чтобы объект существовал в единственном экземпляре на весь контейнер, вы даете этому объекту Singleton Lifestyle. Если вы хотите использовать один и тот же объект в рамках веб-запроса, вы даете ему Per-web-request Lifestyle и т.д.. Когда Lifestyle оканчивается, Windsor понимает, что объекты имеющие этот Lifestyle больше не нужны и подготавливает их к уничтожению.

Когда оканчивается контект

Тогда как некоторые контексты, например такие как веб-запрос, имеют явно определенный конец, который Windsor может отследить, другие контексты не имеют его. И это приводит нас к необходимости использования метода Release().

Windsor контейнер имеет метод Release(), используя который вы можете явно сказать: “Хей, я больше не нуждаюсь в этом объекте”. Хотя название метода звучит довольно императивно, подразумевая немеделенное действие - это часто не так. И между вызовом метода и действиями по уничтожению объекта может пройти довольно много времени. Время, когда объект будет действительно уничтожен, определяется тем, какой Lifestyle вы используете.

  • Singleton будет игнорировать ваши вызовы Release(), потому что экземпляр существует в контексте жизни всего контейнера и то что вам этот объект сейчас не нужен, не значит, что он не нужен сейчас в каком-то другом месте программы или не понадобится мгновением позже. Контекст Singleton Lifestyle имеет явно определенный конец - это момент когда будет disposed сам Windsor контейнер. Это значит, что нет смысла вызывать Release() на объектах с Singleton Lifesyle.

  • Per-web-request будет игнорировать ваши вызовы Relese() по cхожим причинам что и выше - экземпляр существует в рамках веб-запроса, веб-запрос тоже имеет явно определенный конец, поэтому объекты с таким Lifestyle будут разрушаться только во время окончания веб-запроса. Это также значит, что от вас никаких действий не требуется - Windsor сам определит момент для необходимых действий.

  • Per-thread подобен синглтону, только в контексте существования каждого отдельного потока. Release() per-thread компонентов ни к чему не приведет.

  • У Scoped Lifestyle вы сами определяете начало и окончание времени жизни контекста в котором существует экземпляр объекта. Поэтому Release() точно также не будет иметь никакого эффекта, пока scope в котором был создан этот объект не будет завершен.

  • Pooled экземпляры отличаются от предыдущих. Они не имеют явно определенного окончания, поэтому вам необходимо вызывать Release() когда объект вам больше не нужен, чтобы Windsor мог либо возвратить его в пул, либо уничтожить (в зависимости от текущей ситуации и настроек пула).

  • Transient экземпляры подобны Pooled, потому что тоже не имеют явно определенного окончания. И Windsor не знает, нужен вам этот объект еще или нет, пока вы явно не скажете ему об этом (через вызов Release()). Так как transient экземпляры не используются совместно с кем-нибудь еще, то они будут немедленно подготовлены к уничтожению, после вызова Release().

Так как во время использования объектов полученных из Castle Windsor, вы не знаете с каким Lifestyle они были созданы, то напрашивается вывод, что безопаснее всего всегда вызывать метод Release(), так как в худшем случае, этот вызов не произведет никакого эффекта, но поможет избежать утечек памяти.

В реальности же, вы почти никогда не должны вызывать Release() явно в вашем приложении.

Ссылки:

  1. Типы жизненных стилей объектов в Castle Windsor

Вы почти никогда не должны вызывать Release метод контейнера явно

Потому что release одного компонента приводит к release целого графа зависимых от него объектов.

Как я писал в предыдущем параграфае, Windsor самостоятельно определяет окончание жизни объектов используя их Lifesyle. Допустим, вы имеете Per-web-request ShoppingCard компонент в вашем приложении, который зависит от Transient PaymentCalculatorService и Singleton AuditWriter, когда веб-запрос завершится, Windsor будет релизить ShoppingCard со всеми его зависимостями. Так как AuditWriter - это Singleton, то это не будет иметь какого-либо влияния на него и он продолжит свое существование, но PaymentCalculatorService (также как и сам ShoppingCard) будут подготовлены к уничтожению, без какой-либо явной работы с вашей стороны.

Тоже самое происходит и при использовании Типизированных фабрик - когда фабрика релизится, все компоненты, что вы получили из нее, будут релизиться также. Однако вам необходимо быть осторожными - если вы получаете слишком много объектов из фабрики, а время жизни фабрики велико (например Singleton) вы можете закончить с тем, что слишком много компонентов будет находиться в памяти длительное время.

Предположим, вы пишете веб-браузер и имеете фабрику вкладок, которая создает вкладки, для отображения их в браузере. Эта фабрика будет иметь Singleton Lifestyle в вашем приложении, но вкладки, которые она производит будут Transient - пользователь может открыть вкладку, затем закрыть ее, затем открыть еще несколько и снова их закрыть. Наверное, вы сталкивались с тем, как быстро заканчивается оперативная память, когда у вас много открытых вкладок в браузере, поэтому ждать пока контейнер будет полностью уничтожен, чтобы освободить память от давно закрытых вкладок будет неправильно.

Наиболее вероятно, что вы хотели бы сказать Windsor релизить вкладку сразу после ее закрытия, Это довольно просто сделать используя Release() метод интерфейса вашей типизированной фабрики, который вы будете вызывать, когда пользователь закрывает вкладку. Вы скажете, что нет разницы какой Release() метод вызывать Windsor контейнера или вашей фабрики, но по-настоящему это не так, семантически разница велика - вы вызываете метод являющийся частью вашего интерфейса и частью вашей бизнес-логики, а не стороннего компонента.

Пример интерфейса типизированной фабрики:

   public interface IDummyComponentFactory
   {
        IDummyComponent Create();
        void Release(IDummyComponent dummyComponent);
   }

Регистрация типизированной фабрики в контейнере:

   kernel.AddFacility<TypedFactoryFacility>();
   kernel.Register(
       Component.For<IDummyComponent>()
         .ImplementedBy<Calendar>().LifeStyle.Transient,
       Component.For<IDummyComponentFactory>()
         .AsFactory()
   );

Использование фабрики:

   var factory = kernel.Resolve<IDummyComponentFactory>();
   var component = factory.Create();
   ...
   factory.Release(component);

Ссылки:

  1. Типизированные фабрики в Castle Windsor

Места где вы все-таки должны вызывать Release явно

Для того, чтобы определить где вы должны вызывать Release() есть правило:

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

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

   container.Register(
      Component.For<ITaxCalculator>()
         .UsingFactoryMethod(k =>
         {
            var country = k.Resolve<ICountry>(user.CountryCode);
            var taxCalculator = country.GetTaxCalculator();
            k.Release(country);
            return taxCalculator;
         })
      );

Если вы явно получаете из контейнера фабрику, то вы явно должны релизить ее:

   var factory = kernel.Resolve<IDummyComponentFactory>();
   ...
   kernel.Release(factory);

Но только саму фабрику, а не объекты, которые получили из нее.

И помните, что использование Ioc-контейнера в качестве Service Locator во многих местах вашего приложения - это антипаттерн и весь код явного получения объектов из контейнера, а соответственно и их релиза, должен быть сосредоточен в одной точке, которая, как правило, является точкой инициализации приложения.

Ссылки:

  1. Castle Windsor и утечки памяти
  2. Инверсия зависимостей на практике - статья Сергея Теплякова о общих вопросов использования Ioc-контейнеров
  3. Документация Castle Windsor

Статья написана, используя материалы сайта kozmic.net