使用MVC设计模式在Access中实现数据的“查改增删”

使用MVC设计模式在Access中实现数据的“查改增删”

一、前言

“高内聚,低耦合”一直是开发人员追求的目标,因为这样的系统具有更好的重用性,维护性和扩展性。它也是衡量软件设计好坏的标准之一。

MVC是被广泛使用的设计模式,因为它很好的诠释了“高内聚,低耦合”的软件应该怎样设计的。

作为备受专业开发人员诟病的Access应用程序,大多数都是“为了目标不择手段”而开发出来的,“低内聚,高耦合”是它们的典型特征。

本文试图在Access VBA中创建一个MVC设计模式的实现方式,为有兴趣进一步提升自己的Access开发者提供一点灵感。

鉴于Access应用程序中,广泛存在着对数据的“查改增删”的功能,本文将按照MVC的设计模式,来实现这4个功能。


二、MVC简介

MVC模式是软件工程中的一种软件架构模式,它把软件系统分为3个基本部分:模型(Model),视图(View),控制器(Control)。

  • 模型(Model) - 该层代表着应用程序中的数据,它主要负责数据的存取。它不会与视图(View)层直接对话。
  • 视图(View) - 该层处理人机交互和前端控制。它不会与模型(Model)层直接发生关系,但它知道当数据发生改变的时候,需要更新自身的显示。
  • 控制器(Controller)- 负责处理模型(Model)层与视图(View)层之间的交互。

从上述对MVC设计模式的简单介绍中,我们可以看到,模型(Model)层与视图(View)层之间是相互分离的(解耦)。

这种数据与界面的分离,给我们带来两个方面的好处:

  1. 在同一个模型(Model)之上,可以创建多个视图(View)。换句话说,当用户界面需要做出任何调整的时候,数据层可以保持不变。
  2. 如果数据源有任何变化,比如,从本地表变成了外部数据,我们只需要简单修改一下模型(Model)就能立刻运行,用户界面不需要做任何变更。


三、示例数据介绍

我们以一个简单的人事数据为例,比如有一个名为tblData的表:

表中各字段的类型和主键索引如下图:

我们将以该表数据为基础,用MVC设计模式,为其建立一套“查改增删”的功能。


四、模型层(Model)设计

在第二节介绍过,模型层(Model)代表着应用程序中的数据,主要负责数据的存取。整个程序中,只有该层与数据交互。

首先,在VB IDE中新建一个类模块:姑且命名为Model。

用什么来代表tblData中的数据呢?我们可以为tblData中的每一个字段定义个私有变量:

Option Compare Database
Option Explicit

Private mlngEmployeeNo As Long
Private mstrEmployeeName As String
Private mstrGender As String
Private mdatDOB As Date
Private mstrJobTitle As String
Private mcurSalary As Currency

如何将这些私有变量存取到表tblData中呢?我们可以用DAO中的Recordset对象来充当这个中间人角色。自从Access 2007开始,DAO重新回到了数据交互对象的第一梯队,当然你也可以选择使用ADO。

Option Compare Database
Option Explicit

'...
'此处有省略的代码
'...

Private mrst As DAO.Recordset2

Private Sub Class_Initialize()
    Dim db As DAO.Database
    Set db = CurrentDb()
    Set mrst = db.OpenRecordset("tblData")
End Sub

Private Sub Class_Terminate()
    mrst.Close
    Set mrst = Nothing
End Sub

上述代码定义好Recordset对象变量mrst以后,在类的实例化事件中,为mrst创建基于表tblData的数据集。

从该处代码可以看到,tblData目前是位于本地的,如果哪一天tblData需要移到别的地方,我们只需要修改此处的代码即可。

最后,相应的,在类的销毁事件中,将mrst对象变量释放掉。

接下来,我们为Model类模块设计外部接口。首先要考虑的外部接口,肯定是对私有变量的读取:

'...
'以上代码省略

Public Property Get EmployeeNo() As Long
    EmployeeNo = mlngEmployeeNo
End Property

Public Property Let EmployeeNo(ByVal value As Long)
    mlngEmployeeNo = value
End Property

Public Property Get EmployeeName() As String
    EmployeeName = mstrEmployeeName
End Property

Public Property Let EmployeeName(ByVal value As String)
    mstrEmployeeName = value
End Property

Public Property Get Gender() As String
    Gender = mstrGender
End Property

Public Property Let Gender(ByVal value As String)
    mstrGender = value
End Property

Public Property Get DOB() As Date
    DOB = mdatDOB
End Property

Public Property Let DOB(ByVal value As Date)
    mdatDOB = value
End Property

Public Property Get JobTitle() As String
    JobTitle = mstrJobTitle
End Property

Public Property Let JobTitle(ByVal value As String)
    mstrJobTitle = value
End Property

Public Property Get Salary() As Currency
    Salary = mcurSalary
End Property

Public Property Let Salary(ByVal value As Currency)
    mcurSalary = value
End Property

这些读取属性代码只是简单的完成对私有变量的读写,如果你稍微发挥一点想象力,我们可以在其中为各个属性添加商业规则,为简化起见,暂不展开。

除了上述的私有变量读写的外部接口,我们还需要哪些接口?重点来了!!!

当然是“查改增删”接口了。

我们首先看“查”:

我们设想的是,如果提供一个员工号EmployeeNo,那么需要返回改员工号的相应数据。所以我们可以设计一个函数,可是函数只能返回一个值,而我们却需要返回员工姓名、性别、出生日期、岗位名称、工资等好多个值,怎么办?

我们可以将这些返回值保存在类的私有变量中。这也是当时定义这些私有变量初衷:

'...
'以上代码省略

'查
Public Function GetData(ByVal EmployeeNo As Long) As Boolean
    GetData = False
    With mrst
        .Index = "PrimaryKey"
        .Seek "=", EmployeeNo
        If Not .NoMatch Then
            Me.EmployeeNo = .Fields("EmployeeNo")
            Me.EmployeeName = .Fields("EmployeeName")
            Me.Gender = .Fields("Gender")
            Me.DOB = .Fields("DOB")
            Me.JobTitle = .Fields("JobTitle")
            Me.Salary = .Fields("Salary")
            GetData = True
        End If
    End With
End Function

这里使用的是Recordset的Seek方法,该方法需要提供索引值,具有非常快的查询效率。当然它也只能应用于表类型的数据集。我们可以根据函数的返回值来判断是否查询到了数据。

说完了“查”(GetData),接下来谈谈“改”。

从逻辑上来说,修改tblData的某条数据,一定是首先已经找到了那条需要修改的数据。我们需要告诉“改”函数需要修改哪一条数据,同时也需要告诉“改”函数,需要将数据修改成什么目标值。这里需要传递给“改”函数的参数就比“查”(GetData)函数多得多了。

不绕弯子了,我们给“改”接口函数传递一个Model类型的参数!!!

'...
'以上代码省略

'改
Public Function Update(ByRef data As Model) As Boolean
    Update = False
    With mrst
        .Index = "PrimaryKey"
        .Seek "=", data.EmployeeNo
        If Not .NoMatch Then
            .Edit
                .Fields("EmployeeName") = data.EmployeeName
                .Fields("Gender") = data.Gender
                .Fields("DOB") = data.DOB
                .Fields("JobTitle") = data.JobTitle
                .Fields("Salary") = data.Salary
            .Update
            Update = True
        End If
    End With
End Function

上述代码向Model类中的函数Update传递Model类参数,这看上去真的有点恐怖,就像自己把自己给吃了一样,这在逻辑上说得通吗?是的,第一眼看确实说不通。但是,你要知道,类是不存在的,只存在对象。一个Model对象将另一个Model对象作为参数。这样一想,一切就都很符合逻辑了!

将Model类对象作为Update的参数,带来的好处是很明显的,它既能告诉我们要改哪条记录,又可以告诉我们改后的目标值。

我们再回头看看“查”(GetData)接口函数,完全可以将它的返回值类型改为Model类型,这个思路先留给有兴趣的读者自行实践吧。

接下来,我们来看看“增”。

为tblData添加一条记录,首先当然要提供新记录的所有字段的值,还需要考虑主键问题。员工号不能重复。基于这些考虑,我们还是为“增”接口函数传递Model类型的参数:

'...
'以上代码省略

'增
Public Function AddNew(ByVal data As Model) As Boolean
    AddNew = False
    With mrst
        If Not Me.GetData(data.EmployeeNo) Then
            .AddNew
                .Fields("EmployeeNo") = data.EmployeeNo
                .Fields("EmployeeName") = data.EmployeeName
                .Fields("Gender") = data.Gender
                .Fields("DOB") = data.DOB
                .Fields("JobTitle") = data.JobTitle
                .Fields("Salary") = data.Salary
            .Update
            AddNew = True
        End If
    End With
End Function

上述代码首先根据提供的参数,看看能不能在已有数据中查到相同的员工号。如果不能,才允许将记录添加到tblData中。

最后,我们来看看“删”。

“删”比较简单,提供一个员工号,找到后将其删除就好了:

'...
'以上代码省略

'删
Public Function Delete(ByVal EmployeeNo As Long) As Boolean
    Delete = False
    With mrst
        .Index = "PrimaryKey"
        .Seek "=", EmployeeNo
        If Not .NoMatch Then
            .Delete
            Delete = True
        End If
    End With
End Function

至此,我们就完成了模型(Model)层的所有接口的设计。


五、视图层(View)设计

第二节中谈到过,该层主要负责将数据显示到用户界面。在Access中,我们一般用窗体(Form)来做用户界面,所以我们要设计将数据显示到窗体界面的接口。

这里要解释一下,为什么我们不直接在窗体代码模块中处理数据的显示问题,而是额外用一个类模块View来处理?

其实目的就是让窗体与数据的显示解耦。这样,多个窗体就能使用相同视图层(View)中的逻辑。而窗体本身只用专注于用户界面的设计。

首先,在VB IDE中添加一个新的类,命名为View。

因为我们要将数据显示到窗体上,所以我们要声明一个Access窗体变量:

Option Compare Database
Option Explicit

Private mfrm As Access.Form

虽然有了mfrm窗体对象变量,但在代码运行时,并不知道该对象变量会指向具体的哪一个窗体。所以View对象在初始化后,第一件事情,就是为mfrm指定一个具体的窗体。

'...
'以上代码省略

Public Sub Init(ByRef Form As Access.Form)
    Set mfrm = Form
End Sub

Private Sub Class_Terminate()
    If Not mfrm Is Nothing Then
        Set mfrm = Nothing
    End If
End Sub

当然在View类对象销毁时,释放掉mfrm的指针。

然后我们要实现一个接口函数Display,该函数将数据显示到mfrm窗体上。

这里需要考虑两个问题,第一个问题,数据在哪里?这个数据将会作为参数传递给Display函数。因为我们已经设计好了模型层(Model),该模型层就代表着数据,所以我们用一个Model对象变量作为Display的参数。

第二个问题,数据将显示在mfrm窗体的什么地方,或者说哪些控件中?这里我们需要提前做一些假定:针对Model对象变量中的每个属性,假定mfrm窗体都有相应的文本框控件,控件名以“txt”+属性名命名。

'...
'以上代码省略

'数据显示
Public Function Display(ByRef data As Model) As Boolean
    Display = False
    If Not mfrm Is Nothing Then
        With mfrm
            .Controls("txtEmployeeNo") = data.EmployeeNo
            .Controls("txtEmployeeName") = data.EmployeeName
            .Controls("txtGender") = data.Gender
            .Controls("txtDOB") = data.DOB
            .Controls("txtJobTitle") = data.JobTitle
            .Controls("txtSalary") = data.Salary
        End With
        Display = True
    End If
End Function

在显示之前,要确保mfrm对象变量已经指向了一个具体的窗体。

有了可以将数据显示给窗体的接口Display,我们还需要一个反向的接口:GetDisplayedData。获取mfrm窗体上所显示的数据。

'...
'以上代码省略

'获取显示数据
Public Function GetDisplayedData() As Model
    If Not mfrm Is Nothing Then
        Set GetDisplayedData = New Model
        With mfrm
            GetDisplayedData.EmployeeNo = Nz(.Controls("txtEmployeeNo"), 0)
            GetDisplayedData.EmployeeName = Nz(.Controls("txtEmployeeName"), "")
            GetDisplayedData.Gender = Nz(.Controls("txtGender"), "")
            GetDisplayedData.DOB = Nz(.Controls("txtDOB"), 0)
            GetDisplayedData.JobTitle = Nz(.Controls("txtJobTitle"), "")
            GetDisplayedData.Salary = Nz(.Controls("txtsalary"), 0)
        End With
    End If
End Function

最后再添加一个帮助函数,帮助清空mfrm窗体上数据的显示:

'...
'以上代码省略

'清理窗体
Public Function Clear() As Boolean
    Clear = False
    If Not mfrm Is Nothing Then
        With mfrm
            .Controls("txtEmployeeNo") = Null
            .Controls("txtEmployeeName") = Null
            .Controls("txtGender") = Null
            .Controls("txtDOB") = Null
            .Controls("txtJobTitle") = Null
            .Controls("txtSalary") = Null
        End With
        Clear = True
    End If
End Function

至此,视图层(View)的设计就大功告成了。


六、控制器层(Controller)设计

如第二节所述,该层负责处理模型层(Model)与视图层(View)之间的交互。用户窗体界面将使用该层来控制“查改增删”操作。

我们在VB IDE中界面,再创建一个类模块,命名为Controller。

显然,我们需要定义两个对象变量,并将他们初始化:

Option Compare Database
Option Explicit

Private mobjModel As Model
Private mobjView As View

Private Sub Class_Initialize()
    Set mobjModel = New Model
    Set mobjView = New View
End Sub

Public Sub Init(ByRef Form As Access.Form)
    mobjView.Init Form
End Sub

Private Sub Class_Terminate()
    Set mobjModel = Nothing
    Set mobjView = Nothing
End Sub

由于View初始化时,需要给它指定一个具体的窗体,所以我们需要为Controller也设计一个Init接口,传入那个具体的窗体对象。

接下来,我们需要为控制器层(Controller)实现“查改增删”接口。读者可能会问,我们在模型层(Model)中已经实现了“查改增删”接口了,为什么还需要再实现一遍?

因为在最终的用户操作界面的窗体代码中,并不会直接使用模型层(Model)中的接口函数。而是使用控制器层(Controller)来操作数据。我们这里重新实现一边“查改增删”,也不是真的重写一遍,而是将这些操作代理给模型层(Model)对象变量mobjModel。

先看“查”:

'...
'以上代码省略

'清理窗体+查+显示数据
Public Sub Find(ByVal EmployeeNo As Long)
    mobjView.Clear
    If mobjModel.GetData(EmployeeNo) Then
        mobjView.Display mobjModel
    End If
End Sub

这里的“查”Find 与模型层(Model)中的“查”GetData不同,除了查询出数据,还对数据界面做了额外的操作:在查之前,做窗体清理,查完之后,做数据显示。这就是控制器层(Controller)要做的事情:协调模型层和视图层,把问题解决。

接下来看看“改”和“增”:

'...
'以上代码省略

'保存 = 改 或者 增
Public Sub Save()
    Dim objModel As Model
    Set objModel = mobjView.GetDisplayedData
    
    If mobjModel.GetData(objModel.EmployeeNo) Then
        mobjModel.Update objModel
    Else
        mobjModel.AddNew objModel
    End If
    
    Set objModel = Nothing
End Sub

在数据表tblData的层面来讲,修改数据和新增数据,是2种截然不同的操作。但是对于终端用户来讲,可能就是一个保存的操作。新数据的保存,就是新增的意思,已有数据的保存,就是修改的意思。在控制器层(Controller)可以将“改”和“增”合并到一起。

最后看看“删”

'...
'以上代码省略

'删+清理窗体
Public Sub Delete()
    If mobjModel.Delete(mobjView.GetDisplayedData.EmployeeNo) Then
        mobjView.Clear
    End If
End Sub

与上面类似,这里的“删”除了删除数据,也顺便清除了窗体中的数据。

至此,控制器层(Controller)的设计就完成了。


七、在Access窗体中使用MVC

MVC都设计好以后,我们可以开始在窗体中验证我们的设计了!

创建一个窗体,在窗体上添加6个文本框(txtEmployeeID,txtEmployeeName,txtDOB,txtGender,txtJobTitle,txtSalary)和对应的标签,3个命令按钮(cmdFind,cmdSave,cmdDelete)如下图所示:

窗体代码如下:

Option Compare Database
Option Explicit

Private mobjController As Controller

Private Sub Form_Load()
    Set mobjController = New Controller
    mobjController.Init Me
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set mobjController = Nothing
End Sub

Private Sub cmdDelete_Click()
    mobjController.Delete
End Sub

Private Sub cmdFind_Click()
    mobjController.Find Nz(Me.txtEmployeeNo, 0)
End Sub

Private Sub cmdSave_Click()
    mobjController.Save
End Sub

知乎上不能上传Access文件,读者可以到我的个人博客上下载示例文件。

http://www.jasoftiger.com/?ddownload=839www.jasoftiger.com

八、结语

MVC设计模式被专业开发人员广泛应用于其他编程语言中,在Access VBA中的应用该设计模式的的人很少,有可能是完全没有必要,也有可能是不知道如何设计。如果是后者,希望以上的示例能给你带来一点启发。若读者有任何意见和建议,欢迎留言批评和指导。

编辑于 2018-11-08

文章被以下专栏收录