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

Ado*_*rez 8 c# architecture unit-testing unity-container mvvm

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

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

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

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

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

  • 我的所有应用程序ViewModels.我的所有ViewModel都从一个暴露ILogger属性的ViewModelBase继承

我的初始化逻辑如下:

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

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

window.Show();

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

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

    public ToolboxViewModel ToolboxVM{get; set;}
    public SolutionExplorerViewModel SolutionExplorerVM { get; set; }
    public PropertiesViewModel PropertiesVM { get; set; }
    public MessagesViewModel MessagesVM { get; set; }
    
    Run Code Online (Sandbox Code Playgroud)

我打算创建一个初始化每个面板的InitializePanels()方法.

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

选项1:手动初始化ViewModels:

ToolboxVM = new ToolboxViewModel();
//Same for the rest of VM...
Run Code Online (Sandbox Code Playgroud)

缺点:

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

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

[Dependency]
public ToolboxViewModel ToolboxVM{get; set;}
//... Same for rest of Panel VM's
Run Code Online (Sandbox Code Playgroud)

缺点:

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

选项3:使用Unity Constructor注入将所有Panel ViewModel传递给MainViewModel构造函数,以便Unity容器自动解析它们:

public MainViewModel(ToolboxViewModel _tbvm, SolutionExploerViewModel _sevm,....)
Run Code Online (Sandbox Code Playgroud)

优点:

  • 在创建时,依赖关系将是明显且清晰的,这有助于构建我的ViewModel UnitTests.

缺点:

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

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

public MainViewModel(IUnityContainer _container)
Run Code Online (Sandbox Code Playgroud)

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

        Toolbox = _container.Resolve<ToolboxViewModel>();
        SolutionExplorer = _container.Resolve<SolutionExplorerViewModel>();
        Properties = _container.Resolve<PropertiesViewModel>();
        Messages = _container.Resolve<MessagesViewModel>();
Run Code Online (Sandbox Code Playgroud)

缺点:

  • 如果我决定不将Unity用于我的UnitTests,就像许多人建议的那样,那么我将无法解析和初始化我的Panel ViewModel.

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

提前致谢,

k.m*_*k.m 5

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

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

  • #3:通常通过对Facade类中的常用功能进行分组来减轻太多的构造函数依赖性(但是4 并不是那么多).通常,正确设计的代码没有这个问题.请注意,根据您MainViewModel所做的事情,您可能只需要依赖于子视图模型列表,而不是具体的模型.
  • #4:你不应该在单元测试中使用IoC容器.你简单的创建MainViewModel(通过构造函数)手动和注入嘲笑.

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

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

> MyApp.Users
> MyApp.Users.ViewModels
> MyApp.Users.Views
> ...
Run Code Online (Sandbox Code Playgroud)

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

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