具有Unity和单元测试的MVVM架构设计

时间:2012-06-22 14:17:03

标签: c# unit-testing architecture mvvm unity-container

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

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

我有 WPF项目,其中包含:

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

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

  • 我的所有应用程序ViewModels。我的所有ViewModel都从ViewModelBase继承,它暴露了ILogger属性

我的初始化逻辑如下:

  1. 应用程序启动
  2. Unity容器创建和类型注册:MainView和MainViewModel
  3. 解析我的MainView并显示它。
  4. var window = Container.Resolve<MainView>();

    window.Show();

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

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

      public ToolboxViewModel ToolboxVM{get; set;}
      public SolutionExplorerViewModel SolutionExplorerVM { get; set; }
      public PropertiesViewModel PropertiesVM { get; set; }
      public MessagesViewModel MessagesVM { get; set; }
      
    2. 我计划创建一个初始化每个面板的InitializePanels()方法。

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

      选项1:手动初始化ViewModels:

      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 Constructor注入将我的所有Panel ViewModel传递给MainViewModel构造函数,以便Unity容器自动解析它们:

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

      优点:

      • 创建时依赖关系会很明显,这有助于构建我的ViewModel UnitTests。

      缺点:

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

      选项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用于我的UnitTests,就像很多人建议的那样,那么我将无法解析和初始化我的Panel ViewModel。

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

      提前致谢,

3 个答案:

答案 0 :(得分:5)

首先要做的事情......正如您所注意到的,当您进行单元测试(复杂的VM初始化)时,您当前的设置可能会出现问题。但是,只需关注DI principle依赖于抽象而非结果,会立即消除此问题。如果您的视图模型将实现接口,并且依赖关系将通过接口实现,任何复杂的初始化都变得无关紧要,因为在测试中您只需使用模拟。

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

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

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

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

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

为用户编辑窗口尝试不同的视图是重新编译和交换单个程序集(User.Views)的问题。使用 all in bag bag 方法,您将不得不重建更大部分的应用程序,即使大多数应用程序都没有改变。

编辑请记住更改项目的现有结构(即使是很小的项目),通常是一个非常成本高昂的进程没有业务成果。您可能不被允许或根本无法负担这样做。 基于使用(DAL,BLL,BO等)结构可以正常工作,它随着时间的推移变得越来越重。您也可以使用混合模式,核心功能按其用法分组,只需使用模块化方法添加新功能。

答案 1 :(得分:2)

首先,您可能希望使用接口而不是具体类,以便在单元测试时能够传递模拟对象,即IToolboxViewModel而不是ToolboxViewModel

话虽这么说,我会推荐第三个选项 - 构造函数注入。这是最有意义的,因为否则你可以调用var mainVM = new MainViewModel()并最终得到一个非功能性的视图模型。通过这样做,您还可以非常轻松地了解视图模型的依赖关系,从而更轻松地编写单元测试。

我会查看this link,因为它与您的问题相关。

答案 2 :(得分:1)

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

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

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

另一个问题是属于顶级视图模型的职责和对象关系的数量。如果你看一下Prism(WPF + Unity的优秀候选人),它会引入模块填充的“区域”概念。区域可以表示由多个模块填充的工具栏。在这样的设计下,顶级视图模型具有较少的职责(和依赖性!),并且每个模块包含可单元测试的DI组件。从你提供的例子中思考的重大转变。

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