33

警告,前方长篇大论。

我最近一直在思考这个问题,我正在努力在这里找到一个令人满意的解决方案。我将使用 C# 和 autofac 作为示例。

问题

IoC 非常适合构建大型无状态服务树。我解析服务并将数据仅传递给方法调用。伟大的。

有时,我想将数据参数传递给服务的构造函数。这就是工厂的用途。我没有解析服务,而是解析了它的工厂并使用参数调用 create 方法来获取我的服务。更多的工作,但还可以。

有时,我希望我的服务在一定范围内解析到同一个实例。Autofac 提供了InstancePerLifeTimeScope()非常方便的功能。它允许我始终解析到执行子树中的同一个实例。好的。

有时我想将这两种方法结合起来。我想要构造函数中的数据参数并有实例范围。我还没有找到令人满意的方法来实现这一点。

解决方案

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>();

这是正确的。我不会在任何地方存储工厂调用的结果,因为 autofac 会为我存储它。这对于任何会阅读代码的人来说都是非常令人惊讶的。我不确定 autofac 是否甚至打算像这样使用。这样做的好处是我既不需要额外的初始化方法,也不需要额外的类来保存实例。

问题

你对此有什么看法?您如何处理运行时数据参数和生命周期范围的情况?我错过了更好的方法吗?

4

5 回答 5

6

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>();
}
于 2016-11-22T08:50:44.837 回答
3

大多数时候,运行时数据是您需要在任何进程中传递的非静态信息,例如x在数学函数中,因此处理它的最简单方法是在函数中使用参数:

class MyService : IMyService
{
    public MyService(){}

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

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

但是,假设您的示例只是一个示例,并且您之所以问是因为您的类需要保留运行时数据以运行更多进程,并且您不想在每个函数中传递相同的参数:

1.- 如果您不放松每个 Resolve 中运行时数据的范围,您可以使用以下方法解决TypedParameter

乙:

//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.- 如果您在解析的每个地方都没有对运行时数据的引用,您可以在何时何地创建运行时数据时注册实例。Autofac 应该通过Direct Depency PolicymData引入实例

//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();
    }
于 2015-05-21T09:13:32.003 回答
2

我对此的看法是,你已经尽了最大的努力。我对此的唯一烦恼是 Autofac 在帮助您管理这些生命周期范围方面并没有真正做得很好,因此您只能在BeginLifetimeScope某个地方调用它们。而且它们可以嵌套脑洞大开。

另一方面,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>());

关于这一点的好处是,这些绑定的范围专门与命名范围相关联,而不仅仅是与链上最近的生命周期范围相关联。恕我直言,它使这些对象的生命周期更加可预测。

于 2015-05-21T18:36:21.947 回答
2

许多 IoC 框架支持注册工厂函数(或 lambda 表达式),该函数将容器/范围/解析上下文本身的实例作为其参数之一。

这允许使用额外的间接级别,以及使用唯一标识上下文或范围的信息。此外,许多提供挂钩,如事件处理程序或从生命周期范围类派生的选项,以与正在启动或结束的范围进行交互。

原则

对于 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")));
于 2015-05-21T21:47:28.820 回答
1

如果我理解正确,您希望通过将对象创建委托给容器同时将一些参数传递给其构造函数来使用工厂。

这是在带有类型工厂设施的 Castle Windsor 中实现的。

我们要解析的示例类:

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);
    }
} 

您将看到在同一范围内创建的对象是相同的引用。让我知道这是否是您想要的行为。

于 2015-05-21T22:38:36.550 回答