8

我正在 WPF 中构建一个类似 Visual Studio 的应用程序,但在确定组件的最佳架构设计组织时遇到了一些问题。我计划使用 Unity 作为我的依赖注入容器和 Visual Studio 单元测试框架,并且可能使用 moq 来模拟库。

我将首先描述我的解决方案的结构,然后是我的问题:

我有一个WPF 项目,其中包含:

  • 应用程序启动时我的 Unity 容器初始化(引导程序)(在 App.xaml.cs 中)
  • 我所有的应用程序视图 (XAML)。

另一个名为ViewModel的项目包含:

  • 我所有的应用程序视图模型。我所有的 ViewModel 都继承自 ViewModelBase,它公开了 ILogger 属性

我的初始化逻辑如下:

  1. 应用程序启动
  2. Unity 容器创建和注册类型:MainView 和 MainViewModel
  3. 解决我的 MainView 并显示它。

var window = Container.Resolve<MainView>();

window.Show();

我的 MainView 构造函数在其构造函数中接收 MainViewModel 对象:

public MainView(MainViewModel _mvm)
  1. 我的 MainViewModel 的每个面板都有一个子 ViewModel:

    public ToolboxViewModel ToolboxVM{get; set;}
    public SolutionExplorerViewModel SolutionExplorerVM { get; set; }
    public PropertiesViewModel PropertiesVM { get; set; }
    public MessagesViewModel MessagesVM { get; set; }
    

我计划创建一个 InitializePanels() 方法来初始化每个面板。

现在我的问题是:我的 MainViewModel.InitializePanels() 如何初始化所有这些面板?给出以下选项:

选项 1:手动初始化 ViewModel:

ToolboxVM = new ToolboxViewModel();
//Same for the rest of VM...

缺点:

  • 我没有使用 Unity 容器,因此我的依赖项(例如 ILogger)不会自动解析

选项 2:通过注释我的属性来使用 setter 注入:

[Dependency]
public ToolboxViewModel ToolboxVM{get; set;}
//... Same for rest of Panel VM's

缺点:

  • 我读过应该避免 Unity Setter 依赖项,因为在这种情况下它们会与 Unity 产生依赖关系
  • 我还读到你应该避免使用 Unity 进行单元测试,那么如何在我的单元测试中明确这种依赖关系?拥有许多依赖属性可能是配置的噩梦。

选项 3:使用 Unity 构造函数注入将我的所有面板视图模型传递给 MainViewModel 构造函数,以便它们由 Unity 容器自动解析:

public MainViewModel(ToolboxViewModel _tbvm, SolutionExploerViewModel _sevm,....)

优点:

  • 在创建时,依赖关系是显而易见的,这有助于构建我的 ViewModel 单元测试。

缺点:

  • 拥有如此多的构造函数参数可能会很快变得丑陋

选项 4:在容器构建时注册我的所有 VM 类型。然后通过构造函数注入将 UnityContainer 实例传递给我的 MainViewModel:

public MainViewModel(IUnityContainer _container)

这样我可以做类似的事情:

        Toolbox = _container.Resolve<ToolboxViewModel>();
        SolutionExplorer = _container.Resolve<SolutionExplorerViewModel>();
        Properties = _container.Resolve<PropertiesViewModel>();
        Messages = _container.Resolve<MessagesViewModel>();

缺点:

  • 如果我决定不将 Unity 用于我的单元测试,正如许多人所建议的那样,那么我将无法解析和初始化我的面板视图模型。

鉴于这个冗长的解释,什么是最好的方法,以便我可以利用依赖注入容器并最终得到一个可单元测试的解决方案?

提前致谢,

4

3 回答 3

5

首先要做的事情...正如您所注意到的,您当前的设置在进行单元测试(复杂的 VM 初始化)时可能会出现问题。然而,简单地遵循DI 原则依赖于抽象而不是具体化,这个问题就会立即消失。如果您的视图模型将实现接口并且依赖项将通过接口实现,那么任何复杂的初始化都变得无关紧要,因为在测试中您只需使用模拟。

接下来,带注释的属性的问题是您在视图模型和 Unity 之间创建了高度耦合(这就是为什么它很可能是错误的)。理想情况下,注册应该在单个顶级点(在您的情况下是引导程序)处理,因此容器不会以任何方式绑定到它提供的对象。您的选项 #3 和 #4 是解决此问题的最常见解决方案,几乎没有注意事项:

  • #3:过多的构造函数依赖通常可以通过在外观类中分组常见功能来缓解(但毕竟4 并没有那么多)。通常,正确设计的代码不会有这个问题。请注意,根据您MainViewModel所做的事情,您可能需要的只是对子视图模型列表的依赖,而不是具体的。
  • #4:你不应该在单元测试中使用 IoC 容器。您只需手动创建MainViewModel(通过 ctor)并手动注入模拟

我想再谈一点。考虑当您的项目增长时会发生什么。将所有视图模型打包到单个项目中可能不是一个好主意。每个视图模型都有自己的(通常与其他视图模型无关)依赖关系,所有这些东西都必须放在一起。这可能很快变得难以维护。相反,请考虑您是否可以提取一些常见功能(例如消息传递工具)并将它们放在单独的项目组中(同样,拆分为 M-VM-V 项目)。

此外,当您有与功能相关的分组时,交换视图要容易得多。如果项目结构如下所示:

> MyApp.Users
> MyApp.Users.ViewModels
> MyApp.Users.Views
> ...

为用户编辑窗口尝试不同的视图是重新编译和交换单个程序集 ( User.Views) 的问题。使用一体式方法,您将不得不重建应用程序的大部分,即使其中大部分根本没有改变。

编辑:请记住,更改项目的现有结构(即使是很小的)通常是一个非常昂贵的过程,具有较小/没有业务结果。您可能不被允许或根本负担不起这样做。基于使用的(DAL、BLL、BO 等)结构确实有效,只是随着时间的推移变得越来越重。您也可以使用混合模式,将核心功能按用途分组,并使用模块化方法简单地添加新功能。

于 2012-06-22T14:30:47.177 回答
2

首先,您可能希望使用接口而不是具体类,这样您就可以在单元测试时传递模拟对象,即IToolboxViewModel代替ToolboxViewModel等。

话虽如此,我会推荐第三种选择——构造函数注入。这是最有意义的,因为否则您可能会调用var mainVM = new MainViewModel()并最终得到一个非功能视图模型。通过这样做,您还可以很容易地理解您的视图模型的依赖项是什么,从而更容易编写单元测试。

我会查看此链接,因为它与您的问题有关。

于 2012-06-22T14:27:07.333 回答
1

我同意莱斯特的观点,但想补充一些其他的选择和意见。

在通过构造函数将 ViewModel 传递给 View 的地方,这有点不合常规,因为 WPF 的绑定功能允许您通过绑定到 DataContext 对象将 ViewModel 与 View 完全解耦。在您概述的设计中,视图与具体实现耦合并限制重用。

虽然服务外观将简化选项 3,但顶级 ViewModel 承担很多责任的情况并不少见(正如您所概述的)。您可以考虑的另一种模式是组装视图模型的控制器或工厂模式。工厂可以由容器支持来完成工作,但容器是从调用者那里抽象出来的。构建容器驱动应用程序的一个关键目标是限制了解系统如何组装的类的数量。

另一个问题是属于顶级视图模型的职责和对象关系的数量。如果您查看 Prism(WPF + Unity 的一个很好的候选者),它引入了由模块填充的“区域”的概念。一个区域可以表示由多个模块填充的工具栏。在这样的设计下,顶级视图模型的职责(和依赖关系!)更少,并且每个模块都包含可单元测试的 DI 组件。与您提供的示例相比,思维发生了重大转变。

关于选项 4,通过构造函数传入的容器在技术上是依赖反转,但它是以服务位置的形式而不是依赖注入的形式。在我告诉你这是一个非常滑的斜坡(更像是悬崖)之前已经走下这条路:依赖关系隐藏在类中,你的代码变成了一个“及时”疯狂的网络——完全不可预测,完全无法测试。

于 2012-06-23T16:12:18.800 回答