如何避免使用服务定位器模式?是否应该避免使用?

3

我目前正在开发一个WinForms系统(我知道),在创建表单时有很多构造函数注入,但是如果这些表单/视图需要打开另一个表单,我发现DI容器也已被注入,以便我们可以在运行时定位所需视图接口的实现。例如:

public partial class MyView : Form, IMyView
{
    private readonly IDIContainer _container;

    public MyView(IDIContainer container)
    {
        InitializeComponent();

        _container = container;
    }

    public OpenDialogClick(object sender, EventArgs e)
    {
        var dialog = container.Resolve<IDialogView>();
        dialog.ShowDialog(this);
    }
}

我知道这基本上是将容器用作服务定位器。我一再被告知这被认为是反模式,因此我想避免使用它。

我可能可以像这样在构造函数中注入视图:

public partial class MyView : Form, IMyView
{
    private readonly IDialogView _dialog;

    public MyView(IDialogView dialog)
    {
        InitializeComponent();

        _dialog = dialog;
    }

    public OpenDialogClick(object sender, EventArgs e)
    {
        dialog.ShowDialog(this);
    }
}

但是,如果对话框视图实例化成本很高怎么办?
有人建议我们创建某种表单工厂,该工厂内部使用DI容器,但对我来说,这似乎就像是在另一个服务定位器周围创建一个包装器。
我知道,在某个时候,必须要有“某个东西”知道如何创建IDialogView,因此我认为它要么在组合根创建时被解析(如果有许多表单并且某些或所有表单的创建成本高,则可能不理想),要么组合根本身有一种方式来解决依赖关系。这种情况下,组合根必须具有类似于服务定位器的依赖关系?但是子表单如何创建这样的对话框呢?它们会通过事件向上调用组合体以打开这样的对话框吗?
我经常遇到的一个特殊问题是,容器几乎无法轻松地进行模拟。这正是使我考虑表单工厂想法的一部分,即使它只是一个容器的包装器。这是一个明智的理由吗?
我自己琢磨出了一个难题吗?还有没有简单的方法解决这个问题?还是我要剪断这个难题,找到适合我的东西呢?
4个回答

1

还是我应该采取直接解决问题的方式,找到适合我的东西呢?

工厂类:

public interface IDialogFactory {
    IDialogView CreateNew();
}
// 实现 sealed class DialogFactory: IDialogFactory { public IDialogView CreateNew() { return new DialogImpl(); } }
// 或者单例... sealed class SingleDialogFactory: IDialogFactory { private IDialogView dialog; public IDialogView CreateNew() { if (dialog == null) { dialog = new DialogImpl(); } return dialog; } }

你的代码:

public partial class MyView : Form, IMyView {
   private readonly IDialogFactory factory;
   public MyView(IDialogFactory factory) {
      InitializeComponent();
      //assert(factory != null);
      this.factory = factory;
   }
public OpenDialogClick(object sender, EventArgs e) { using (var dialog = this.factory.CreateNew()) { dialog.ShowDialog(this); } } }

SimpleInjector 的注册

container.RegisterSingle<IDialogFactory, DialogFactory>();

或者使用单例版本
container.RegisterSingle<IDialogFactory, SingleDialogFactory>();

container.RegisterSingle<IMyView, MyView>();

这对他来说没有任何帮助,我猜。现在,要创建MyView的实例,他仍然需要容器来解决依赖关系。相反,工厂应该是一个具体的类,具有可插拔的提供者。MyView应该直接使用工厂,无需注入它。工厂提供者在组合根中设置。 - Wiktor Zychla
很抱歉,我不明白为什么“要创建MyView的实例,他仍然需要容器来解决依赖关系”。如果不使用容器,我可以这样写:var fac = new SingleFactory(); var myView = new MyView(fac); - o3o
这将违背根据接口注册实现的思想。客户端如何知道IDialogFactory已经注册到了SingleFactory - Wiktor Zychla

1
你绝对不想在整个应用程序中使用DI容器。你的DI容器只应该是组合根的一部分。然而,你可以有一个工厂在组合根内使用DI容器。因此,如果Program.cs是你连接一切的地方,你可以在那里简单地定义该工厂类。
WinForms并不考虑DI;窗体是通过默认构造函数生成的。这可能或可能不是一个问题,这取决于你使用哪个DI容器。
我认为,在这种情况下,使用构造函数注入在WinForms中会比使用服务定位器遇到的任何问题更加痛苦。在组合根(Program.cs)中声明一个静态方法以包装调用DI容器以解决你的引用不是件丢脸的事。

1
我非常了解这个问题。关于解决方案,我学到的所有知识(我学到了很多)都或多或少地掩盖了服务定位器。

1
一家本地工厂对使用容器并在组合根中设置的实现感到满意,这不是服务定位器,而是依赖项解析器。
区别如下:定位器在容器定义附近定义和满足。要使用定位器,需要对容器基础结构进行外部引用。这会导致项目依赖于外部依赖项(定位器)。
另一方面,依赖项解析器局限于项目范围内。它用于满足其紧密邻近的依赖关系,但不依赖于任何外部内容。
组合根不应用于解决实际特定的依赖关系,例如您所担心的依赖关系。相反,组合根应该设置所有这些本地依赖项解析器的实现,这些解析器在整个应用程序中使用。局部解析器越多越好- MVC的构造函数工厂是一个很好的例子。另一方面,WebAPI的解析器处理了许多不同的服务,仍然是一个很好的解析器-它在WebAPI基础设施中是本地的,不依赖于任何东西(相反,其他WebAPI服务依赖于它),并且可以以任何可能的方式实现并设置在组合根中。
我之前写过一篇博客文章。

http://www.wiktorzychla.com/2012/12/di-factories-and-composition-root.html

在那里,您将找到讨论您的问题以及如何设置工厂(也称解析器)的示例。


很遗憾,由于“恶意软件的不良声誉”,我的组织已经将您的网站屏蔽了 :( - adhocgeek
有趣的是,一个博客怎么可能会因为恶意软件而拥有“不良声誉”?这绝对是在你们组织方面的问题。我无法对此做任何事情。 - Wiktor Zychla
肯定是我的公司防火墙的问题,是的。有趣的是现在似乎可以加载了...也许是你网站上的广告有关? - adhocgeek

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