MVVM和服务模式


14

我正在使用MVVM模式构建WPF应用程序。现在,我的视图模型调用服务层以检索模型(与视图模型无关)并将其转换为视图模型。我正在使用构造函数注入将所需的服务传递给viewmodel。

它易于测试,并且适用于几乎没有依赖关系的viewmodel,但是当我尝试为复杂模型创建viewModels时,我就有了一个构造函数,其中注入了很多服务(一个用于检索每个依赖关系和所有可用值的列表)绑定到itemsSource)。我想知道如何处理这样的多种服务,并且仍然拥有一个可以轻松进行单元测试的视图模型。

我在考虑一些解决方案:

  1. 创建一个包含所有可用服务作为接口的服务单例(IServices)。示例:Services.Current.XXXService.Retrieve(),Services.Current.YYYService.Retrieve()。这样,我就没有一个包含大量服务参数的庞大构造函数。

  2. 为viewModel使用的服务创建外观,并将此对象传递到我的viewmodel的ctor中。但是,然后,我必须为我的每个复合视图模型创建一个外观,这可能会有点多...

您认为实现这种架构的“正确”方法是什么?


我认为“正确”的方法是创建一个单独的层来调用服务,并执行创建ViewModel所需的任何强制转换。您的ViewModels不负责创建自己。
艾米·布兰肯希

@AmyBlankenship:视图模型不必(或者甚至必须能够)创建自己,但是有时不可避免地要负责创建其他视图模型。具有自动工厂支持的IoC容器在这里有很大的帮助。
亚伦诺特,2013年

“有时”和“应该”是两种不同的动物;)
艾米·布兰肯希

@AmyBlankenship:您是否建议视图模型不应创建其他视图模型?那是很难吞下的药。我可以理解地说,视图模型不应该new用于创建其他视图模型,而是想像MDI应用程序那样简单,在MDI应用程序中,单击“新文档”按钮或菜单将添加一个新标签或打开一个新窗口。外壳/导体必须能够创建某些事物的新实例,即使该事物隐藏在一层或几层间接方法之后。
亚伦诺特,2013年

好吧,当然它必须具有请求在某个地方创建视图的能力。但是要自己做?不属于我的世界:)。但是话又说回来,在我生活的世界中,我们称VM为“演示模型”。
艾米·布兰肯希

Answers:


22

实际上,这两种解决方案都是不好的。

创建一个包含所有可用服务作为接口的服务单例(IServices)。示例:Services.Current.XXXService.Retrieve(),Services.Current.YYYService.Retrieve()。这样,我就没有一个包含大量服务参数的庞大构造函数。

本质上,这是服务定位器模式,它是一种反模式。如果这样做,您将无法不查看视图模型的私有实现而了解视图模型实际依赖的内容,这将使测试或重构变得非常困难。

为viewModel使用的服务创建外观,并将此对象传递到我的viewmodel的ctor中。但是,然后,我必须为我的每个复合视图模型创建一个外观,这可能会有点多...

这不是反模式,而是代码的味道。本质上,您是在创建一个参数对象,但PO重构模式的重点是处理经常在许多不同地方使用的参数集,而此参数只能使用一次。就像您提到的那样,它会造成很多代码膨胀,但并没有真正的好处,并且不能与很多IoC容器配合使用。

实际上,以上两种策略都忽略了整个问题,即视图模型与服务之间的耦合度过高。简单地这些依赖项隐藏在服务定位器或参数对象中并不会真正改变视图模型所依赖的其他对象数量。

想想如何对这些视图模型之一进行单元测试。您的设置代码有多大?为了使它起作用,需要初始化几件事?

从MVVM开始的很多人都试图为整个屏幕创建视图模型,这从根本上来说是错误的方法。MVVM都是关于合成的,具有许多功能的屏幕应该由几种不同的视图模型组成,每种视图模型仅取决于一个或几个内部模型/服务。如果他们需要彼此通信,则可以通过pub / sub(消息代理,事件总线等)进行通信。

您实际需要做的是重构视图模型,以减少依赖。然后,如果需要聚合“屏幕”,则可以创建另一个视图模型以聚合较小的视图模型。这种聚合视图模型本身并不需要做很多事情,因此反过来也很容易理解和测试。

如果您正确执行了此操作,那么仅通过查看代码就应该显而易见,因为您将拥有简短,简洁,特定且可测试的视图模型。


是的,那可能就是我要做的!非常感谢先生。
alfa-alfa

好吧,我已经假设他已经尝试过了,但是没有成功。@ alfa-alfa
欣快感,2013年

@Euphoric:您如何“不成功”?正如Yoda所说:做或不做,没有尝试。
Aaronaught

@Aaronaught例如,他真的需要单个viewmodel中的所有数据。也许他有网格,不同的列来自不同的服务。您不能通过合成来做到这一点。
欣快的

@Euphoric:实际上,您可以使用合成解决此问题,但是可以在视图模型级别下完成。这仅仅是创建正确的抽象的问题。在这种情况下,您只需要一个服务即可处理初始查询以获取ID列表,以及一个序列“ enrichers” /列表/“ enrichers”数组,并使用其自身的信息进行注释。使网格本身成为自己的视图模型,您已经通过有效的两个依赖关系解决了该问题,并且测试非常容易。
亚伦诺特,2013年

1

我可以写一本关于这本书...实际上我是;)

首先,没有通用的 “正确”方法来做事。您必须考虑其他因素。

您的服务可能过于精细。使用提供特定ViewModel或相关ViewModel集群的接口的Facades将服务包装起来可能是一个更好的解决方案。

更简单的方法是将服务包装到所有视图模型使用的单个Facade中。当然,对于一般情况而言,这可能是一个非常大的界面,具有许多不必要的功能。但是我想说这与处理系统中每条消息的消息路由器没有什么不同。

实际上,我看到的许多体系结构最终演变为围绕诸如Event Aggregator模式之类构建的消息总线。在这里进行测试很容易,因为您的测试类仅向EA注册了一个侦听器并触发相应的事件作为响应。但这是一个先进的方案,需要一段时间才能发展。我说从统一的门面开始,然后从那里开始。


庞大的服务外观与消息代理非常不同。它几乎在依赖范围的另一端。此体系结构的标志是,发送方对接收方一无所知,并且可能有许多接收方(发布/订阅或多播)。也许您将其与RPC样式的“远程处理”混淆了,后者只是通过远程协议公开传统服务,并且发送方在物理上(端点地址)和逻辑上(返回值)仍然耦合到接收方。
Aaronaught

相似之处在于,外观就像路由器一样,呼叫者不知道哪个服务处理呼叫,就像客户端发送消息不知道谁处理消息一样。
Michael Brown

是的,但是外墙才是上帝的对象。它具有视图模型所具有的所有依赖关系,可能更多,因为它将被多个共享。实际上,您已经消除了松耦合的好处,而松耦合最初是您努力工作的,因为现在,无论何时碰到大型立面,您都不知道它真正依赖于什么功能。图片为使用外观的类编写单元测试。您可以为立面创建一个模拟。现在,您要嘲笑哪些方法?您的设置代码是什么样的?
Aaronaught

这与消息代理非常不同,因为代理还不知道消息处理程序的实现。它在引擎盖下使用IoC。外立面了解收件人的所有情况,因为它必须将呼叫转发给他们。总线零耦合;立面具有令人讨厌的高传出耦合。无论您在何处更改几乎所有内容,也会影响外观。
亚罗诺(Aaronaught)

我认为这里的部分混乱-而且我经常看到这一点-就是依赖性的含义。如果您拥有一个依赖于另一个类的类,但是调用了该类的4个方法,则它具有4个依赖关系,而不是1个。将其全部放在外观上不会改变依赖关系的数量,只会使它们更难理解。
亚罗诺(Aaronaught)

0

为什么不将两者结合?

创建一个外观,并放置您的视图模型使用的所有服务。这样,您就可以为所有视图模型使用单一外观,而无需输入不良的S字。

或者,您可以使用属性注入而不是构造函数注入。但是,然后,您需要确保已正确注入了它们。


如果您提供了伪C#示例,则这将是一个更好的答案。
罗伯特·哈维
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.