【问题标题】:MVVM with Unity and Unit Testing architectural designMVVM 与 Unity 和单元测试架构设计
【发布时间】:2012-06-24 20:36:02
【问题描述】:

我正在 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 用于我的单元测试,那么我将无法解析和初始化我的面板视图模型。

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

提前致谢,

【问题讨论】:

  • 你完全正确。我应该针对接口而不是具体实现进行编码,但是,我通常开始对具体类进行编码,然后使用 Resharper 提取它们的接口,还没有达到这一点,但我很快就会!
  • 我对此的快速而肮脏的解决方案是将 app.xaml 中的视图模型实例化为资源,然后根据需要进行组合。 &lt;t:ConsoleViewModel x:Key="cvm"/&gt; &lt;t:MainViewModel Console="{StaticResource cvm}" /&gt; 这使得单元测试易于设置。

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


【解决方案1】:

我同意 Lester 的观点,但想补充一些其他选项和意见。

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

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

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

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

【讨论】:

  • 感谢您的反馈@bryanbcook。事实上,PRISM 是我的基础架构选项之一,我通过了一些关于区域和模块的示例,但我认为要深入了解 PRISM 框架需要花费更多的学习曲线。
  • Prism 只是一个可以进一步细分职责的例子。
【解决方案2】:

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

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

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

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

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

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

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

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

【讨论】:

  • 伙计,我已将您的答案读了 10 次,每次都更有意义。这是一个令人难以置信的大开眼界。关于为每个“模块”构建单独的项目是有道理的,但我已经有很多项目,如 DAL(数据访问层)、Common、BLL(业务逻辑层)、业务对象、测试项目等......
  • @AdolfoPerez:如果是这样,您必须考虑在更改现有基础架构上投入的时间和精力。如果你负担不起,就不要这样做 - "bags" 结构也可以,只是随着时间的推移它们会变得更难维护。我已将其添加到我的答案中,因此没有误解。
  • 事实上,我正处于设计/开发的早期阶段,所以移动东西不会产生非常负面的影响,我只需要对我想要的方式感到舒服走。再次感谢您的洞察力。
  • 现在@jimmy_keen 正在考虑外观模式来解决我的多构造函数参数问题。不会造成同样的问题吗?假设我创建了一个 PanelsViewModel 并为里面的所有 Panels 创建属性。我仍然需要创建该类并传递依赖项不是吗?
  • @AdolfoPerez:您不为此使用外观(分组面板)。不要将外观视为 bag ,您将所有依赖项都扔到一个构造函数参数中;而是将其视为聚合相似对象的一种手段(功能方面)。再一次,4 并没有那么多。一旦你得到 6-7,就该开始思考将对象组合在一起的意义所在。 也许您的一些虚拟机可以拆分成更小的虚拟机,以便更好地对这些小型虚拟机进行分组。
【解决方案3】:

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

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

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

【讨论】:

  • 谢谢 Lester,是的,我认为选项 3 也更有意义,尤其是在 @jimmy_keen 建议的通过外观类对构造函数参数进行分组时。谢谢你的帮助!将等待听取其他人的建议
猜你喜欢
  • 2023-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-24
相关资源
最近更新 更多