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

8
我习惯在Web应用程序中使用IoC/DI - 主要是Ninject与MVC3一起使用。我的控制器已经为我创建,所有依赖项都已就位,包括子依赖项等。
然而,在厚客户端应用程序中情况不同。我必须创建自己的对象,或者我必须回到服务定位器样式的方法,其中我通过某个接口(可能是为了可测试性)要求内核给我一个完整的带有依赖项的对象。
但是,我已经看到有几个地方将服务定位器描述为反模式。
因此,我的问题是 - 如果我想在我的厚客户端应用程序中受益于Ninject,是否有更好/更适当的方法来完成所有这些操作?
测试性 适当的DI / IoC 尽可能少的耦合
请注意,我不仅谈论MVVM并将视图模型放入视图中。这是特别由需要从内核提供存储库类型对象触发的,并且然后从该存储库检索注入了功能的实体(当然,数据来自数据库,但还需要根据世界状态提供一些对象作为参数,而Ninject知道如何提供)。我是否可以在不使存储库和实体成为无法测试的混乱的情况下完成此操作?
如果有任何不清楚的地方,请告诉我。谢谢!
编辑7月14日
我确信提供的两个答案可能是正确的。但是,我的每一个细胞都在抵制这种变化;其中一部分可能是由于缺乏知识,但还有一个具体原因,我很难看到这种做事方式的优雅之处;
我没有在原始问题中足够清楚地解释这一点,但是事实是,我正在编写一个将由几个(首先是4-5,以后可能更多)WPF客户端应用程序使用的库。这些应用程序都在同一领域模型等上运行,因此将其全部放在一个库中是保持DRY的唯一方法。但是,该系统的客户还有可能编写自己的客户端 - 我希望他们拥有一个简单,干净的库来进行交流。我不想强迫他们在组合根中使用DI(像Mark Seeman在他的书中使用的那样)-因为与他们只是新建一个MyCrazySystemAdapter()并使用它相比,这会大大复杂化事情。
现在,MyCrazySystemAdapter(名称选择因为我知道人们会不同意我)需要由子组件组成,并使用DI组合。 MyCrazySystemAdapter本身不应该需要注入。它是客户端与系统交流的唯一接口。因此,客户端应该愉快地得到其中之一,DI在幕后自动完成,并且使用最佳实践和原则组成了许多不同的对象。
我知道这种想法可能会引起争议。但我也了解将成为此API客户的人员。如果他们发现需要学习和连接DI系统,并在应用程序入口点(组合根)中预先创建整个对象结构,而不是新建单个对象,他们会对我竖中指,直接与数据库打交道,并以你难以想象的方式搞砸事情。
简而言之,提供一个正确构造的API对客户来说太麻烦了。我的API需要提供一个单一的对象-在幕后使用DI和适当的实践构造-供他们使用。现实有时会胜过为了保持模式和实践而反向构建所有内容的愿望。
2个回答

5
我建议您看一下像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>();
    }
}

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

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

如果您有更多的表单并且想要在那里使用工厂,您只需要像在主表单中一样将工厂注入这些表单,也要在启动时注册附加表单,并使用工厂从主表单打开它们。

这是您想知道的吗?


编辑:
Ruben,你当然是对的。我的错误。
我回答中的整个内容都是一个我曾经留在某个地方的旧例子,但当我发布回答时,我很匆忙,没有仔细阅读旧例子的上下文。

我的旧例子包括一个主表单,从中您可以打开应用程序的任何其他表单。 这就是工厂的作用,因此您不必通过构造函数注入每个其他表单到主表单中。
相反,您可以使用工厂打开任何新表单:

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

4
你的 GetStuff() 方法像是穿着礼服的服务定位器,不要这么做。(你回答中的其他部分没有问题,但服务定位器的特性使得它们无关紧要。) - Ruben Bartelink
即使在看到Remo的答案之前(我相信那是正确的方法),我也打算建议注入工厂(如适当的Funcs,类似于https://dev59.com/QW445IYBdhLWcg3wdqSO#4851885)。 - Ruben Bartelink

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