65.9K
CodeProject 正在变化。 阅读更多。
Home

MVVM-B - B 代表业务逻辑

starIconstarIconstarIconstarIconstarIcon

5.00/5 (15投票s)

2022年3月12日

CPOL

11分钟阅读

viewsIcon

13354

使用 MVVM 时可能犯的错误以及如何克服它们。

背景

我有个朋友正在学习如何开发 iOS 应用程序。在我们多次交谈中,他告诉我他正在使用 MVVM。我其实并不知道 MVVM 被用于 iOS 开发,出于好奇,我决定在网上搜索 iOS MVVM 最佳实践。

我惊讶地发现我找到了很多糟糕的材料。在一个案例中,文章正确地解释了 Model、View 和 ViewModel……但接下来展示的示例却只有 View 和 ViewModel。Model 完全被遗忘了,我当时想:“如果示例完全忽略了 Model,并且没有解释原因,那大家怎么才能学习 Model 呢?”

在其他案例中,作者声称 ViewModel 是放置业务逻辑的地方。我当时就想:“什么???”

因此,本文旨在通过提供示例并阐明 MVVM 背后的逻辑,而不是仅仅告诉你“该怎么做”才能遵循这个模式。

在继续之前

在本文中,我将重点讨论人们盲目遵循 MVVM 时遇到的一些问题。

我不是 iOS 开发者,也不会展示 Swift 代码,尽管我的灵感来自于 iOS 文章。

事实上,在我最初搜索 iOS 后,我又找了一些 WPF 文章,也看到了 MVVM 的许多“怪异之处”。所以,我正在努力关注核心思想。

我老实说不知道 MVVM 最初的想法是什么,但我可以肯定地说,早在 MVVM 流行之前我就使用过类似的模式,所以我会用它来解释 MVVM,或者至少是 MVVM-B,这绝对是我对 MVVM 的解释。

基础知识 - MVVM 意为 Model-View-ViewModel

如果我们在谈论一个数据库应用程序,Model 可能就是我们将从数据库中读取和编辑的数据。

View 是我们将在屏幕上显示的窗口或控件,用于可视化和编辑 Model 的内容。

ViewModel 的存在是为了“帮助”View 和 Model 进行通信。老实说,我认为在许多情况下 ViewModel 是多余的,View 可以直接与 Model 通信。然而,作为一种模式,ViewModel 总是存在。此外,如果我要“干掉”一个类,因为我可以简化事情,我会干掉 ViewModel,而不是 Model。

你发现问题了吗?

好吧……这是一个不公平的问题。人们可能会看到许多不同的问题,我不能指望看到一个与我当前想法匹配的答案。

到目前为止,我看到的问题是 MVVM 完全是关于在 View 和 Model 之间创建同步,使用 ViewModel 作为辅助,但它没有谈论任何业务逻辑。

在本文中,MVVM-B 完全是关于业务逻辑的。也许我只是在解释 MVVM 一直以来的意图。也许我正在创建一个次要模式(这也解释了名称末尾的 B)。无论如何……

一些公认的解决方案

在我看来,MVVM 业务逻辑的最大问题是:

  • 来自 MVC 的人说业务逻辑最初应该在控制器中,所以现在应该在 ViewModel 中;
  • 来自简单 OOP 或富数据库对象的人说 Model 需要包含业务逻辑,并包含像 Insert()Update()Delete() 这样的方法。

此外,还有使用 POJOs、POCOs、POSOs 等的新想法,我甚至开玩笑说我们应该将编程语言从等式中剔除,而不是说一个对象是纯旧 Java 对象或类似的东西,我们应该直接说它们是 POO(即,纯旧对象,不涉及编程语言)。

好吧……我知道 POO 在英语中听起来很糟糕,所以我会说它们只是 POxO,许多人的想法是,我们使用简单的对象而不是复杂的对象。这些简单的对象被传递给其他真正知道如何操作的对象/方法。

总之,Model 应该只是 POxO 吗?

当我第一次阅读 MVVM 时,它们不是。但是随着这种“新理解”,即它们需要是 POxO,似乎 MVC 类似的解决方案(ViewModel 包含业务逻辑)才是正确的答案。

所以,我不想同意这一点,我想更深入地探讨这个问题。

回到问题的根源以寻求答案

我小时候(自学时)和高中时看到的最基本的数据库示例之一,就是如何创建一个代码/名称表,然后向其中插入数据。

我会在屏幕上有两个文本框,一个“保存”按钮,其“OnClick”实现会像这样:

  ExecuteQuery("INSERT INTO SomeTable(Code, Name) 
    VALUES (" + textBoxCode.Text + ", '" + textBoxName.Text + "')");

只要用户在点击“保存”之前,在文本框中输入了真正期望的内容,这样的代码就能正常工作。这样的代码容易受到 SQL 注入和许多其他问题的影响,比如如果用户需要在名称中使用 '" 时就会失败。

无论如何,我不想在这篇文章中处理 SQL 注入。我只想说,假设所有值都正确,它就能工作,插入就会发生。

我还要说,像 MVVM 这样的模式不会帮助解决 SQL 注入问题。这不是它试图解决的问题。

那么,MVVM 到底解决了什么问题?

代码隔离

答案实际上是“代码隔离”。但在探讨这个问题之前,我们知道这意味着什么吗?或者这有多重要?

如果我们只是说“我遵循了模式,我的代码被正确隔离了”,我们真的解决了一个问题吗?

使用相同类的不同应用程序 - 包括单元测试

作为对前面问题的回答,人们关心 MVVM 的主要原因之一是它使代码更“可测试”。

但“可测试”只是另一种说法,即代码能够在“用户输入信息并点击屏幕上的保存按钮”之外的不同情况下运行。

例如,我可能决定创建一个高度依赖数据库的应用程序,该应用程序为数据库中存在的每个表都有一个视图,并且还有一个控制台应用程序,它读取文本文件并使用相同的代码插入数千(甚至数百万)条记录。

也就是说,我刚刚展示的代码

  ExecuteQuery("INSERT INTO SomeTable(Code, Name) 
     VALUES (" + textBoxCode.Text + ", '" + textBoxName.Text + "')");

将需要由控制台应用程序执行。

你看到问题了吗?

如果你还没有发现问题,我来解释一下。像 textBoxCode.TexttextBoxName.Text 这样的部分意味着我们需要创建两个有效的文本框,并填充它们的文本,才能使一切正常工作。

所以,我们有可能需要实际创建窗口,显示它,在它的文本框中输入值,才能最终插入一条记录。

对于一个只导入数据的控制台应用程序来说,这难道不是太多了吗?

直接“执行插入”而不处理可视化控件难道不更简单吗?

答案是:是的 - 让我们在一个单独的函数中进行插入

因此,我们不再直接从点击事件访问文本框,而是可以有一个函数/方法,例如:

void InsertIntoSomeTable(int code, string name)
{
  ExecuteQuery("INSERT INTO SomeTable(Code, Name) VALUES (" + code + ", '" + name + "')");
}
// Again, this code is susceptible to SQL injection, but I am just dealing
// with MVVM, not SQL Injection, in this article.

所以,现在,这个函数就是控制台应用程序将调用的。它不需要访问任何 View(或与 View 相关的对象)。只要这段代码与 View 足够“隔离”,我们就可以调用它。此外,对于 View 本身,“最简单/最愚蠢”的解决方案是这样调用这个函数:

  InsertIntoSomeTable(int.Parse(textBoxCode.Text), textBoxName.Text);

这不仅对有效值执行与以前相同的工作,而且如果 textBoxCode 中的值无法转换为整数,它会立即引发错误,而不是让数据库驱动程序报告错误。

InsertIntoSomeTable 在哪里?

在原始解决方案中,整个代码作为 View 的一部分存在。是 View 的“OnClick”包含了访问数据库的代码。

现在,因为我们有一个可视化应用程序(带有 View 的那个)和一个控制台应用程序(读取文本文件的那个),我们不得不将部分代码放到“其他地方”。

那个“其他地方”是控制台应用程序和可视化应用程序之间的公共代码。我不在乎你会称之为“公共”库、“businessLogic”库或类似的东西。它只需要作为独立于可视化应用程序和控制台应用程序的东西存在。

重要的是,这个“公共代码”并非可视化应用程序独有。谈到 MVVM,这意味着这段代码不能成为 ViewModel 的一部分,因为一个只读取文本文件的控制台应用程序绝不应该处理 ViewModel

请注意,我们离 MVVM 模式还很远。无论如何,很明显,可视化应用程序和“导入数据控制台应用程序”之间的“公共代码”不应该存在于应用程序的“可视化”端附近。也就是说,它们不应该存在于 ViewViewModel 中。

那么,它们到底住在哪里?

我会说它们要么作为 Model 本身的一部分存在(所以 Model 不仅仅是 POxO 对象),要么作为独立的事物存在,比如在业务逻辑类中。而这在 MVVM 自身中并没有涵盖。

展望未来

目前的情况是

  • 我们只有一个表;
  • 我们有一个可视化(GUI)应用程序用于插入记录,还有一个控制台/导入器应用程序用于读取文本文件并插入记录;
  • 我们已经能够使“一些代码”在这两个应用程序之间共享。

到目前为止,我们还没有 ModelViewModel

对于只有两个字段,一个 Model 可能听起来多余。但是,如果不是只有两个字段,而是有 50 个字段,那么调用 InsertSomeRecord(record) 可能比调用 InsertSomeRecord 并传递 50 个参数更有意义。

此外,根据实现方式,我们可以调用 record.Insert(); 而不是 InsertSomeRecord(record);

这意味着我们将创建一个记录,填充其字段并调用 Insert();,无论它是控制台应用程序还是可视化应用程序。

这不是 Model 吗?

在我偏离主题太远之前……你有没有发现我们只是在创建一个 Model 类?

Model 实际上包含我们保存到数据库的所有数据……以及所需的 Insert() 方法。这样的类可以被可视化应用程序和控制台应用程序使用。谈到 MVVM,我们已经达到了一个阶段,一边是 View,另一边是 Model……我们现在只需要 ViewModel。

但是,最重要的是,你有没有注意到业务逻辑已经在这里了,而不是 View 或 ViewModel 的一部分?这是迄今为止最重要的事情。我们绝不希望将业务逻辑放在 ViewModel 中。

最后,ViewModel!

ViewModel 存在的唯一原因是 Model 中的数据格式不正确,或者 View 具有一些我们不想保存(到 Model 中)的额外特性。

例如,ViewModel 可能包含一个不时变化的背景颜色,这不属于 View 逻辑,但也不属于 Model。

ViewModel 也可能将枚举转换为字符串以供视图显示。特别是这种情况,在 WPF 这样的框架中似乎无关紧要,因为我们可以使用转换器(并创建默认转换器)等,而不是依赖 ViewModel,但它们在其他环境中可能更有用,这似乎是 iOS/Swift 的情况,但即使如此也可能值得商榷。

另一个重要的点是:动作!

WPF 使用命令的概念,这似乎不适用于 iOS。

无论如何,所有这些都可以成为 ViewModel 的“方法”。但这并不意味着我们将“业务逻辑”放入 ViewModel 中。ViewModel 应该只是“重定向”到其他类,而不是成为包含业务逻辑的类。

因此,作为一种模式,我们有:

  • Model - 可能是一个 POxO 对象;
  • View - 屏幕上实际显示的内容;
  • Business - 使模型进入数据库,或从数据库创建模型的代码 - 可以分离或作为模型的一部分;
  • ViewModel - 仅仅是一个辅助程序,用于将模型数据放入视图,将视图数据放入模型,并且可能调用一些业务方法,而不是业务对象本身。

这将创建一个 MV-B-VM 模式,但是,由于 MVVM 如此广为人知,最好将其命名为 MVVM-B。意为 Model-View-ViewModel-Business。

历史

  • 2022年3月12日:初始版本
© . All rights reserved.