DI/IoC container lifestyles

В прошлой статье были описаны top-level понятия, охватывающие проблемы управления зависимостями между компонентами в приложении. Были показаны подходы к их решению, в частности при помощи Dependency Injection/Inversion of control контейнеров.

В этой статье будет рассмотрено понятие lifestyle в контексте DI/IoC, описаны существующие типы lifestyle политик на примере Castle.Windsor и Autofac, а также будет показана реализация собственной lifestyle политики для Castle.Windsor.

  1. What is lifestyle?
  2. Lifestyle types
  3. Component tracking and release
  4. Lifestyle types comparison in Castle.Windsor and Autofac
  5. "Per lifetime scope" lifestyle
  6. Implementation of "per lifetime scope" lifestyle for Castle.Windsor

What is lifestyle?

В рамках разрешения зависимостей контейнер компонент должен решать задачи управления жизненным циклом компонент (создание, использование и освобождение компонента). При регистрации компонента в контейнере указывается lifestyle политика.

container.Register(Component
    .For<ISimpleService>()
    .ImplementedBy<SimpleServiceImpl>()
    .LifeStyle.Singleton;

container.Register(Component
    .For<ICompositeService>()
    .ImplementedBy<CompositeServiceImpl>()
    .LifeStyle.Transient;

Lifestyle политика определяет границы разделения, повторного использования компонента и времени жизни компонента. То есть указывая lifestyle, мы даем возможность контейнеру получить ответы на следущие вопросы:

  • Каким экземпляром компонента разрешать зависимость?
  • Нужно ли создавать новый компонент всякий раз, чтобы разрешить зависимость, либо использовать для этого ранее созданные экземпляры?
  • Каковы границы повторного использования ранее созданных компонент?
  • Когда ранее созданные компоненты становятся не пригодными для разрешения новых зависимостей?
  • Должен или компонент освобождать (release) созданные ранее компоненты? В какой момент времени нужно это делать?

В контексте обсуждения lifestyle'ов неважно в какой форме контейнер разрешает зависимость. Различие лишь в том, кто иницирует процесс разрешения и инвертирован ли этот процесс.

  • direct request, приложение запросило у контейнера предоставить ему зависимость.
  • dependency injection, контейнер сам предоставил зависимость компоненту.

Lifestyle types

Опишем наиболее общие типы lifestyle'ов, которые можно встретить во разных реализациях DI/IoC контейнеров.

Transient

Контейнер создает новый уникальный экземпляр компонента каждый раз, когда нужно разрешить зависимость. Созданные экземпляры не используются для разрешения последующих зависимостей. Transient политика по своей природе означает, что зависимость не может быть shared, то есть не может использоваться несколькими компонентами совместно.

[Test]
public void Given_transient_dependency_should_resolve_new_instance_of_component_for_any_request()
{
    var container = new WindsorContainer();
    container.Register(Component
        .For<ISimpleService>()
        .ImplementedBy<SimpleServiceImpl>()
        .LifeStyle.Transient);
            
    var instance1 = container.Resolve<ISimpleService>();
    var instance2 = container.Resolve<ISimpleService>();

    // verify instances are different
    Assert.IsFalse(ReferenceEquals(instance1, instance2));
}

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

Важно отметить, что реализация того, как и когда освобождаются transient компоненты, может варьироваться для разных реализаций DI/IoC контейнеров. К примеру, Autofac отслеживает все созданные transient компоненты, которые являются IDisposable, и освобождает их только при уничтожении самого контейнера компонент. Это значит, что если у вас в приложении есть контейнер компонент, время жизни которого совпадает с временем жизни приложения (к примеру, Windows service, при старте которого создается контейнер, и уничтожается только при останове сервиса), и вы запрашиваете transient зависимости, которые являются IDisposable, то такие зависимости останутся в памяти до завершения работы приложения, то есть возможны memory leaks. У Nicholas Blumhardt, разработчика Autofac, есть отличный пост на тему управления временем жизни компонент в Autofac, в частности он показывает, что нужно делать, чтобы избегать таких ситуаций.

Singleton

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

[Test]
public void Given_singleton_dependency_should_use_same_component_instance_throughout_container_lifetime()
{
    //setup component and dependent component
    var container = new WindsorContainer();
    container.Register(Component
        .For<ISimpleService>()
        .ImplementedBy<SimpleServiceImpl>()
        .LifeStyle.Singleton);
    container.Register(Component
        .For<ICompositeService>()
        .ImplementedBy<CompositeService>()
        .LifeStyle.Singleton);
            
    var simpleInstance1 = container.Resolve<ISimpleService>();
    var simpleInstance2 = container.Resolve<ISimpleService>();
    var compositeInstance = container.Resolve<ICompositeService>();

    // verify container returns same instances when directly asked
    Assert.AreEqual(simpleInstance1, simpleInstance2);
    // verify container uses the very same instance with dependency injection
    Assert.AreEqual(simpleInstance1, compositeInstance.SimpleService);
}

Per thread

Этот lifestyle похож на singleton lifestyle, c тем лишь отличием, что контекст повторного использования компонент ограничен рамками отдельного потока, а не рамками жизни контейнера. Для разных потоков будут созданы различные экземпляры. Зависимость также является разделяемой.

[Test]
public void Given_per_thread_dependency_should_reuse_same_component_within_same_thread()
{
    var container = new WindsorContainer();
    container.Register(Component
        .For<ISimpleService>()
        .ImplementedBy<SimpleServiceImpl>()
        .LifeStyle.PerThread);

    var instanceFromThreadA = GetInstanceResolvedInThreadA(container);
    var instanceFromThreadB = GetInstanceResolvedInThreadB(container);
    // verify instances resolved in different threads are different
    Assert.AreNotEqual(instanceFromThreadA, instanceFromThreadB);

    var instanceFromSameThread1 = container.Resolve<ISimpleService>();
    var instanceFromSameThread2 = container.Resolve<ISimpleService>();
    // verify instances resolved in same threads are same
    Assert.AreEqual(instanceFromSameThread1, instanceFromSameThread2);
}

Per Web Request

Аналогичен per thread, c тем лишь отличием, что границами времени жизни и разделения компонент является отдельный web request (имеется ввиду ASP.NET Http Request).

Component tracking and release

Помимо указания lifestyle политики разные реализации контейнеров так или иначе позволяют повлиять на то, входит ли в обязанности контейнера отслеживание созданных экземпляров и их освобождение.

Autofac позволяет указать контейнеру не освобождать IDisposable компоненты, подразумевая, что это становится обязанностью приложения. Хотя контейнер по-прежнему продолжает отслеживать разделяемые компоненты для их дальнейшего использования (singleton, per thread, per web request). Однако контейнеру более не надо отслеживать IDisposable transient компоненты.

[Test]
public void Autofac_gives_ability_to_configure_container_not_to_release_component()
{
    var containerBuilder = new ContainerBuilder();
  
    // let application to manage component disposal
    containerBuilder
        .RegisterType<SimpleServiceImpl>()
        .As<ISimpleService>()
        .ExternallyOwned();
}

Castle.Windsor предоставляет похожую функциональность при помощи понятия release policy. Однако по сравнению с Autofac в Castle.Windsor эта политика не может быть применена к отдельному компоненту, а лишь ко всему контейнеру в целом.

[Test]
public void Castle_windsor_gives_ability_to_configure_container_not_to_track_components()
{
    var container = new WindsorContainer();
   
    // do not track and dispose transient components
    // let application to manage component disposal
    container.Kernel.ReleasePolicy = new NoTrackingReleasePolicy();
}

Lifestyle types comparison in Castle.Windsor and Autofac

Сравним lifestyle политики, которые доступны в Castle.Windsor и Autofac.
Lifestyle policy Castle.Windsor Autofac
Transient Yes Yes
Singleton Yes Yes
Per lifetime scope No Yes
Per Thread Yes No
Per Web Request Yes No
Pooled Yes No
Custom lifestyle policy Yes No

Набор out-of-the-box lifestyle политик в Castle.Windsor несомненно больше, чем в Autofac, также в Castle.Windsor есть возможность создать собственную реализацию lifestyle, реализовав ILifestyleManager интерфейс. Однако большинство функциональных возможностей out-of-the-box lifestyle политик, доступных в Castle.Windsor, с легкостью можно получить в Autofac при помощи одной лишь "per lifetime scope" политики.

Например, per thread поведение можно получить следующим образом.

void ThreadStart()
{
   using (var threadLifetime = container.BeginLifetimeScope())
   {
      var thisThreadsInstance = threadLifetime.Resolve<MyThreadScopedComponent>();
   }
} 

Аналогично можно получить поведение per web request, просто открыв lifetime scope в начала web request'а (HttpApplication.BeginRequest), и закрыв при завершении (HttpApplication.EndRequest).

Далее более детально посмотрим на "per lifetime scope" lifestyle политику.

"Per lifetime scope" lifestyle

Ранее описанные политики имеют одну общую черту: границы жизненного цикла и разделения компонент предопределены и являются внешними по отношению к приложению факторами. Per thread ограничен рамками потока, per web request - рамками http запроса, singleton - рамками контейнера. Было бы неплохо, если бы приложение могло cамо явно задавать рамки повторного использования компонент. И это как раз то, чем является "Per lifetime scope". Короче говоря, "per lifetime scope" политика позволяет явно обозначить unit of work в приложении.

К примеру, если приложение оперирует обработкой заказов, то имеет смысл определить такие unit of work, как client processing, order processing.

[Test]
public void Should_be_able_to_create_two_nested_lifetime_scopes()
{
    var containerBuilder = new ContainerBuilder();
    containerBuilder
        .RegisterType<SimpleServiceImpl>()
        .As<ISimpleService>()
        .InstancePerMatchingLifetimeScope("client");
    containerBuilder
        .RegisterType<CompositeService>()
        .As<ICompositeService>()
        .InstancePerMatchingLifetimeScope("order");

    using (var container = containerBuilder.Build())
    {
        Client client= GetClient();
        // begin single client processing and open lifetime scope
        using(var clientProcessingScope = container.BeginLifetimeScope("client"))
        {
            // create and use client-specific service
            var simpleService = clientProcessingScope.Resolve<ISimpleService>();
            //process orders of the client
            foreach (var order in client.Orders)
            {
                //begin single order processing, and open lifetime scope
                using (var orderProcessingScope = container.BeginLifetimeScope("order"))
                {
                    // create order-specific service, which uses service from client processing scope
                    var compositeService = orderProcessingScope.Resolve<ICompositeService>();
                    var clientSpecificServiceReferenced = compositeService.SimpleService;

                    // destroy all order-specific service at the end of scope
                }
            }

            // destroy all client-specific service at the end of scope
        }
    }
}

Отличительная черта и главная ценность "per lifetime scope" lifestyle по сравнению с другими политиками, это то, что рамками повторного использования компонент (lifetime scope) управляет приложение. Это дает дополнительную гибкость в выборе нужного lifetime scope, и не быть привязанными к готовым lifestyle'ам (per thread, per web request).

Implementation of "per lifetime scope" lifestyle for Castle.Windsor

Попытаемся восполнить отсутствие "per lifetime scope" lifestyle политики в Castle.Windsor и реализуем ее, используя ILifestyleManager extension point. В качеcтве вдохновения используем уже готовый lifestyle из Autofac.

Полная версия проекта (.net4, vs 2010) и исходный код этого решения выложен на github.

Поведение

Основные use case, которые хотим покрыть таковы.

  1. Запросить компонент дважды в рамках открытого lifetime scope, и убедиться, что экземпляры идентичны.
    [Test]
    public void When_resolving_same_service_in_same_scope_several_times_should_return_the_same_component()
    {
        var container = new WindsorContainer();
        container.Register(Component
            .For<ISimpleService>()
            .ImplementedBy<SimpleServiceImpl>()
            .LifeStyle.Custom<PerLifetimeScopeLifestyle<string>>());
    
        using (new LifetimeScope<string>())
        {
            var service1 = container.Resolve<ISimpleService>();
            var service2 = container.Resolve<ISimpleService>();
            Assert.AreEqual(service1, service2);
        }
    }
    
  2. Запросить один и тот же компонент в разных lifetime scope'ах, и убедиться что экземпляры разные.
    [Test]
    public void When_resolving_same_service_in_different_scope_several_times_should_return_different_components()
    {
        var container = new WindsorContainer();
        container.Register(Component
            .For<ISimpleService>()
            .ImplementedBy<SimpleServiceImpl>()
            .LifeStyle.Custom<PerLifetimeScopeLifestyle<string>>());
    
        ISimpleService service1, service2;
        using (new LifetimeScope<string>())
        {
            service1 = container.Resolve<ISimpleService>();
        }
        using (new LifetimeScope<string>())
        {
            service2 = container.Resolve<ISimpleService>();
        }
    
        Assert.AreNotEqual(service1, service2);
    }
    
  3. Во вложенном lifetime scope'е разрешить компонент, который имеет зависимость на компонент, который доступен во внешнем lifetime scope.
    [Test]
    public void If_service_in_scope_has_dependecy_to_service_which_is_in_outer_scope_container_should_wire_it()
    {
        var container = new WindsorContainer();
        container.Register(Component
            .For<ISimpleService>()
            .ImplementedBy<SimpleServiceImpl>()
            .LifeStyle.Custom<PerLifetimeScopeLifestyle<int>>());
        container.Register(Component
            .For<ICompositeService>()
            .ImplementedBy<CompositeServiceImpl>()
            .LifeStyle.Custom<PerLifetimeScopeLifestyle<string>>());
    
        using (new LifetimeScope<int>())
        {
            var simpleService = container.Resolve<ISimpleService>();
            using (new LifetimeScope<string>())
            {
                var compositeService = container.Resolve<ICompositeService>();
                Assert.AreEqual(simpleService, compositeService.SimpleService);
            }
        }
    }
    

Объявление границ lifetime scope (unit of work)

Итак, начнем с самого простого. Приложению нужно каким-то образом обозначить начало и конец lifetime scope, то есть обозначить границы unit of work.

public class LifetimeScope<TContext> : IDisposable
{
    public LifetimeScope()
    {
        LifetimeScopeStore.Get>TContext>().OpenScope();
    }

    public void Dispose()
    {
        LifetimeScopeStore.Get<TContext>().CloseScope();
    }
}

Механизм хранения созданных компонент

Далее, необходимо определить, где будут хранится экземпляры компонент с "per lifetime scope" lifestyle, созданные контейнером. Требования к механизму хранения следующие.

  1. Нужно обеспечить, чтобы компоненты, созданные однажды, были доступны в любом месте ниже по call hierarchy. Допустим, имеется цепочка вызовов методов. available-deeper-in-call-hierarchy
  2. Нужно обеспечить, чтобы компоненты были доступны в дочерних потоках. available-in-child-thread

Для этого нам подойдет такое хранилище как CallContext, подробнее здесь.

При использовании CallContext, можно столкнутся с проблемой, что данные, сохраненные в одном потоке, становятся недоступными в дочернем, то есть не удовлетворяется требование №2. Для того чтобы побороть эту проблему, нужно пометить класс, экземпляры которого будут хранится в Call context, маркерным интерфейсом ILogicalThreadAffinative.

private class LogicalThreadAffinativeDictionary : Dictionary<object, object>, ILogicalThreadAffinative
{ }

Реализация ILifestyleManager

ILifestyleManager - extension point Castle Windsor'а для создания собственных lifestyle политик. Наша реализация PerLifetimeScopeLifestyle при разрешении зависимости будет создавать новый экземпляр зависимости и сохранять его в слоте данных CallContext. Если компонент ранее был создан, то будет использовать его повторно. В случае явного запроса на освобождение компонента, будет удалять его из слота данных CallContext'а.

public class PerLifetimeScopeLifestyle>TContext> : AbstractLifestyleManager
{
    public class ComponentInstance : IDisposable { ... }
        
    public override void Dispose()
    { }

    public override bool Release(object instance)
    {
        LifetimeScopeStore.Get<TContext>().TryRemove(this);
        return base.Release(instance);
    } 

    private bool ReleaseOnScopeExiting(object instance)
    {
        return base.Release(instance);
    }

    public override object Resolve(CreationContext context)
    {
        return LifetimeScopeStore.Get<TContext>()
            .GetOrAdd(this, lifestyleManager => new ComponentInstance(lifestyleManager, base.Resolve(context)))
            .Instance;
    }
}

Ограничения и недостатки решения

Недостатком является то, что Lifetime scope обязательно должен быть помечен неким тегом. В данном реализации тегом выступает тип TContext, используемый при создании класса LifetimeScope.

// this is supported
// lifetime scope is tagged with Client
using (new LifetimeScope<Client>())
{...}

// this is not supported
using (new LifetimeScope())
{...}

Source code hosted on github

Полная версия проекта (.net4, vs 2010) и исходный код этого решения выложен на github.

 $ git clone git://github.com/samoshkin/ContextualLifetimeScope.git

3 comments:

Alexey Diyan said...

Хорошая статья.

До прочтения не знал о существовании метода .ExternallyOwned() при регистрации сервиса. Про тип Owned знал, а вот про метод .ExternallyOwned() нет.

Возможно в определенных ситуациях он будет кстати.

Пабел Синглит said...

В первый раз слышу о lifestyle. Прочитал раздел "What is lifestyle?", и не нашел там ответа на заявленный вопрос, что же вообще такое lifestyle.

Anonymous said...

Очень хорошая статья. Автор все доходчиво обьяснил

Post a Comment