摘要
- 所有模式的使用都是情境性的,如果有的话,好处在于减少复杂性。
- MVVM指导我们如何在GUI应用程序中分配类之间的责任。
- ViewModel将数据从Model投射到适合View的格式中。
- 对于琐碎的项目,MVVM是不必要的。只使用View就足够了。
- 对于简单的项目,ViewModel/Model分离可能是不必要的,只使用Model和View就足够了。
- Model和ViewModel不需要一开始就存在,可以在需要时引入。
何时使用模式以及何时避免它们
对于足够简单的应用程序,每个设计模式都是过度设计。假设您编写一个GUI应用程序,显示一个按钮,按下后显示“Hello world”。在这种情况下,像MVC,MVP,MVVM这样的设计模式增加了很多复杂性,而没有添加任何价值。
通常情况下,仅因某种程度上匹配就引入设计模式是一个不好的决定。设计模式应该用于减少复杂性,可以通过直接减少总体复杂性或者用熟悉的复杂性替代陌生的复杂性来实现。如果设计模式不能以这两种方式之一减少复杂性,则不要使用它。
为了解释熟悉和陌生的复杂性,请看以下两个字符序列:
- "D.€|Ré%dfà?c"
- "CorrectHorseBatteryStaple"
虽然第二个字符序列是第一个序列的两倍长度,但由于更加熟悉,它比第一个序列更易读、更快写和更容易记忆。同样的道理也适用于代码中的熟悉模式。
当考虑到熟悉度取决于读者时,这个问题又增加了另一个维度。有些读者会认为"3.14159265358979323846264338327950"比上述密码更容易记忆,而有些人则不会。因此,如果想使用MVVM的一种特定版本,请尝试使用与所使用的具体语言和框架中最常见形式相似的版本。
MVVM
话虽如此,让我们通过一个例子来深入探讨MVVM。 MVVM指导我们如何在GUI应用程序(或层之间 - 更多关于此后面)之间分配职责,目标是拥有少量的类,同时保持每个类的职责数量小且明确定义。
“正确”的MVVM假定至少是一个中等复杂的应用程序,该应用程序处理从“某处”获取的数据。它可以从数据库、文件、Web服务或其他无数来源获取数据。
示例
在我们的示例中,我们有两个类
View
和
Model
,但没有
ViewModel
。
Model
包装了一个csv文件,在启动时读取并在应用程序关闭时保存所有用户对数据所做的更改。
View
是一个窗口类,以表格形式显示来自
Model
的数据,并允许用户编辑数据。 csv内容可能看起来像这样:
ID, Name, Price
1, Stick, 5$
2, Big Box, 10$
3, Wheel, 20$
4, Bottle, 3$
新需求:显示欧元价格
现在我们被要求对我们的应用程序进行更改。数据由一个二维网格组成,其中已经有一个“价格”列,包含美元价格。我们需要添加一个新的列,显示以欧元计价的价格,而不是美元,基于预定义的汇率。CSV文件的格式不能更改,因为其他应用程序使用相同的文件,并且这些其他应用程序不在我们的控制范围内。
一种可能的解决方案是简单地将新列添加到“Model”类中。这不是最好的解决方案,因为“Model”保存它公开给csv的所有数据 - 我们不想在csv中增加新的欧元价格列。因此,对“Model”的更改将是非平凡的,并且描述“Model”类所做的事情也会更加困难,这是一种
code smell。
我们也可以在
View
中进行更改,但是我们当前的应用程序使用数据绑定直接显示由我们的
Model
类提供的数据。由于我们的GUI框架不允许在表格数据绑定到数据源时引入额外的计算列,因此我们需要对
View
进行重大更改才能使其正常工作,从而使
View
变得更加复杂。
引入ViewModel
应用程序中没有
ViewModel
,因为直到现在
Model
以Csv需要的方式呈现数据,这也是
View
所需的方式。在中间添加一个
ViewModel
只会增加无意义的复杂性。但是现在
Model
不再按照
View
需要的方式呈现数据,因此我们编写了一个
ViewModel
。
ViewModel
以一种使View
变得简单的方式投影Model
的数据。之前,
View
类订阅了
Model
类。现在,新的
ViewModel
类订阅了
Model
类,并将
Model
的数据公开给
View
——其中包括一个显示欧元价格的额外列。
View
不再知道
Model
,它现在只知道
ViewModel
,从
View
的角度看,
ViewModel
与之前的
Model
相同——除了公开的数据包含一个新的只读列。
新要求:以不同的方式格式化数据
下一个客户请求是,我们不应该将数据显示为表格中的行,而是将每个项目的信息(即行)显示为卡片/框,并在屏幕上以4x5网格的形式显示20个框,每次显示20个框。因为我们保持了 View
的逻辑简单,所以我们只需完全用符合客户要求的新类替换 View
。当然还有另一个客户喜欢旧的 View
,所以我们现在需要支持两者。由于所有通用业务逻辑已经在 ViewModel
中发生,这并不是什么大问题。因此,我们可以通过将 View 类重命名为 TableView
,并编写一个新的 CardView
类来显示以卡片格式显示数据。我们还需要编写一些粘合代码,这可能是启动函数中的一行代码。
新要求:动态汇率
下一位客户的要求是从互联网上获取汇率,而不是使用预定义的汇率。这就是我们重新审视“层”的早期声明的时候了。我们不会改变我们的Model类来提供汇率。相反,我们编写(或查找)一个完全独立的附加类来提供汇率。那个新类成为模型层的一部分,我们的ViewModel将csv-Model和汇率-Model的信息合并在一起,然后呈现给View。对于这个更改,旧的Model类和View类甚至不需要被修改。好吧,我们需要将Model类重命名为CsvModel,并称新类为ExchangeRateModel。
如果我们没有在适当的时候引入ViewModel,而是等到现在才这样做,那么引入ViewModel现在需要的工作量会更高,因为我们需要从View和Model中删除大量功能,并将这些功能移动到ViewModel中。
关于单元测试的后记
MVVM的主要目的并不是使Model和ViewModel中的代码可以进行单元测试。MVVM的主要目的是将代码分解为具有少量明确定义职责的类。其中一个好处是,让代码由少量明确定义职责的类组成时更容易进行单元测试。更大的好处是,代码更容易理解、维护和修改。