揭秘和简化 MVC 框架





5.00/5 (18投票s)
MVC 作为一种设计模式,已经被“四人帮”明确定义,但近年来,它更多地被用作一个营销术语,而非设计模式。本文旨在简化和定义 MVC 设计模式是什么,并提供一个你可以重用于自己项目的简单 MVC 框架。
引言
模型-视图-控制器 (MVC) 设计模式已经存在很长时间了。多年来,它一直是创建能够在技术、实现甚至理念之间扩展的优秀应用程序的黄金标准。在20世纪90年代,微软将其 MFC (Microsoft Foundation Classes) 应用程序框架建立在 MVC 设计模式之上;将 GUI 从逻辑和数据访问中抽象出来是一个新颖的概念,事实证明,这对开发人员和微软来说都是一个巨大的竞争优势。
背景
随着 VB6 和 .Net 的推出,企业编码理念变得更像一个“狂野西部”(这些领域的开发人员比 C++ MFC 的开发人员便宜得多)。经典 ASP 与 VB6 COM 对象成为一种潮流,并被 ASP.Net WebForms 所取代。ASP.Net WebForms 遵循与 VB6 桌面应用程序相同的事件模型,这使得它对那些在商业世界中构建客户端-服务器应用程序的开发人员非常容易上手。世界似乎正在从 MVC 转向一个以网络为中心、前端为主导的世界,这让人回想起大型机时代。
接着,微软让世界大吃一惊,发布了 ASP.Net MVC。哇。这简直是回到了原点!实际上,这对于微软来说是一个相当可预测的举动。.Net 开发环境正在迅速成熟,开发人员渴望抛弃 ASP.Net WebForms 及其开销。许多人已经开始创建自己的 MVC 框架(例如我的白天“另一个自我”),因为他们研究了真正的面向对象编程(OOP)原则。有一点是肯定的,MVC 作为一种设计模式正在重新回到前沿。
自 ASP.Net MVC 推出以来,Javascript 开发者创建了自己的 MVC 框架,例如 AngularJS。虽然这在理论上是一项崇高的努力,但用 Javascript 编写的单页应用程序 (SPA) 的性能差异很大,因为它们受浏览器、计算机、平板电脑和手机的不可预测性影响。大型企业的内部网受到的影响不如公共网站大,因为它们通常对运行 Javascript 的客户端有更多的控制权。
关于 SimpleMVC
我有多年的各种类型(ASP.Net、WinForms、WPF、UWP、Android、Xamarin)MVC 应用程序开发经验,我开发了许多可重用的概念(参见我链接到我的“另一个自我”的文章),这些概念帮助我快速轻松地在几分钟内启动并运行一个基于 MVC 的应用程序。我认真思考了这个问题,并决定是时候创建一个 MVC 库了,任何希望构建易于实现和测试的应用程序的人都可以重用它。
我刚才提到测试了吗?是的,我确实提到了。你看,MVC 最伟大的特性之一是各层对其他层的内部工作一无所知,除了它们公开的公共契约。这通过适当使用接口来抽象各个层并隔离相关部分来实现。MVC 是真正的 OOP,它呈现了封装(模型和控制器)、多态(视图和模型)和继承(控制器)。当你学习 MVC 时,你也在学习 OOP。
SimpleMVC 实际上是经过十多年的酝酿而成的。我创建过更简单的 MVC 框架,也创建过更复杂的 MVC 框架。SimpleMVC 是两者之间的平衡,与爱因斯坦的主张非常一致:事物应该简单,但不能比它们本来的样子更简单。
说了这么多,我们来谈谈 SimpleMVC 是什么,不是什么。
SimpleMVC 是
- 可扩展
- 简单
- 可复用的
- 健壮的
- 松散耦合的
- 紧密内聚的
- 基于标准的
SimpleMVC 不是
- 脆弱的
- 过程化的
- 一个营销工具
SimpleMVC 作为一个框架,公开了一些非常有用的公共接口和基类。
利用 MVC 架构构建一个出色的应用程序,您所需要的就是这些。
使用代码
请遵循以下步骤
- 通过扩展
ModelBase
来创建您的模型。模型基类允许模型公开对模型进行 CRUD 操作的基本功能。如果您正在使用 Entity Framework 或您希望的任何 DAL 提供程序,您只需在从ModelBase
扩展的方法中实现访问代码。控制器将对模型内部的详细信息一无所知;控制器所要做的就是请求数据操作并处理结果。为了进一步增强应用程序的可重用性,您应该使用适配器模式为您的模型提供一个框架,以便利用该框架访问数据。 - 使用表达应用程序用户界面的接口来扩展
ISimpleView
接口。每一组独立的功能都值得拥有自己的接口。如果您正在创建一个 WPF 应用程序,您将为每个页面以及可重用控件创建一个接口。对于 ASP.net Webforms 应用程序,页面将获得一个接口,任何用户控件也将获得自己的接口。 - 扩展
SimpleControllerBase
。在这里您需要做出一些重要的决定。在微软的 ASP.Net 框架中,您有一个控制器-每个-模型的范式。我不认为这是应用程序的准确划分。我建议将*相关*功能的业务逻辑分组在一起。例如,如果您有一个学生注册课程并参加考试。在微软的范式中,您将有三个模型和三个控制器。在 SimpleMVC 的世界中,建议您有三个模型但只有一个控制器。控制器是模型和视图之间的粘合剂。没有业务逻辑,模型和视图就没有意义,而且业务逻辑很少能 neatly 封装到单个模型的数据中。 - 创建模拟 UI 对象,它们实现步骤 1 中创建的抽象视图。
- 创建模拟 DAL 对象,它们实现操作模型持久化所需的 CRUD 功能。
- 创建单元测试,通过模拟视图的事件和回调来执行控制器中的业务规则。
- 让所有测试都通过。(这意味着让你的测试成功)。
假设我们接到任务,要创建一个小的 WinForms 应用程序来管理学生的入学注册。这个 WinForms 应用程序将由一两位管理员运行。该应用程序将与学校网络上的 SQL Server 数据库进行通信。该应用程序只需要创建、更新、打印和删除学生。
接下来,假设我们接到任务,要创建一个小的 Asp.Net WebForms 应用程序,允许学生随时随地注册课程。学生数据将从学校网络上的 SQL Server 数据库中检索。
最后,设想我们被要求创建一个网络服务,供教授们提交成绩。他们可能使用 Excel 或其他应用程序来记录学生的测试和作业,但最终,无论成绩来自何处,都必须汇总到一个地方。
这里我们有三个截然不同的应用程序,但它们都围绕同一个问题域工作:学生。
我们知道所有数据都将存储在学校的 SQL Server 中。我们知道与数据交互的逻辑在这三个应用程序之间是一致的。我们知道从视图中与控制器交互时要使用哪些数据。
第一步:模型
我们知道我们需要存储一些关于学生的信息,例如他们的出生日期、姓氏、名字和照片。学生将通过一个 StudentID
号码来识别。此外,学生将参加课程,我们也必须定义这些课程。一门课程将有名称、时间表和教授。最后,课程将有成绩,成绩将包括日期、百分比分数和名称。所有这些都将存储在 SQL Server 中,因此我们将创建一个数据库图来构建模型。
现在,尽管这是很好的第三范式关系数据,但由于几个原因,它与应用程序融合得不好。
- 交叉表操作起来很不方便。用 List 来表示多对多关系的另一端的聚合要简单得多。
- 尽管 Grade 表在技术上是一个交叉表,但其范围首先是针对 Class,因为输入将来自 Professor,而不是 Student。因此,Grade 的唯一链接将通过 Class 对象作为
Dictionary<Student, List<Grade>>
。
这样我们就需要创建三个模型,如下所示
public class StudentModel : ModelBase { public string StudentID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public byte[] Picture { get; set; } public List<Class> Classes { get; set; } // Boilerplate CRUD methods } public class ClassModel : ModelBase { public Guid ClassID { get; set; } public string ClassName { get; set; } public string Professor { get; set; } public DateTime Time { get; set; } public bool IsMonday { get; set; } public bool IsTuesday { get; set; } public bool IsWednesday { get; set; } public bool IsThursday { get; set; } public bool IsFriday { get; set; } public List<StudentModel> Students { get; set; } public List<StudentModel, List<GradeModel>> Grades { get; set; } // Boilerplate CRUD methods } public class GradeModel : ModelBase { public Guid GradeID { get; set; } public string GradeName { get; set; } public decimal GradeValue { get; set; } public StudentModel Student { get; set; } public ClassModel Class { get; set; } // Boilerplate CRUD methods }
第二步,视图
我们现在准备定义我们的视图。首先,是 WinForms 应用程序——我们从您喜欢的工具中模拟表单开始。
这个窗口包含了一些有趣的元素。首先,有一个搜索框用于查找学生,还有一个网格用于显示搜索结果。这是一个主视图,将作为它自己的 ISimpleView
实现,名为 IStudentMasterView
。
IStudentMasterView
需要一个属性来保存搜索条件,一个学生模型集合,一个事件来通知控制器执行搜索,以及一个回调函数来让控制器通知视图搜索结果。
public interface IStudentMasterView : ISimpleView { string Criteria { get; } List<StudentModel> StudentsResult { get; set; } event EventHandler PerformSearch; void Databind(); }
窗口的下半部分是详情区。当用户从网格中选择用户时,表单将填充其出生日期、名字、姓氏和照片。另外,用户可以点击“新建”按钮来初始化新学生的子表单。在子表单中,我们将需要学生 ID、名字、姓氏、出生日期和照片的属性。接下来,我们需要事件来初始化新学生和保存学生。最后,我们需要一个回调函数来通知视图保存已完成,以防保存是异步的(也应该如此)。
public interface IStudentDetailView : ISimpleView { string StudentID { get; set; } string FirstName { get; set; } string LastName { get; set; } DateTime DOB { get; set; } event EventHandler LoadStudent; event EventHandler SaveStudent; void StudentLoaded(); void StudentSaved(); }
最后,您将在 Visual Studio 中创建您的 WinForms 窗口,并为主控和详细信息创建两个用户控件。主控用户控件将实现 IStudentMasterView
,详细信息用户控件将实现 IStudentDetailView
。
将相应的属性连接到窗口控件的访问器属性,将网格绑定到学生模型列表,并将按钮连接以触发事件。很快,您将拥有一个完整的用户界面,以满足学生注册应用程序的需求!
第三步,控制器
到目前为止,我们已经设计了分别与数据库和用户交互的模型和视图。现在是时候应用一些逻辑来完成两者之间的交互了。关于控制器和业务逻辑,有两种思想流派。一方面,您会听到所有业务规则都应该封装在独立的类和程序集中,以便逻辑可以在应用程序和架构之间共享。另一方面,MVC 模式被认为是如此通用,以至于控制器*就是*业务规则,并且只是提供了这些规则的公共接口;新的应用程序无论如何都应该使用 MVC 模式,以利用控制器和模型,即使视图完全不同。就我个人而言,我喜欢第二种立场,因为它在逻辑、模型和 UI 之间保持了强大的内聚性,而无需强制任何紧密耦合。此外,它使得应用程序更加健壮,因为业务逻辑的更改不必在多个地方实现。如果业务逻辑代码与控制器分离,并且您在另一个项目中有一个控制器使用相同的库,那么对该业务逻辑代码的更改可能需要对两个控制器进行代码更改,从而强制对两个应用程序进行质量保证周期。
在我们的示例应用程序中,我们已经定义了学生的视图。将来,将有用于注册课程和发布成绩的新视图,两者都使用不同的实现技术。现在,我们将从控制器的事件处理程序开始,用于两个视图接口。
public class StudentController : SimpleControllerBase { public override bool Initialize() { var result = true; foreach(var view in Views.Values) { if(view is IStudentHeaderView) { var shv = view as IStudentHeaderView; shv.PerformSearch += ShvOnPerformSearch; result &= true; } else if(view is StudentDetailView) { var sdv = view as IStudentDetailView; sdv.PerformSearch += SdvOnLoadStudent; sdv.SaveStudent += SdvOnSaveStudent; result &= true; } else { result = false; } } return result; } public async void ShvOnPerformSearch(object sender, EventArgs e) { var view = sender as IStudentHeaderView; if(view != null) { var models = await StudentModel.SearchAsync( new SearchCriteria<StudentModel> { SearchCriteriaTypes = SearchCriteriaTypes.Contains, CriteriaValues = new List<object> { view.Criteria } }); view.StudentsResult = models; view.Databind(); } } public async void SdvOnLoadStudent(object sender, EventArgs e) { var view = sender as IStudentDetailView; if(view != null) { // Marshal the data to the View. // We don't have an overload of LoadAsync that accepts a string var model = await StudentModel.LoadSearchAsync( new SearchCriteria<StudentModel { SearchCriteriaTypes = SearchCriteriaTypes.IsEquals, CriteriaValues = new List<object> { view.StudentID } }).FirstOrDefault(); if(model != null) { view.StudentID = model.StudentID; view.FirstName = model.FirstName; view.LastName = model.LastName; view.DateOfBirth = model.DateOfBirth; view.Picture = model.Picture; view.Classes = model.Classes; view.StudentLoaded(); } } } public async void SdvOnSaveStudent(object sender, EventArgs e) { var view = sender as IStudentDetailView; if(view != null) { // Business rules here! if(view.DateOfBirth >= DateTime.Today) throw new ApplicationException("Invalid Date of Birth."); // Marshal the data from the View. // We don't have an overload of LoadAsync that accepts a string var model = await StudentModel.LoadSearchAsync( new SearchCriteria<StudentModel { SearchCriteriaTypes = SearchCriteriaTypes.IsEquals, CriteriaValues = new List<object> { view.StudentID } }).FirstOrDefault(); if(model == null) { model = new StudentModel(); } model.StudentID = view.StudentID; model.FirstName = view.FirstName; model.LastName = view.LastName; model.DateOfBirth = view.DateOfBirth; model.Picture = view.Picture; model.Classes = view.Classes; if(await Model.SaveAsync() == 1) { view.StudentSaved(); } } } }
信不信由你,这就是我们所需要的一切。
几点说明
- 事件处理程序被标记为异步,以便它们可以在响应式 UI 环境中使用。WPF、UWP 甚至 WinForm 应用程序都从中受益匪浅。
- 显示逻辑位于视图的实现中(显示逻辑与业务规则是另一个话题),这保持了松散耦合。
- 模型公开了一个迷你搜索引擎,其详细信息在适配器类中实现(这是另一个话题)。
在 GitHub 上,SimpleMVC 仓库包含一个单元测试项目。克隆仓库,研究它并运行测试。项目站点位于http://simplemvc.gatewayprogramming.school。
结论
人们普遍对编程充满热情,关于如何实现 MVC 模式的争论自“四人帮”引入它以来就一直激烈。本文只是又一个立足点。我强烈建议您在盲目遵循单一实现方法之前,多了解 MVC 模式。