【问题标题】:How to use a factory with Dependecy Injection without resorting to using Service Locator pattern如何在不使用服务定位器模式的情况下使用具有依赖注入的工厂
【发布时间】:2011-11-17 11:56:22
【问题描述】:

我有一个 GUI 应用程序。在其中,我允许用户从容器提供的算法列表中进行选择。每个算法将在另一个视图中作为后台任务启动。我需要支持这个视图的多个实例,并且支持同一个算法的多个实例。该视图也将由容器提供。该算法也是有状态的。

所以我有一个案例,我需要创建视图和算法的实例,并在运行时将它们绑定在一起。我没有这些实例的静态绑定点,所以我不能使用正常的注入工具(构造函数或属性注入)。我不想打电话给new,也不想像Service Locator那样使用容器。

我在 Castle.Windsor 中使用 Typed Factory Facility 解决了这个问题,但我不得不在我的应用程序中处理工厂问题。工厂设计也有点奇怪,因为我必须在完成后将实例返回工厂。

我现在正在考虑使用 NInject,因为到目前为止学习曲线和介绍文档要好得多,我想提出一个容器供我的团队使用。但是对于这样的场景,我认为我必须编写自己的工厂并直接调用内核来解析新实例(嵌入在工厂中的服务定位器),以及在我的注册代码中添加工厂方法。

是否有通用的方法来解决这个问题,或者这仅仅是依赖注入本身无法解决的问题?


澄清:

我在 cmets 中说过我想要一个关于 Ninject 的具体答案,我已经得到了。非常感谢 :) 在现实生活中,我可能只会使用已经提出的务实解决方案。

但是我把我的基础作为一个具体的问题来解决我的问题。我希望对我的标题中的问题有一个更纯粹的基本答案。

是否有一种纯 DI 技术允许用户在运行时触发组件的新实例?或者所有这些实现都将容器用作服务定位器,或者需要容器的特定“怪癖”(例如内置工厂支持,ala Castle.Windsor 或即将发布的 Ninject 工厂功能),而不是而不是只利用“纯”DI的各个方面?

我只从 Java 世界中听到过这个词,我不太明白它的含义 - 所以请原谅我 :) 我在寻找某种“外射”吗?

【问题讨论】:

  • 也许这更多的是关于programmers.SE的话题?其他人投票了,所以我附议了投票。除非有人可以为我提供一些一般适用于其他 DI 框架的 Ninject 代码:)
  • 我不认为这个问题是题外话。这是一个非常有效的问题 IMO。
  • @Ruben:该答案解决了静态依赖关系,但我有计数 N 的组件,我想在我的应用程序生命周期内“实例化”它们,但从容器中解析。但我想避免了解容器。我想我描述的场景不够好?
  • @Merlyn 不,你描述得很好(因此我的 +1)。这只是一个稍微相关的话题(另一个 Q 也想在应用程序运行期间这样做),这可能会帮助你思考它——而不是 VTC 建议或任何东西。我真的不明白这里的 VTC 是关于什么的。使用Nness 因素,我假设您正在考虑将IEnumerable<IMyComponent> 注入用户代码中的某些内容(或者IEnumerable<IMyComponentFactory>,如果您需要决定某些内容和/或以某种方式单独拆除它们)。无论如何,这些都只是让你决定的输入!

标签: .net dependency-injection ninject factory-pattern


【解决方案1】:

最好创建这样的工厂接口

public interface IFooFactory
{
    IFoo CreateFoo(int someParameter);
}

对于 Ninject 2.3 参见https://github.com/ninject/ninject.extensions.factory 并通过添加以下配置让它由 Ninject 实现。

Bind<IFooFactory>().AsFactory();

对于 2.2,请自行执行。此实现是容器配置的一部分,而不是您的实现的一部分。

public class FooFactory: IFooFactory
{
    private IKernel kernel;
    public FooFactory(IKernel kernel)
    {
        this.kernel = kernel;
    }

    public ISession CreateFoo(int someParameter)
    {
        return this.kernel.Get<IFoo>(
            new ConstructorArgument("someParameter", someParameter));
    }
}

【讨论】:

  • 所以容器必须有明确的支持,并且用户代码必须知道工厂。这就是我的想法,因为我不确定您将如何以其他方式获得新实例。这个工厂扩展如何处理IDisposable?这取决于工厂的消费者吗? (我有点喜欢这样,Castle.Windsor 需要发布方法让我觉得很奇怪)
  • Disposing 的工作方式与调用 kernel.Get() 完全相同(最终这就是幕后发生的事情)。如果你有一个范围,它会在生命周期结束时由 Ninject 处理。对于瞬态绑定,您必须自己处理它。
  • @Merlyn:正如@Mark Seemann 的这本出色的书所说,Windsor 需要调用Release 方法的原因是它跟踪所有创建的对象,包括瞬态对象。这提供了比 Ninject 提供/选择采取/迄今已实施的更详细的控制级别。 Ninject 中的范围/生命周期管理完全不同,最好阅读 Nate Kohari Cache and Collect 文章来理解。查看代码,了解InRequestScope 在想要抢占缓存和收集机制来管理内存使用的情况下如何主动管理释放
【解决方案2】:

我真的很感谢其他人的回答。他们会帮助我解决这个问题。我很可能会接受 Remo 的回答,因为它符合我当前实际面临的问题。

根据我的理解,我还想就我提出的这个更广泛的答案获得反馈。


我不确定依赖注入是否直接支持我刚才谈到的机制,通过构造函数、属性或方法注入。在这一点上,我认为这些是“纯粹的”DI——尽管我愿意被影响。

我认为注入依赖意味着一个相对静态的对象图。它可以从配置文件加载,也可以通过程序生成,但它无法直接适应不可知的运行时状态,例如用户反复请求新实例。

但是,在考虑了其中一些替代方案后,我开始认为有一些解决方法可以支持纯度,而且我所描述的纯度可能并不像我想象的那么重要。一些不太“纯粹”的选项仍然可以以一种基本干净的方式与大多数容器一起使用,并且似乎很容易为容器添加支持以在其余部分清理它们。

这是我目前考虑过的变通方法(其中一些已经提到过)。

参考定制工厂中的容器,然后洗手:

您的组件可以随心所欲地实现。您可以使用任何您想要的容器(只要它支持瞬态实例)。您只需要接受这样一个事实,即您将向代码中注入工厂,并且这些工厂将直接从容器中解析。当你可以务实时,谁需要纯洁?

示例代码:

public class ComponentFactory // Might inherit from an interface...
{
    private readonly IContainer container;

    public ComponentFactory(IContainer container)
    {
        this.container = container;
    }

    public IComponent Create(IOtherComponent otherComponent)
    {
        return container.Get<IComponent>(otherComponent);
    }
}

使用容器特定的工厂扩展:

您的组件可以随心所欲地实现。但是您的容器必须直接支持将工厂注入您的代码,并自动实现这些工厂,这样他们就不必对容器有任何特定的了解。

示例代码:

// Black magic - the container implemented it for us!
// But the container basically implemented our code from the previous example...
public interface IComponentFactory
{
    public IComponent Create(IOtherComponent otherComponent);
}

使用容器特定的对象池:

确保您的组件是无状态的,并允许它们被池化。容器将负责为您分配或池化对象。这或多或少是一个具有奇特实现的托管工厂,您必须同时分配和解除分配。

伪代码(我之前没有使用过基于容器的对象池):

public class SomeUI
{
    private readonly IComponentPool componentPool;

    public OtherComponent(IComponentPool componentPool)
    {
        this.componentPool = componentPool;
    }

    public void DoSomethingWhenButtonPushed()
    {
        var component = componentPool.Get();
        component.DoSomething();
        componentPool.Release(component);
    }
}

此伪代码的优点是您不必为工厂定义接口。缺点是你必须依赖池接口,所以你的容器有它的卷须。我也没有将任何东西传递给Get 方法。这可能是有道理的,因为对象必须支持实例重用。

如果真正的池不是这样工作的,它们可能看起来与上面的“容器特定工厂扩展”示例相同,只是它们总是需要 Release 方法和 Create 方法。

使用享元模式:

Flyweight Pattern - 不确定我是否正确识别了模式,或者我只是有一个奇怪的用途)

注入充当对象行为的无状态组件,或“重量级”组件。使用传递给行为组件的享元状态对象支持组件的单独“实例”,或者让它们包装行为组件。

这将极大地影响组件的架构。您的实现必须是无状态的,并且您的状态对象的设计方式必须使其适用于所有可能的组件实现。但它完全支持“纯”注入模型(只注入到构造函数、属性、方法中)。

但这不适用于 UI。视图类通常需要直接创建,我们不希望我们的“享元”成为 UI 类...

public class ComponentState
{
    // Hopefully can be less generic than this...
    public Dictionary<string, object> Data { get; set; }
}

public interface IComponent
{
    int DoSomething(ComponentState state);
}

public SomeUI
{
    private readonly IComponent component;

    public OtherComponent(IComponent component)
    {
        this.component = component;
    }

    public void DoSomethingWhenButtonPushed()
    {
        var state = new ComponentState();
        component.DoSomething(state);
    }
}

为用户请求的每个新实例使用子容器:

容器在从单个根创建对象图时效果最佳。与其试图与之抗争,不如与之抗争。当用户单击一个按钮来创建算法的新实例时,为这些对象创建一个新的子容器并调用全局配置代码,但这需要完成。然后将子容器附加到父容器。

这意味着生成代码需要在某种程度上了解容器。也许把它包装在工厂里是最好的方法。

public class SubComponentFactory // Might inherit from an interface...
{
    private readonly IContainer container;

    public ComponentFactory(IContainer container)
    {
        this.container = container;
    }

    public IComponent Create(IOtherComponent otherComponent)
    {
        // Todo: Figure out any lifecycle issues with this.
        // I assume the child containers get disposed with the parent container...

        var childContainer = container.CreateChildContainer();
        childContainer.Configure(new SubComponentConfiguration());

        return childContainer.Get<SubComponent>(otherComponent);
    }
}

有点像我们开始的地方。但是我们有一个新的对象图根,所以我不确定我是否可以使用服务定位器模式来调用它。这种方法的问题是与容器耦合最紧密。不仅直接引用容器,而且工厂依赖于容器支持子容器的实现细节。

【讨论】:

  • +1 干得好。有很多人在他们的头脑和架构中带着同样的谜题四处游荡。希望@Mark Seemann 在某个阶段认为适合在该线程上添加一些 cmets - 您在列举您的想法和为您的孩子需要考虑的东西(如果只是这样的话)方面做得很好子系统构建
【解决方案3】:

我总是喜欢给出一般性的答案,而不是过多地介绍特殊容器的功能。你这样说:

  1. 您需要消费者请求新实例。
  2. 必须返回实例(可能会重复使用)。

使用工厂可能是最好的方法。第二个要求可以通过返回IDisposable 对象来解决。然后您可以编写如下代码:

using (var algorithm = this.algoFactory.CreateNew(type))
{
    // use algorithm.
}

有多种方法可以做到,但可以让算法接口实现IDisposable

public interface IAlgorithm : IDisposable
{
}

您可以返回一个装饰器,而不是返回真正的算法,它允许将该实例返回到池中:

public sealed class PooledAlgorithmDecorator : IAlgorithm
{
    private readonly IAlgorithmPool pool;
    private IAlgorithm real;

    public PooledAlgorithmDecorator(IAlgorithm real,
        IAlgorithmPool pool)
    {
        this.real = real;
        this.pool = pool;
    }

    public void Dispose()
    {
        if (this.real != null)
        {
            this.Pool.ReturnToPool(this.real);
            this.real = null;
        }
    }
}

现在你的工厂可以用装饰器包装真正的算法并将其返回给消费者:

public class PooledAlgorithmFactory : IAlgorithmFactory
{
    private readonly IAlgorithmPool pool;

    public PooledAlgorithmFactory(IAlgorithmPool pool)
    {
        this.pool = pool;
    }

    public IAlgorithm CreateNew(string type)
    {
        var instance = this.pool.GetInstanceOrCreateNew(type);

        return new PooledAlgorithmDecorator(instance, this.pool);
    }
}

这当然只是一个例子。您的设计可能会有所不同。根据您选择的容器,您可能会获得对工厂、对象池等的支持。我认为首先找到正确的设计很重要,下一步是看看你的容器是否能帮助你。

【讨论】:

  • +1;您已经很好地解决了这一行:“工厂设计也有点奇怪,因为我必须在完成后将实例返回工厂”。 RAII 绝对是处理这个问题的方法。你的游泳池基本上就是温莎工厂的样子。
  • @Merlyn:这是对您的澄清的回应。从工厂类中调用容器不被视为 Service Locator 模式,只要该工厂实现位于 composition root 中。请参阅 Mark Seemann 的Service Locator: roles vs. mechanics
  • 好东西。我曾考虑让我的代码知道工厂接口,并且只将工厂作为我的“配置”的一部分来实现(听起来像 Composition Root 是正确的术语)。这感觉像是要走的路。看来我需要读那本书:)
猜你喜欢
  • 1970-01-01
  • 2017-03-23
  • 2011-06-04
  • 2011-07-24
  • 2016-08-06
  • 1970-01-01
  • 1970-01-01
  • 2020-12-08
  • 1970-01-01
相关资源
最近更新 更多