在Windows客户端(WPF)应用程序中执行依赖注入的正确方法

时间:2011-07-09 11:59:51

标签: dependency-injection ninject service-locator testability thick-client

我习惯于Web应用程序中的IoC / DI - 主要是带有MVC3的Ninject。我的控制器是为我创建的,充满了所有依赖关系,子依赖关系等。

但是,胖客户端应用程序的情况有所不同。我必须创建自己的对象,或者我必须恢复到服务定位器样式的方法,我要求内核(可能通过一些接口,允许可测试性)给我一个完整的依赖项对象。

但是,我已经看到Service Locator被描述为反模式的几个地方。

所以我的问题是 - 如果我想在我的胖客户端应用程序中受益于Ninject,是否有更好/更合适的方法来获取所有这些?

  • 可测试性
  • 正确的DI / IoC
  • 可能的耦合量最少

请注意我不只是在这里讨论MVVM并将视图模型放入视图中。这特别是需要从内核提供存储库类型对象,然后从该存储库中提取的实体注入了功能(当然数据来自数据库,但它们也需要一些对象作为参数,具体取决于状态世界,Ninject知道如何提供)。我可以以某种方式做到这一点,而不会将存储库和实体都作为不可测试的混乱吗?

如果有任何不清楚的地方,请告诉我。谢谢!

编辑7月14日

我确信提供的两个答案可能是正确的。然而,我身体的每一根纤维都在对抗这种变化;其中一些可能是由于缺乏知识造成的,但也有一个具体原因导致我无法看到这种做事方式的优雅;

我在原始问题中没有充分解释这个问题,但问题是我正在编写一个将由几个(最初的4-5个,可能更晚)WPF客户端应用程序使用的库。这些应用程序都在相同的域模型等上运行,因此将它们保存在一个库中是保持DRY的唯一方法。但是,该系统的客户也有可能编写自己的客户端 - 我希望他们有一个简单,干净的库来与之交谈。我不想强迫他们在他们的作文根中使用DI(在他的书中使用像Mark Seeman这样的术语) - 因为与他们刚刚创建MyCrazySystemAdapter()并使用它相比,HUGELY使事情变得复杂。

现在,MyCrazySystemAdapter(因为我知道人们会在这里不同意我选择的名称)需要由子组件组成,并使用DI组合在一起。 MyCrazySystemAdapter本身不需要注入。它是客户端与系统通信所需的唯一接口。所以一个客户应该得到其中一个,DI就像幕后的魔术一样,对象由许多不同的对象使用最佳实践和原则组成。

我确实意识到这将成为一种想要做事的有争议的方式。但是,我也知道将成为此API客户的人员。如果他们发现他们需要学习并连接DI系统,并在他们的应用程序入口点(Composition Root)中提前创建他们的整个对象结构,而不是新建一个对象,他们会给我中指和直接搞乱数据库并以你难以想象的方式搞砸了。

TL; DR:提供结构合理的API对客户来说太麻烦了。我的API需要提供一个单独的对象 - 使用DI和适当的实践在幕后构建 - 他们可以使用。现实世界有时候强调了为了保持模式和实践的真实性而向后建造一切的愿望。

2 个答案:

答案 0 :(得分:5)

我建议看一下像Caliburn这样的MVVM框架。它们提供与IoC容器的集成。


基本上,您应该在app.xaml中构建完整的应用程序。如果稍后需要创建某些部分,因为您还不知道在启动时创建它们的所有内容,那么将工厂注入接口(参见下文)或Func(请参阅Does Ninject support Func (auto generated factory)?)到需要创建此实例的类中。两者都将在下一个Ninject版本中得到本地支持。

e.g。

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

    public IFoo CreateFoo()
    {
        this.kernel.Get<IFoo>();
    }
}

请注意,工厂实现在逻辑上属于容器配置,而不属于业务类的实现。

答案 1 :(得分:1)

我对WPF或MVVM一无所知,但你的问题基本上是关于如何在不使用服务定位器(或直接容器)的情况下从容器中取出东西,对吧? 如果是的话,我可以给你举个例子。

关键是你使用的是工厂,它在内部使用容器。这样,您实际上只在一个地方使用容器。

注意:我将使用WinForms的示例而不是绑定到特定容器(因为,正如我所说,我不知道WPF ... 我使用Castle Windsor而不是NInject),但由于你的基本问题并没有特定地与WPF / NInject相关联,所以你应该很容易将我的答案“移植”到WFP / NInject。

工厂看起来像这样:

public class Factory : IFactory
{
    private readonly IContainer container;

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

    public T GetStuff<T>()
    {
        return (T)container.Resolve<T>();
    }
}

您的应用的主要形式通过构造函数注入来获取此工厂:

public partial class MainForm : Form
{
    private readonly IFactory factory;

    public MainForm(IFactory factory)
    {
        this.factory = factory;
        InitializeComponent();  // or whatever needs to be done in a WPF form
    }
}

应用程序启动时初始化容器,并解析主窗体(因此它通过构造函数注入获取工厂)。

static class Program
{
    static void Main()
    {
        var container = new Container();
        container.Register<MainForm>();
        container.Register<IFactory, Factory>();
        container.Register<IYourRepository, YourRepository>();

        Application.Run(container.Resolve<MainForm>());
    }
}

现在,主窗体可以使用工厂从容器中获取存储库等内容:

var repo = this.factory.GetStuff<IYourRepository>();
repo.DoStuff();

如果你有更多的表格,并且也想从那里使用工厂,你只需要将工厂注入这些表格,就像进入主表格一样,在启动时注册其他表格,然后从主表格中打开它们与工厂合作。

这是你想知道的吗?


修改
鲁本,当然你是对的。我的错误。
我的答案中的所有内容都是我在某个地方撒谎的一个老例子,但是当我发布我的答案并且没有仔细阅读我的旧例子的背景时,我很着急。

我的旧示例包括一个主表单,您可以从中打开任何其他形式的应用程序。 那是工厂的用途,因此您不必通过构造函数注入将每个其他表单注入主窗体。
相反,您可以使用工厂打开任何新表单:

var form = this.factory.GetStuff<IAnotherForm>();
form.Show();

当然,只要通过构造函数注入将存储库传递给表单,您就不需要工厂只从表单中获取存储库。 如果你的应用程序只包含几个表单,你根本不需要工厂,你也可以通过构造函数注入传递表单:

public partial class MainForm : Form
{
    private readonly IAnotherForm form;

    // pass AnotherForm via constructor injection
    public MainForm(IAnotherForm form)
    {
        this.form = form;
        InitializeComponent();  // or whatever needs to be done in a WPF form
    }

    // open AnotherForm
    private void Button1_Click(object sender, EventArgs e)
    {
        this.form.Show();
    }
}

public partial class AnotherForm : Form
{
    private readonly IRepository repo;

    // pass the repository via constructor injection
    public AnotherForm(IRepository repo)
    {
        this.repo= repo;
        InitializeComponent();  // or whatever needs to be done in a WPF form

        // use the repository
        this.repo.DoStuff();
    }
}