Как работать с параметрами времени выполнения при использовании определения времени жизни? - PullRequest
31 голосов
/ 17 февраля 2012

Предупреждение, длинный пост впереди.

В последнее время я много думал об этом и изо всех сил пытаюсь найти здесь удовлетворительное решение. Я буду использовать C # и autofac для примеров.

Проблема

IoC отлично подходит для построения больших деревьев сервисов без сохранения состояния. Я разрешаю сервисы и передаю данные только вызовам методов. Отлично.

Иногда я хочу передать параметр данных в конструктор службы. Вот для чего нужны фабрики. Вместо разрешения службы я разрешаю ее фабрику и вызываю метод create с параметром для получения службы. Немного больше работы, но хорошо.

Время от времени я хочу, чтобы мои службы разрешались к одному и тому же экземпляру в определенной области. Autofac предоставляет InstancePerLifeTimeScope(), что очень удобно. Это позволяет мне всегда разрешать один и тот же экземпляр в поддереве выполнения. Хорошо.

И бывают случаи, когда я хочу объединить оба подхода. Я хочу, чтобы параметр данных в конструкторе, и экземпляры имеют границы. Я не нашел удовлетворительного способа достичь этого.

* Решения 1016 * 1. Метод инициализации Вместо передачи данных в конструктор, просто передайте их методу Initialize. Интерфейс: interface IMyService { void Initialize(Data data); void DoStuff(); } Класс: class MyService : IMyService { private Data mData; public void Initialize(Data data) { mData = data; } public void DoStuff() { //... } } Регистрация: builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); Использование: var myService = context.Resolve<IMyService>(); myService.Init(data); // somewhere else var myService = context.Resolve<IMyService>(); После первого разрешения службы и вызова Initialize я могу с радостью разрешить в том же контексте и получить тот же инициализированный экземпляр. Мне не нравится тот факт, что перед вызовом Initialize у меня есть непригодный объект. Существует опасность, что экземпляр будет разрешен и использован где-то еще, прежде чем я вызову Initialize (). 2. Держатель выкройки Это шаблон, который содержит ссылку на объект данных, и вместо инъекции самого объекта данных я внедряю объект-держатель. Интерфейс: interface IMyService { void DoStuff(); } Класс: class MyService : IMyService { private Data mData; public MyService(IDataHolder dataHolder) { mData = dataHolder.Data; } public void DoStuff() { //... } } Регистрация: builder.RegisterType<MyService>().As<IMyService>(); builder.RegisterType<DataHolder>().As<IDataHolder>().InstancePerLifetimeScope(); Использование: var holder = context.Resolve<IDataHolder>(); holder.Data = data; // somewhere else var myService = context.Resolve<IMyService>(); Это немного лучше, так как я перенес ответственность за хранение экземпляра в другой класс. Теперь я могу использовать держатель в других службах. Другое преимущество заключается в том, что я могу при необходимости выполнять горячую замену данных в держателе. Мне не нравится тот факт, что он запутывает код и добавляет еще один интерфейс, который я должен смоделировать во время тестирования. 3. Пусть контейнер содержит экземпляр Интерфейс: interface IMyService { void DoStuff(); } Класс: class MyService : IMyService { private Data mData; public MyService(Data data) { mData = dataHolder.Data; } public void DoStuff() { //... } } Регистрация: builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); Использование: var myServiceFactory = context.Resolve<Func<Data, IMyService>>(); myServiceFactory(data); // somewhere else var myService = context.Resolve<IMyService>(); Это верно. Я нигде не храню результат заводского вызова, потому что автофак хранит его для меня. Это довольно удивительно для всех, кто будет читать код. Я не уверен, что автофак даже был предназначен для такого использования. Приятно то, что мне не нужен ни дополнительный метод инициализации, ни дополнительный класс для хранения экземпляра. Вопрос

Что вы думаете об этом? Как вы справляетесь с ситуацией с параметрами данных времени выполнения и областью видимости? Я пропускаю лучший подход?

Ответы [ 5 ]

4 голосов
/ 22 ноября 2016

Autofac теперь поддерживает это "из коробки" с расширением на все время жизни.Метод BeginLifetimeScope() имеет перегрузку, которая принимает Action<ContainerBuilder>, что позволяет добавлять новые регистрации, относящиеся только к этой области действия.Так что для данного примера это будет выглядеть примерно так:

var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
var container = builder.Build();

using(var scope = container.BeginLifetimeScope(
  builder =>
  {
    builder.RegisterInstance(new Data(....));
  }))
{
  // References to 'IMyService' will always be resolved to the same instance within this lifetime scop
  // References to 'Data' will be resolved to the instance registered just for this lifetime scope.
  var svc = scope.Resolve<IMyService>();
}
3 голосов
/ 21 мая 2015

В большинстве случаев данные времени выполнения - это нестатическая информация, которую нужно передавать в любом процессе, например, x в математической функции, поэтому самый простой способ справиться с этим - использовать параметр в функции:

class MyService : IMyService
{
    public MyService(){}

    public void DoStuff(Data mData)
    {
        //...
    }
}

var myService = context.Resolve<IMyService>();
myService.DoStuff(data);

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

1.- Если вы не потеряете объем данных времени выполнения в каждом разрешении, которое вы можете разрешить с помощью TypedParameter:

Ej:

//initilization
var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
var container = builder.Build();

//any point of your app
Data mData = new Data("runtimeData"); // must to be accesible in every place you Resolve

using(var scope = container.BeginLifetimeScope())
{

  var service = scope.Resolve<IMyService>(new TypedParameter(typeof(Data), mData));
service.DoStuff();
}

using(var scope = container.BeginLifetimeScope())
{

  var service2 = scope.Resolve<IMyService>(new TypedParameter(typeof(Data), mData));
service2.DoStuff();
}

2.- Если у вас нет ссылки на данные времени выполнения во всех местах, которые вы решаете, вы можете RegisterInstance , когда и где вы создаете данные времени выполнения. Autofac должен инъектировать mData экземпляр благодаря политике прямой зависимости

//initilization
var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
var container = builder.Build();

//where you create or modify runtime data. When runtime data changes you have to update the container again.
var mData = new Data("runtimeData");
updatedBuilder= new ContainerBuilder();
updatedBuilder.RegisterInstance(mData).As<Data>
updatedBuilder.Update(builder);

//in any point of your app
using(var scope = updatedBuilder.BeginLifetimeScope())
    {

      var service = scope.Resolve<IMyService>();
    service.DoStuff();
    }

//in any other point of your app
    using(var scope = updatedBuilder.BeginLifetimeScope())
    {

      var service2 = scope.Resolve<IMyService>();
    service2.DoStuff();
    }
2 голосов
/ 22 мая 2015

Многие платформы IoC поддерживают регистрацию фабричной функции (или лямбда-выражения), которая принимает в качестве одного из аргументов экземпляр самого контекста контейнера / области видимости / разрешения.

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

Принцип

ДляAutoFac и ваш конкретный пример, следующий принцип будет работать, используя дополнительные уровни косвенности в регистрации.

// Inject `Data` instance resolved from current scope.
builder.Register<IMyService>(ctx => new MyService(ctx.Resolve<Data>()));

// Extra level of indirection, get a "factory" for a 'Data' instance.
builder.Register<Data>(ctx => ctx.Resolve<Func<Data>>()()).InstancePerLifetimeScope();

// The indirection resolves to a map of scopes to "factory" functions. 
builder.Register<Func<Data>>(ScopedDataExtensions.GetFactory);

Мы можем использовать любое доступное уникальное свойство в контексте / области действия для построения этого отображения.

// Maps scopes to data "factories".
public static class ScopedDataExtensions
{
    private static readonly ConcurrentDictionary<object, Func<Data>> _factories = new ConcurrentDictionary<object, Fund<Data>>();

    public static Func<Data> GetFactory(this IComponentContext ctx) 
    {
        var factory = default(Func<Data>);
        return _factories.TryGetValue(ctx.ComponentRegistry, out factory) ? factory : () => null;
    }
    public static void SetFactory(this ILifetimeScope scope, Func<Data> factory)
    {
        _factories[scope.ComponentRegistry] = factory;
    }
}

Мы можем использовать его таким образом, чтобы предоставлять «локальные» экземпляры данных, которые будут внедрены в наши экземпляры сервисов с определенной областью.

var myData = new Data("nested");
nestedScope.SetFactory(() => myData);
// ...
var myService = nestedScope.Resolve<IMyService>();

Более полный и общий пример для AutoFac приведен ниже.

Общий класс расширения для этого шаблона

public static class AutofacScopeExtensions
{
    // Map from context => factories per type
    public static readonly ConcurrentDictionary<object, ConcurrentDictionary<Type, object>> _factories =
        new ConcurrentDictionary<object, ConcurrentDictionary<Type, object>>();

    private static class ScopedFactoryFor<T>
    {
        public static Func<T> DefaultFactory = () => default(T);
        public static Func<T> GetFactory(ConcurrentDictionary<Type, object> fromContext)
        {
            object factory;
            return (fromContext.TryGetValue(typeof(T), out factory)) ? (Func<T>)factory : DefaultFactory;
        }
    }

    public static IRegistrationBuilder<T, SimpleActivatorData, SingleRegistrationStyle> 
        WithContextFactoryFor<T>(this ContainerBuilder builder, Func<T> defaultFactory = null)
    {
        if (defaultFactory != null)
            ScopedFactoryFor<T>.DefaultFactory = defaultFactory;
        builder.Register<Func<T>>(AutofacScopeExtensions.GetFactory<T>);
        return builder.Register<T>(ctx => ctx.Resolve<Func<T>>()());
    }
    public static IContainer BuildContainer(this ContainerBuilder builder)
    {
        var container = builder.Build();
        container.ChildLifetimeScopeBeginning += OnScopeStarting;
        return container;
    }
    public static ILifetimeScope SetScopeFactory<T>(this ILifetimeScope scope, Func<T> factory)
    {
        ScopeMapFor(scope)[typeof(T)] = factory;
        return scope;
    }
    public static ILifetimeScope SetScopeValue<T>(this ILifetimeScope scope, T instance)
    {
        return SetScopeFactory(scope, () => instance);
    }
    public static Func<T> GetFactory<T>(IComponentContext ctx)
    {
        return ScopedFactoryFor<T>.GetFactory(ScopeMapFor(ctx));
    }

    private static ConcurrentDictionary<Type, object> ScopeMapFor(IComponentContext ctx)
    {
        return _factories.GetOrAdd(ctx.ComponentRegistry, x => new ConcurrentDictionary<Type, object>());
    }
    private static void OnScopeStarting(object sender, LifetimeScopeBeginningEventArgs evt)
    {
        evt.LifetimeScope.ChildLifetimeScopeBeginning += OnScopeStarting;
        evt.LifetimeScope.CurrentScopeEnding += OnScopeEnding; // so we can do clean up.
    }
    private static void OnScopeEnding(object sender, LifetimeScopeEndingEventArgs evt)
    {
        var map = default(ConcurrentDictionary<Type, object>);
        if (_factories.TryRemove(evt.LifetimeScope.ComponentRegistry, out map))
            map.Clear();
    }
}

Допускает следующий синтаксис для регистрации:

builder.WithContextFactoryFor<Data>(() => new Data("Default")).InstancePerLifetimeScope();
builder.Register<IMyService>(ctx => new MyService(ctx.Resolve<Data>()));

И разрешается следующим образом:

// ...
var myData = new Data("Some scope");
// ...
context.SetScopeFactory(() => myData);

// ...

// Will inject 'myData' instance.
var myService = context.Resolve<IMyService>();

Упрощенная альтернатива

Если вы явно запускаете вложенные области и в то же время вы знаете, как должен создаваться экземпляр Data с областью действия, вы можете пропустить класс расширения и зарегистрировать «фабричный» делегат с вложенной областью действия при создании:

var nestedScope = container.BeginLifetimeScope(
    "L2", 
    x => x.RegisterInstance<Func<Data>>(() => new Data("nested")));
2 голосов
/ 21 мая 2015

Я полагаю, что вы сделали так хорошо, как только можете.Единственный недостаток, который у меня есть по этому поводу, заключается в том, что Autofac на самом деле не очень хорошо помогает вам управлять этими прицельными областями, поэтому вы застряли, называя их BeginLifetimeScope где-то. И они могут быть вложенными .Mind Blown.

С другой стороны, Ninject делает действительно классные вещи, которые не требуют выворачивания вашего мозга наизнанку.Их именованное расширение области действия позволяет вам создать ( gasp ) именованную область действия и связать время жизни объектов в этой области действия.Если вы используете фабрики (разумеется, судя по вопросу), вы также захотите использовать расширение для сохранения контекста, чтобы материал, активированный на фабриках, получал управление временем жизни из названной области действия, в которой была активирована фабрика.Привязки в конечном итоге выглядят примерно так:

var scopeName = "Your Name Here";

Bind<TopLevelObject>().ToSelf().DefinesNamedScope(ScopeName);
Bind<ISomeScopedService>().To<SomeScopedService>().InNamedScope(ScopeName);
// A minor bit of gymnastics here for factory-activated types to use
//  the context-preservation extension.
Bind<FactoryActivatedType>().ToSelf().InNamedScope(ScopeName);
Bind<IFactoryActivatedType>().ToMethod(x => x.ContextPreservingGet<FactoryActivatedType>());

Приятно то, что область этих привязок специально связана с именованной областью , а не просто привязана к какой-либоБлижайшая продолжительность жизни вверх по цепочке.ИМХО, это делает время жизни этих объектов намного более предсказуемым.

1 голос
/ 22 мая 2015

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

Это реализовано в замке Виндзор с фабричным набором

Примеры классов, которые мы хотим разрешить:

public interface IMyService
{
    void Do();
}

public class MyService : IMyService
{
    private readonly Data _data;
    private readonly IDependency _dependency;

    public MyService(Data data, IDependency dependency)
    {
        _data = data;
        _dependency = dependency;
    }

    public void Do()
    {
        throw new System.NotImplementedException();
    }
}

public class Data
{    
}

public interface IDependency
{         
}

public class Dependency : IDependency
{
}

Мы создаем фабричный интерфейс:

public interface IMyServiceFactory
{
    IMyService Create(Data data);
    void Release(IMyService service);
}

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

Затем мы выполняем регистрацию и пытаемся разрешить значения.

[Test]
public void ResolveByFactory()
{
    WindsorContainer container = new WindsorContainer();
    container.AddFacility<TypedFactoryFacility>();
    container.Register(Component.For<IMyServiceFactory>().AsFactory());
    container.Register(Component.For<IMyService>().ImplementedBy<MyService>().LifestyleScoped());
    container.Register(Component.For<IDependency>().ImplementedBy<Dependency>().LifestyleScoped());

    IMyServiceFactory factory = container.Resolve<IMyServiceFactory>();

    IMyService myService1;
    IMyService myService2;

    using (container.BeginScope())
    {
        myService1 = factory.Create(new Data());
        myService2 = factory.Create(new Data());

        myService1.Should().BeSameAs(myService2);
    }

    using (container.BeginScope())
    {
        IMyService myService3 = factory.Create(new Data());

        myService3.Should().NotBeSameAs(myService1);
        myService3.Should().NotBeSameAs(myService2);
    }
} 

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

...