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



我习惯在web应用程序中使用IoC/DI -主要是使用MVC3。我的控制器是为我创建的,填充了所有的依赖项,子依赖项等。

但是,在厚客户端应用程序中情况就不同了。我必须创建自己的对象,或者我必须恢复到服务定位器风格的方法,在这种方法中,我要求内核(可能通过某些接口,允许可测试性)给我一个具有依赖关系的对象。

然而,我在一些地方看到Service Locator被描述为一种反模式。

所以我的问题是-如果我想从Ninject在我的厚客户端应用程序中受益,有没有更好/更合适的方式来获得这一切?

  • 可测试性
  • 适当的DI/IoC
  • 尽可能少的耦合

请注意,我不只是在这里谈论MVVM和视图模型到视图。这是由于需要从内核中提供一个存储库类型对象,然后从该存储库中获取实体并注入功能(数据当然来自数据库,但它们也需要一些对象作为参数,这取决于世界的状态,Ninject知道如何提供)。我能在不让存储库和实体成为不可测试的混乱的情况下做到这一点吗?

如果有什么不清楚的,让我知道。谢谢!

编辑7月14日

我确信提供的两个答案可能是正确的。然而,我身体的每一根纤维都在与这种变化作斗争;部分原因可能是由于缺乏知识,但也有一个具体的原因,为什么我很难看到这种做事方式的优雅;

我在最初的问题中没有解释得足够好,但事情是我正在编写一个库,将被几个(最初4-5个,以后可能更多)WPF客户端应用程序使用。这些应用程序都在相同的领域模型上运行,所以把它们都放在一个库中是保持DRY的唯一方法。然而,这个系统的客户也有可能编写他们自己的客户端——我希望他们有一个简单、干净的库。我不想强迫他们在他们的组合根中使用DI(使用Mark Seeman在他的书中提到的术语)——因为与他们仅仅新建一个MyCrazySystemAdapter()并使用它相比,这将使事情变得非常复杂。

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

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

TL;DR:交付一个结构合理的API对客户来说太麻烦了。我的API需要交付一个他们可以使用的对象——使用DI和适当的实践在幕后构造。有时候,现实世界胜过了为了忠于模式和实践而向后构建所有东西的愿望。

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


基本上,您应该在app.xaml中构建完整的应用程序。如果有些部分需要稍后创建,因为你还不知道如何在启动时创建它们,那么将工厂作为接口(见下文)或Func(参见Ninject是否支持Func(自动生成工厂)?)注入需要创建该实例的类中。两者都将在下一个Ninject版本中得到原生支持。

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

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

我对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();
    }
}