Seemann的依赖注入,“三次调用模式”与服务定位器反模式

3

我使用依赖注入(Dependency Injection, DI)和Ninject作为DI容器创建了一个WinForms MVC应用程序。基本架构如下:

Program.cs(WinForms应用程序的主入口点):

static class Program
{
    [STAThread]
    static void Main()
    {
        ...

        CompositionRoot.Initialize(new DependencyModule());
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(CompositionRoot.Resolve<ApplicationShellView>());
    }
}

DependencyModule.cs

public class DependencyModule : NinjectModule
{
    public override void Load()
    {
        Bind<IApplicationShellView>().To<ApplicationShellView>();
        Bind<IDocumentController>().To<SpreadsheetController>();
        Bind<ISpreadsheetView>().To<SpreadsheetView>();
    }
}

CompositionRoot.cs

public class CompositionRoot
{
    private static IKernel ninjectKernel;

    public static void Initialize(INinjectModule module)
    {
        ninjectKernel = new StandardKernel(module);
    }

    public static T Resolve<T>()
    {
        return ninjectKernel.Get<T>();
    }

    public static IEnumerable<T> ResolveAll<T>()
    {
        return ninjectKernel.GetAll<T>();
    }
}

ApplicationShellView.cs(应用程序的主窗体)

public partial class ApplicationShellView : C1RibbonForm, IApplicationShellView
{
    private ApplicationShellController controller; 

    public ApplicationShellView()
    {
        this.controller = new ApplicationShellController(this);
        InitializeComponent();
    }

    public void InitializeView()
    {
        dockPanel.Extender.FloatWindowFactory = new CustomFloatWindowFactory();
        dockPanel.Theme = vS2012LightTheme;
    }

    private void ribbonButtonTest_Click(object sender, EventArgs e)
    {
        controller.OpenNewSpreadsheet();
    }

    public DockPanel DockPanel
    {
        get { return dockPanel; }
    }
}

在哪里

public interface IApplicationShellView
{
    void InitializeView();
    DockPanel DockPanel { get; }
}

ApplicationShellController.cs

public class ApplicationShellController
{
    private IApplicationShellView shellView;

    public ApplicationShellController(IApplicationShellView view)
    {
        this.shellView = view;
    }

    public void OpenNewSpreadsheet(DockState dockState = DockState.Document)
    {
        SpreadsheetController controller = (SpreadsheetController)GetDocumentController("new.xlsx");
        SpreadsheetView view = (SpreadsheetView)controller.New("new.xlsx");
        view.Show(shellView.DockPanel, dockState);
    }

    private IDocumentController GetDocumentController(string path)
    {
        return CompositionRoot.ResolveAll<IDocumentController>()
            .SingleOrDefault(provider => provider.Handles(path));
    }

    public IApplicationShellView ShellView { get { return shellView; } }
}

SpreadsheetController.cs

public class SpreadsheetController : IDocumentController 
{
    private ISpreadsheetView view;

    public SpreadsheetController(ISpreadsheetView view)
    {
        this.view = view;
        this.view.SetController(this);
    }

    public bool Handles(string path)
    {
        string extension = Path.GetExtension(path);
        if (!String.IsNullOrEmpty(extension))
        {
            if (FileTypes.Any(ft => ft.FileExtension.CompareNoCase(extension)))
                return true;
        }
        return false;
    }

    public void SetViewActive(bool isActive)
    {
        ((SpreadsheetView)view).ShowIcon = isActive;
    }

    public IDocumentView New(string fileName)
    {
        // Opens a new file correctly.
    }

    public IDocumentView Open(string path)
    {
        // Opens an Excel file correctly.
    }

    public IEnumerable<DocumentFileType> FileTypes
    {
        get
        {
            return new List<DocumentFileType>()
            {
                new DocumentFileType("CSV",  ".csv" ),
                new DocumentFileType("Excel", ".xls"),
                new DocumentFileType("Excel10", ".xlsx")
            };
        }
    }
}

实现的接口所在位置

public interface IDocumentController
{
    bool Handles(string path);

    void SetViewActive(bool isActive);

    IDocumentView New(string fileName);

    IDocumentView Open(string path);

    IEnumerable<DocumentFileType> FileTypes { get; }
}

现在与此控制器相关联的视图是:
public partial class SpreadsheetView : DockContent, ISpreadsheetView
{
    private IDocumentController controller;

    public SpreadsheetView()
    {
        InitializeComponent();
    }

    private void SpreadsheetView_Activated(object sender, EventArgs e)
    {
        controller.SetViewActive(true);
    }

    private void SpreadsheetView_Deactivate(object sender, EventArgs e)
    {
        controller.SetViewActive(false);
    }

    public void SetController(IDocumentController controller)
    {
        this.controller = controller;
        Log.Trace("SpreadsheetView.SetController(): Controller set successfully");
    }

    public string DisplayName
    {
        get { return Text; }
        set { Text = value; }
    }

    public WorkbookView WorkbookView
    {
        get { return workbookView; }
        set { workbookView = value; }
    }
    ...
}

最后,视图接口是

public interface ISpreadsheetView : IDocumentView
{
    WorkbookView WorkbookView { get; set; } 
}

并且。
public interface IDocumentView
{
    void SetController(IDocumentController controller);

    string DisplayName { get; set; }

    bool StatusBarVisible { get; set; }
}

现在是我的问题。在Seemann的书籍“依赖注入在.NET中”中,他谈到了“三次调用模式”,这就是我在上面尝试实现的内容。代码可以工作,shell视图显示并且通过MVC模式控制器正确地打开视图等。然而,我很困惑,因为上述内容明显具有“服务定位器反模式”的特点。在Seemann的书的第3章中,他说:
“组合根模式描述了您应该在哪里使用DI容器。但是,它没有说明如何使用。注册解析释放模式解决了这个问题[...] DI容器应该在称为注册、解析和释放的连续三个阶段中使用。”
“在其纯粹的形式中,注册解析释放模式表明,您应该在每个阶段中仅进行一个方法调用。Krzysztof Kozimic将此称为“三次调用模式”。”
“在单个方法调用中配置DI容器需要更多的解释。组件注册应该发生在单个方法调用中的原因是,您应该将DI容器的配置视为单个原子操作。完成配置后,容器应被视为只读。”
这听起来像可怕的“服务定位器”,为什么这不被视为服务定位器?
为了将我的代码调整为使用构造函数注入,我将我的入口代码更改为
[STAThread]
static void Main()
{
    var kernel = new StandardKernel();
    kernel.Bind(t => t.FromThisAssembly()
                      .SelectAllClasses()
                      .BindAllInterfaces());

    FileLogHandler fileLogHandler = new FileLogHandler(Utils.GetLogFilePath());
    Log.LogHandler = fileLogHandler;
    Log.Trace("Program.Main(): Logging initialized");

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(kernel.Get<ApplicationShellView>());
}

使用Ninject.Extensions.Conventions,然后我更改了ApplicationShellController以通过构造函数注入来正确注入IDocumentController

public class ApplicationShellController
{
    private IApplicationShellView shellView;
    private IEnumerable<IDocumentController> controllers; 

    public ApplicationShellController(IApplicationShellView shellView, IEnumerable<IDocumentController> controllers)
    {
        this.shellView = shellView;
        this.controllers = controllers;
        Log.Trace("ApplicationShellController.Ctor(): Shell initialized successfully"); 
    }
    ...
}

在哪里

public class SpreadsheetController : IDocumentController 
{
    private ISpreadsheetView view;

    public SpreadsheetController(ISpreadsheetView view)
    {
        this.view = view;
        this.view.SetController(this);
    }
    ...
}

但这样会导致循环依赖,我该如何处理?
问题摘要:
1.为什么我的Ninject最初使用"三次调用模式"和CompositionRoot.Resolve<T>()与服务定位器反模式不同或不好?
2.如果我想切换到纯构造函数注入,如何解决上述循环依赖问题?
非常感谢您的时间。
1个回答

3
在某个过程中,你必须使用服务定位。然而,依赖注入(DI)和服务定位(SL)的区别在于,在SL中,你在请求服务时解析它们,而在DI中,你在某种工厂(如控制器工厂)中解析它们,然后构造对象并传递引用。
你应该创建一些基础设施来调度命令,并使用某种工厂来定位被创建对象所使用的依赖项。
通过这种方式,你的其余代码没有依赖关系的解析,并且你遵循了一个DI模式,只不过是在构建点处不同。

你能提供一个“控制器工厂”示例或参考资料吗?非常感谢您的时间… - MoonKnight
@Killercam - 我还没有看到任何成功使用 DI 的 WinForms 代码,但你可以看一下 asp.net MVC 框架作为一个例子(尽管许多人不喜欢 MVC6 使用的“符合容器”方法)。 - Erik Funkenbusch
但是为什么Seemann的书中会谈到Three Call Pattern,好像它应该像包装器一样用于DI容器的“Resolve”方法?这似乎使得在大多数情况下使用它成为服务定位...这是本章中我不欣赏的一个方面...感谢您的时间。 - MoonKnight

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