使用Ninject绑定默认实现,同时避免可怕的服务定位器反模式

7
使用Ninject(或任何其他IoC容器)创建一个默认绑定,以便在不存在适当的实现时使用此默认绑定,而不是在请求特定绑定时处理ActivationException,这是否可行或是个好主意?
我一直在使用Ninject的FactoryConventions扩展项目,但我想知道它们是否掩盖了我在更基本层面上犯的错误,因此我创建了一个测试,尽可能简单地说明我想做的事情:
给定以下内容:
public interface IWidget { }
public class DefaultWidget : IWidget { }
public class BlueWidget : IWidget { }

以下是使用FluentAssertionsxUnit测试示例:

[Fact]
public void Unknown_Type_Names_Resolve_To_A_Default_Type()
{
    StandardKernel kernel = new StandardKernel();

    // intention: resolve a `DefaultWidget` implementation whenever the 
    // 'name' parameter does not match the name of any other bound implementation
    kernel.Bind<IWidget>().To<DefaultWidget>();
    kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>();

    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
}

我不确定这样做是否可行,如果可以的话,如何去实现服务定位器模式实现不好,但其他人发表了类似问题的答案,也对此提出了警告。
那么,我所要求的是滥用/误用IoC容器吗?
对于所有已知类型,我都应该使用IoC来绑定/解析类型,所以这一部分看起来没有问题。在实际情况中,像BlueWidget这样的显式绑定会有很多种,并且“RedWidget”字符串值的变化数量也不确定。
总的来说,似乎默认实现某些接口的概念并不少见,那么,如果不在IoC容器的范围内,那么解决请求的机制会在哪里呢?
我还计划使用工厂模式来创建IWidget实现。到目前为止,我一直在使用由Ninject.Extensions.Factory自动创建的工厂,配合自定义实例提供程序和自定义绑定生成器,但是我无法解决这个问题。
使用自己的工厂实现是否有助于更好地控制(换句话说,使用我自己的工厂而不是来自Ninject.Extensions.Factory的自动工厂)?过去,我曾经使用程序集反射找到候选类型,并使用Activation.CreateInstance()来创建我需要的特定实现或默认实现,但是一旦这些实现采用面向依赖注入的原则,它们就具有自己的构造函数注入依赖项,因此这变得非常麻烦。因此,我转而使用IoC容器来解决问题-但这并不像我希望的那样奏效。
更新1——使用我的工厂实现成功
我其实对此不太满意,因为每次编写新的IWidget实现时,我都必须打开这个工厂并进行更新。在我的示例中,我还必须在绑定中添加另一行-但是这就需要使用基于约定的绑定,我计划使用该方法以避免不断更新绑定定义。
使用这个工厂实现:
public interface IWidgetFactory { IWidget Create(string name); }
public class WidgetFactory : IWidgetFactory 
{
    private readonly IKernel kernel;
    public WidgetFactory(IKernel kernel) { this.kernel = kernel; }

    public IWidget Create(string name)
    {
        switch (name)
        {
            case "Blue":
                return this.kernel.Get<IWidget>(typeof (BlueWidget).Name);
            default:
                return this.kernel.Get<IWidget>(typeof (DefaultWidget).Name);
        }
    }
}

我可以使这个测试通过:

[Fact]
public void WidgetBuilders_And_Customizers_And_Bindings_Oh_My()
{
    StandardKernel kernel = new StandardKernel();
    kernel.Bind<IWidget>().To<DefaultWidget>().Named(typeof(DefaultWidget).Name);
    kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof (BlueWidget).Name);
    kernel.Bind<IWidgetFactory>().To<WidgetFactory>().InSingletonScope();

    kernel.Get<IWidgetFactory>().Create("Blue")
        .Should().BeOfType<BlueWidget>();
    kernel.Get<IWidgetFactory>().Create("Red")
        .Should().BeOfType<DefaultWidget>();
}

它能够正常工作,但是感觉不对,原因如下:

  1. 我必须将 IKernel 注入到 IWidgetFactory
  2. 对于每个新的 IWidget 实现,都必须更新 IWidgetFactory
  3. 似乎应该已经为这种情况创建了 Ninject 扩展

更新1结束

在这种情况下,你会怎么做,考虑到 IWidget 实现的数量很高,"widget name" 参数的预期范围基本上是无限的,并且所有无法解析的 widget 名称都应该使用 DefaultWidget 处理?

如果您有兴趣,可以继续阅读以下各种测试的完整演变过程:

[Fact]
public void Unknown_Type_Names_Resolve_To_A_Default_Type()
{
    StandardKernel kernel = new StandardKernel();

    // evolution #1: simple as possible
    //      PASSES (as you would expect)
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #2: make the only binding to a different IWidget
    //      FAILS (as you would expect)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #3: add the binding to `BlueWidget` back
    //      ACTIVATION EXCEPTION (more than one binding for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #4: make `BlueWidget` binding a *named binding*
    //      ACTIVATION EXCEPTION (more than one binding for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof (BlueWidget).Name);
    //kernel.Get<IWidget>().Should().BeOfType<BlueWidget>();

    // evolution #5: change `Get<>` request to specifiy widget name
    //      PASSES (yee-haw!)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    //kernel.Get<IWidget>("BlueWidget").Should().BeOfType<BlueWidget>();

    // evolution #6: make `BlueWidget` binding *non-named*
    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>("BlueWidget").Should().BeOfType<BlueWidget>();

    // evolution #7: ask for non-existance `RedWidget`, hope for `DefaultWidget`
    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>();
    //kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>();

    // evolution #8: make `BlueWidget` binding a *named binding* again
    //      ACTIVATION EXCEPTION (**NO matching bindings available** for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    //kernel.Get<IWidget>("RedWidget").Should().BeOfType<DefaultWidget>();

    // evolution #9: remove `RedWidget` specification in Get<> request
    //      ACTIVATION EXCEPTION (back to **more than one** binding for `IWidget`)
    //kernel.Bind<IWidget>().To<DefaultWidget>();
    //kernel.Bind<IWidget>().To<BlueWidget>().Named(typeof(BlueWidget).Name);
    //kernel.Get<IWidget>().Should().BeOfType<DefaultWidget>();
}

@Ruben,感谢您的评论 - 这些测试是否在主要的Ninject GitHub存储库中?我找不到任何名为“NamedBindingsTests”的测试文件,尽管我在“Planning”文件夹中找到了BindingMetadata。 - Jeff
不知道 - 只是根据我以前的探索猜测,2秒钟...似乎https://github.com/ninject/ninject/blob/master/src/Ninject.Test/Integration/StandardKernelTests.cs最接近。我记不清是否有关于默认命名行为的维基,博客文章或者SO答案,但我相信它肯定存在(我个人要么全部命名,要么全部不命名,所以这从未成为一个问题)。 - Ruben Bartelink
这个,但它相当古老,而Remo Gloor的答案包括一个调用.Binding.IsImplicit,这似乎不再存在。我在SO上寻找任何包括“Ninject Default Fallback Binding”等内容的东西,但没有什么有用的结果。我在程序员stackexchange上提出了一个更抽象的问题,看看这是否是一个从一开始就不好的想法。 - Jeff
看起来你走在了正确的轨道上。我经常发现最好的方法是通过阅读测试来摸索如何编写扩展方法,这样我就可以继续我的工作了。然后在6个月的时间里,我通常能够摆脱一些不必要的东西。我敢打赌,如果你的例子更具体一些,或者你需要更多地向自己证明需求的合理性,你现在可能已经放弃了!很抱歉我实际上不能提供帮助! - Ruben Bartelink
@Ruben,我认为通过我的概括方式来澄清问题。这在我的机器上是一个更具体的例子 - 我可以将其作为更新发布。问题是,我不理解Ninject作者的意图,无法识别错误和我对IoC的误用之间的差异。 - Jeff
显示剩余2条评论
1个回答

2

不知道这个方法在哪里排名,但它是有效的。然而你不能通过命名参数来传递参数,只能使用空参数。换句话说,你不能这样做:

var stuff1 = kernel.Get<IWidget>("OrangeWidget");

除非OrangeWidget存在,否则需要执行以下操作才能获取默认值:
var stuff2 = kernel.Get<IWidget>();

这是一个例子:

IDictionary dic = new Dictionary<string, string>();

dic.Add("BlueWidget", "BlueWidget");
dic.Add("RedWidget", "RedWidget");

kernel.Bind<IWidget>().To<DefaultWidget>()
    .When(x => x.Service.Name != (string)dic[x.Service.Name]); 
kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget");

var stuff1 = kernel.Get<IWidget>("BlueWidget");
var stuff2 = kernel.Get<IWidget>();

这篇文章是一篇关于Ninject的很酷的帖子,你可能会感兴趣...

我想补充一些关于命名参数的内容。这基于这份文档。命名参数允许您在获取绑定时按名称调用它们,但它不会“默认”任何内容。所以你实际上必须传递名称DefaultWidget才能获取该绑定。这样做是可行的:

kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget");
kernel.Bind<IWidget>().To<DefaultWidget>().Named("DefaultWidget");

var blueOne = kernel.Get<IWidget>("BlueWidget");
var defaultOne = kernel.Get<IWidget>("DefaultWidget");

如果有人能够弄清楚如何实现默认值,我很想知道,虽然我从未需要过。我是 Ninject 的学生,非常喜欢它。

更新:

我搞定了。在 这里找到了一个很棒的解决方案。

我创建了一个类来扩展 Ninject:

public static class NinjectExtensions
{
    public static T GetDefault<T>(this IKernel kernel)
    {
        return kernel.Get<T>(m => m.Name == null);
    }

    public static T GetNamedOrDefault<T>(this IKernel kernel, string name)
    {
        T result = kernel.TryGet<T>(name);

        if (result != null)
            return result;

        return kernel.GetDefault<T>();
    }
}

这是你的Unknown_Type方法...
public static void Unknown_Type_Names_Resolve_To_A_Default_Type()
{
    StandardKernel kernel = new StandardKernel();

    IDictionary dic = new Dictionary<string, string>();

    dic.Add("BlueWidget", "BlueWidget");
    dic.Add("RedWidget", "RedWidget");

    kernel.Bind<IWidget>().To<DefaultWidget>().When(x => x.Service.Name != (string)dic[x.Service.Name]);
    kernel.Bind<IWidget>().To<BlueWidget>().Named("BlueWidget");

    // this works!
    var sup = kernel.GetNamedOrDefault<IWidget>("Not here");

    var stuff1 = kernel.Get<IWidget>("BlueWidget");
    var stuff2 = kernel.Get<IWidget>();
}

有趣 - 你的第一个例子似乎类似于我的“evolution#5”(请参见我的问题底部的测试演变列表),我能够使用When。(x =>…像你展示的那样通过它而不需要额外的绑定条件。自从我发布了这个问题以来,我开始相信我正在滥用Ninject的功能,试图将该类型特定的默认回退逻辑插入到绑定中,并且当它找不到我请求的特定小部件时,我不得不在我的代码中使用简单的switch语句来检索DefaultWidget - Jeff
这可能更多是一个一般性的设计问题,也许在程序员等网站上有答案,因为这个问题的一部分涉及到Ninject的能力,而另一部分则让人想知道那个逻辑是否应该与任何IoC容器(包括Ninject)耦合。 - Jeff
我实际上使用了一个IOC容器,并将所有的“new”语句放在绑定模块中。然后我像这样调用所有东西:IOCCOntainer.instance.Get<typeofobject>()。我发现它真的可以组织我的代码,我可以通过指向不同的绑定模块来实例化我的模拟对象以切换到测试模式。我确实模拟了所有的服务等,有些人认为第三方模拟更好。这样我的第一次测试很容易。Ninject很容易做到这一点,所以我喜欢它,也因此我会逛Stackoverflow之类的网站学习更多技巧。你的问题很有趣,因为我从不使用默认值。 - CodeChops
请提供您的扩展方法的参考来源,以获取+1分。 - Jeff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接