MVVM是否违反DRY原则?

41

看起来我创建的ViewModels与其他类非常相似,并且它们似乎需要大量的代码重复,例如在当前项目中:

  • SmartForm:代表要填写的数据表单的模型,具有以下属性:
    • IdCode
    • Title
    • Description
    • SmartFormFields集合
    • 等等。
  • SmartFormControlView 视图
  • SmartFormControlViewModel 视图模型
    • IdCode
    • Title
    • Description
    • SmartFormFields集合
    • 等等。

因此,我的ViewModel基本上与我的Model相同,只是带有所有与视图绑定的OnPropertyChanged功能。

随着我的重构和扩展,似乎每次对模型进行小改动时,我都必须对ViewModel进行镜像更改

这似乎违反了设计模式的一个基本原则:不要重复自己

我是否错误地实现了MVVM模式,或者MVVM中始终存在Model和ViewModel之间的一对一重复是固有特性?


2
我经常也在想同样的问题...只不过在我的情况下,我选择在我的模型上实现更改通知(对我来说更有意义--因为它们是存储数据的地方,所以它们知道何时发生了更改)。这使得重复变得更加突出。 - Joe White
1
我的方法是模型应该能够在任何环境中使用,因此如果您想在ASP.NET MVC应用程序中使用它们,则不希望对其进行更改通知。因此,我认为您的模型上的更改通知使它们锁定在WPF环境中,对吗? - Edward Tanguay
1
为什么不在SmartFormControlViewModel中拥有一个SmartForm类的实例,而不是复制字段呢? - Matthew Groves
不,更改通知不会将您锁定到WPF。它们在其他环境中只是不必要的。 (INotifyPropertyChanged位于System.dll中,因此它是核心.NET Framework的一部分,而不是任何特定于WPF的库的一部分。)但我喜欢您的推理 - 这是我没有想到的东西。 - Joe White
8个回答

22

我个人认为这并不违反DRY原则,因为模型和视图模型(我更喜欢称之为Presenter)并不指向相同的信息。例如,您的VM和M都具有Title属性,但是您的VM的Title属性可能还包括验证,而模型的Title属性可能会假定其有效性。

虽然VM可能包含模型的所有属性,但也有可能具有验证(例如:Title必须非空),数据依赖项,可绑定的UI特定属性(图标、颜色、画刷等),这些都不是视图的一部分。

实际上,所有UI模式在你所述的方式中都有类似的“重复”:即级联修改。试着在不改变控制器的情况下更改MVC中的模型。

话虽如此,MVVM(或任何旨在将UI、逻辑和状态分离的UI模式)可能对像你的例子这样简单的情况过于繁琐。当逻辑仅仅是状态传递时,将控制器/Presenter/视图模型分离的价值就会降低。

在您的特定情况中,如果确实没有逻辑、验证或UI特定属性未在VM公开,并且您的模型不需要持久化、序列化或与现有结构兼容(或在VM中添加此逻辑很简单),我强烈建议将M和VM合并,以避免创建唯一目的是获取/设置基础模型属性的属性。


2
我永远不会在模型层面上假设有效性。将模型抽象出来的一个主要好处是,您可以用其他东西(不同的UI、Web服务)替换UI层,并获得代码重用。如果您将模型验证委托给原始UI,则模型变得不可能正确地重用。 - Eric J.
@ Eric J:回复晚了,但你提出了一个很好的观点。在大多数MVVM示例中,并且由WPF/SL绑定基础设施本身建议,模型在行为上是轻量级的(例如Fowler的贫血领域模型),将验证推迟到数据绑定时,例如VM层。我很想知道您对数据绑定的想法和方法,以限制其对领域模型的干扰,同时又依赖于领域模型进行验证。 - micahtan
相反,必须强制执行领域模型(聚合)不变量以提供模型的一致性。如果模型不能强制执行自己的一致性,那么模型的目的是什么? - Sergey Vyacheslavovich Brunov
@SergeyBrunov:这不就是我说的吗? - Eric J.
@EricJ.,请原谅我。我没有听清你上一条消息的最后一句话。这完全相同。 - Sergey Vyacheslavovich Brunov

3
一个简单的解决方案是创建一个抽象的ViewModel(VM)基类,暴露模型。在适当的场景中,您可以选择使用此VM。
例如:
public abstract class ViewModelBase<T>
{
    public T Model { get; set; }
}

如果你的 Model 实现了 INotifyPropertyChanged 接口,那么你的视图将会接收到事件。然而这样做会使得视图可以访问到 Model 中的每一个属性,有时候并不是我们想要的。
另外,你也可以使用属性初始化器来实现,例如下面这种方式(我个人将其存储在代码片段中):
public abstract class SampleViewModel
{
    public int MyProperty
    {
        get { return Model.MyProperty; }
        set
        {
            Model.MyProperty = value;
            OnPropertyChanged("MyProperty");
        }
    }
}

在大多数情况下,您的视图将对虚拟机进行更改,当它发生变化时,任何绑定到该属性的控件都会被告知发生了某些事情。
希望这能帮到您。

1
在他的书《领域驱动设计》中,Eric Evans提到模型重构不应该太难,并且概念变化不应该跨越太多模块,否则,重构模型就会变得困难。因此,如果你问我,在ViewModel中将Model“复制”肯定会使Model重构变得更加困难。
Eric提到,我们应该更注重模型的内聚性和隔离性,而不是基于技术问题(数据库访问、POCOS、演示)来划分层次结构的整洁程度。最重要的问题是领域模型是业务领域的良好表示,因此领域模型最重要的是处于单独的层中,而不是跨越多个模块,以便轻松更新(重构)。
考虑到刚才所说的,我会在ViewModel中使用相同的Model对象,如果我想要减少对Model对象的“访问”级别,那么我会“传递”一个由Model对象实现的接口的引用。例如:
    // The primary Model classes
    public partial class OrderItem {
        public int Id { get; }
        public int Quantity { get; set; }
        public Product Item { get; set; }
        public int Total { get; set; }
        public void ApplyDiscount(Coupon coupon) {
            // implementation here
        }
    }

    public partial class Product {
        public int Id { get; }
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }    
    }

    // The shared views of those model classes
    public partial class OrderItem : IOrderItemDTO {
        public IProductDTO Item { 
            get { 
                return this.product; 
            } 
        }
    }

    public partial class Product : IProductDTO {
    }


   // Separate interfaces...
   // You enforce the rules about how the model can be
   // used in the View-ViewModel, without having to rewrite
   // all the setters and getters.
    public interface IOrderItemDTO {
        int Id { get; }
        int Quantity { get; set; }
        IProductDTO Item { get; }
        int Total { get; }
    }

    public interface IProductDTO {
        string Name { get; }
        string Description { get; }
        decimal Price { get; }
    }

    // The viewmodel...
    public class OrderItemViewModel {
        IOrderItemDTO Model { get; set; }
    }

1

这里似乎有一件被忽略的事情,而你的简单例子没有暴露出来,那就是你的视图经常会聚合多个域模型类型中包含的数据。在这种情况下,你的 ViewModel 将包含对多个域模型(类型不同)的引用,从而聚合了一组相关数据,可能是某个特定视图希望公开的。


1

其他人已经对MVC/MVVM模式的组件角色提供了很好的评论。我想提供一个基本观察,解释不管选择哪种模式都会出现重复性。

通常情况下,数据层、业务层和UI层之间会存在某种形式的重复。毕竟,一般需要将每个属性展示给最终用户(UI)、对其行为进行建模(业务层),并保持其值(数据层)。

正如其他人指出的那样,每个层次上可能会略微不同地处理属性,这解释了某些重复性的根本原因。

在处理足够大的系统(或者与正确团队一起进行的小型项目)时,我倾向于使用UML对这种类型的信息进行建模,并使用代码生成(通常与部分类配合使用)来处理重复的方面。举个简单的例子,姓氏属性可能有一个要求(在我的UML模型中),即将数据限制为50个字符。我可以生成代码来强制执行该限制到我的UI层(例如通过物理限制输入),生成代码到我的业务层以重新检查该限制(“不要相信UI”),例如如果数据过长,则抛出异常,并生成我的持久层(例如NVARCHAR(50)列,适当的ORM映射文件等)。 2012年更新

微软的数据注释及其在UI层(例如ASP.Net MVC)和数据层(Entity Framework)中的支持,大大有助于实现我之前为许多问题生成代码的工作。


1

这是一个有趣的评论...确实,经常需要修改ViewModel以反映Model中的更改。

如果可以自动化就好了...实际上我认为这是可能的,通过在ViewModel中实现ICustomTypeDescriptor:通过反射获取GetProperties将返回模型的所有属性。但是我不确定它是否有意义,因为模型可能根本不包含属性:它可以是方法、字段或任何东西,并且并非模型中的所有内容都对ViewModel有用。


目前我正在使用代码片段和代码生成来处理它,我期望新版本的WPF和Visual Studio会更好地支持MVVM,这样就可以使用Entity Framework等在ViewModel中创建对象了。也许这在EF 4.0中已经可以实现了? - Edward Tanguay
也许在 EF 4.0 中已经有可能实现了?嗯,在 .NET 4.0 中我没有看到关于 MVVM 的具体内容... 或许微软会提供一个工具包,就像他们最初为 ASP.NET AJAX 和 WPF 工具包(部分包含在 WPF 4 中)所做的那样。 - Thomas Levesque

0

我认为,普通的MVVM确实违反了DRY原则。但是我已经开始PDX库,我认为它可以在许多情况下解决这个问题。我写了这篇文章,我相信它回答了这个问题。

基本上,我的目标之一是拥有不必担心UI通知的Viewmodel。PDX项目仍处于初期阶段,但如果您正在阅读这个问题,您可能会发现它很有用,我将感激您提供任何反馈。


0

我只知道MVC,而在MVC中,包含GUI的Model-Class存在一些错误。SmartForm似乎是一个表单,这意味着它不是一个模型。我不知道你试图编写什么程序,但我给你举个例子:

以日历为例。您可以询问该类今天是什么日期,是哪个月,每个月有多少天等等。但它没有图形表示。视图(CalenderViewMonth或其他您想要的)在屏幕上打印一个月份。它知道一个日历并询问它在不同单元格中要写什么。

本质上,您可能在建模/理解MVVM方面有一些问题(这是MVC的现代.NET变体)。


编辑:

我刚在维基百科上查了MVVM。Model就像MVC中的Model一样。View也像MVC中的View一样——只是图形表示。ViewModel是通用视图和专业模型之间的粘合剂。某种适配器。不应该违反DRY原则。


SmartForm只是一个数据持有者,它确定了UI表单的外观,在MVVM中,如果可能的话,View将连接到Model,但Model没有任何“绑定魔法”(OnPropertyChanged,ObservableCollection),因此您必须创建一个ViewModel,它基本上与Model相同,但具有这些绑定功能,ViewModel可能不必为特定视图实现Model的所有属性和功能,但根据我的经验,它通常会这样做,因此VM = M +绑定魔法。 - Edward Tanguay
我同意,这里的重复来自于一个无用的层(SmartForm)。 - Nicolas Dorier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接