【发布时间】:2016-01-28 05:32:05
【问题描述】:
我有一个包含多个用户窗体的 excel 文件。要打开用户窗体,我有诸如
之类的代码Sub runAdjuster()
Adjuster.Show
End Sub
其中大约有 5 个。就应保留此代码的位置而言,什么被认为是最佳实践?我最初将它放在一个模块中,但决定将它移动到 ThisWorkbook 对象。寻找有关通常如何保持代码清洁的提示。
【问题讨论】:
我有一个包含多个用户窗体的 excel 文件。要打开用户窗体,我有诸如
之类的代码Sub runAdjuster()
Adjuster.Show
End Sub
其中大约有 5 个。就应保留此代码的位置而言,什么被认为是最佳实践?我最初将它放在一个模块中,但决定将它移动到 ThisWorkbook 对象。寻找有关通常如何保持代码清洁的提示。
【问题讨论】:
假设Adjuster 是表单的名称,您在此处使用默认实例,这并不理想。
这样就更好了:
Dim view As Adjuster
Set view = New Adjuster
view.Show
是的,这是更多的代码。但是您使用的是专用的对象(即view),如果该对象的状态被修改,这些更改不会影响默认实例。将该默认实例视为一个全局对象:它是全局,这不是非常 OOP。
现在,您可能会争辩,为什么不在声明的同一行“新建”对象呢?
考虑一下:
Sub DoSomething()
Dim c As New Collection
Set c = Nothing
c.Add "test"
End Sub
此代码是否访问空引用并因运行时错误 91 而爆炸?不!令人困惑?是的!因此,请避免使用 As New 快捷方式,除非您喜欢让 VBA 在背后自动执行隐式操作。
所以,您问的是最佳实践...我倾向于将 VBA UserForms 视为 winforms 的早期 pre-.NET 版本,而 WinForms 的最佳实践设计模式是Model-View-Presenter 模式(又名“MVP”)。
按照这种模式,您将让 UserForms 对 presentation 严格负责,并且您的业务逻辑要么在 presenter 对象中实现,要么在专用的演示者使用的对象。像这样的:
类模块:MyPresenter
presenter 类从 model 接收事件,并根据模型的状态执行应用程序逻辑。它知道 view 的 concept,但不必与具体实现(例如 MyUserForm)紧密耦合 - 使用适当的工具,您可以 write unit tests 以编程方式验证您的逻辑,而无需实际运行代码并显示表单并单击各处。
Option Explicit
Private Type TPresenter
View As IView
End type
Public Enum PresenterError
ERR_ModelNotSet = vbObjectError + 42
End Enum
Private WithEvents viewModel As MyModel
Private this As TPresenter
Public Sub Show()
If viewModel Is Nothing Then
Err.Raise ERR_ModelNotSet, "MyPresenter.Show", "Model is not set to an object reference."
End If
'todo: set up model properties
view.Show
If Not view.IsCancelled Then DoSomething
End Sub
Public Property Get View() As IView
Set View = this.View
End Property
Public Property Set View(ByVal value As IView)
Set this.View = value
If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property
Public Property Get Model() As MyModel
Set Model = viewModel
End Property
Public Property Set Model(ByVal value As MyModel)
Set viewModel = value
If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property
Private Sub Class_Terminate()
Set this.View.Model = Nothing
Set this.View = Nothing
Set viewModel = Nothing
End Sub
Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
'todo: execute logic that needs to run when something changes in the form
End Sub
Private Sub DoSomething()
'todo: whatever needs to happen after the form closes
End Sub
类模块:IView
这就是抽象,它代表了View的概念,它揭示了Presenter需要知道的一切任何用户窗体 - 请注意,它需要知道的一切并不多:
Option Explicit
Public Property Get Model() As Object
End Property
Public Property Set Model(ByVal value As Object)
End Property
Public Property Get IsCancelled() As Boolean
End Property
Public Sub Show()
End Sub
类模块:MyModel
模型类封装了表单需要和操作的数据。它不知道 view,也不知道 presenter:它只是一个封装数据的容器,具有启用两个视图的简单逻辑以及在修改任何属性时执行代码的演示者。
Option Explicit
Private Type TModel
MyProperty As String
SomeOtherProperty As String
'todo: wrap members here
End Type
Public Enum ModelProperties
MyProperty
SomeOtherProperty
'todo: add enum values here for each monitored property
End Enum
Public Event PropertyChanged(ByVal changedProperty As ModelProperties)
Private this As TModel
Public Property Get MyProperty() As String
MyProperty = this.MyProperty
End Property
Public Property Let MyProperty(ByVal value As String)
If this.MyProperty <> value Then
this.MyProperty = value
RaiseEvent PropertyChanged(MyProperty)
End If
End Property
Public Property Get SomeOtherProperty() As String
SomeProperty = this.SomeOtherProperty
End Property
Public Property Let SomeOtherProperty(ByVal value As String)
If this.SomeOtherProperty <> value Then
this.SomeOtherProperty = value
RaiseEvent PropertyChanged(SomeOtherProperty)
End If
End Property
'todo: expose other model properties
用户表单:我的用户表单
UserForm 严格负责视觉呈现;它的所有事件处理程序都是更改模型中属性的值 - 然后模型告诉演示者“嘿,我已经被修改了!”,演示者相应地采取行动。表单还监听模型上修改的属性,因此当演示者更改模型时,视图可以执行代码并相应地更新自身。这是一个简单的表单示例,将MyProperty 模型属性“绑定”到一些TextBox1 的文本;我为SomeOtherProperty添加了一个监听器只是为了说明视图也可以在模型更改时间接更新。
显然,视图不会对与演示者相同的属性更改做出反应,否则您将进入无休止的回调,最终会炸毁堆栈……但您明白了。
请注意,表单实现了IView 接口,因此演示者可以在不了解其内部工作原理的情况下与其交谈。接口实现只是引用具体成员,但具体成员甚至不需要实际存在,因为它们甚至不会被使用!
Option Explicit
Implements IView
Private Type TView
IsCancelled As Boolean
End Type
Private WithEvents viewModel As MyModel
Private this As TView
Private Property Get IView_Model() As Object
Set IView_Model = Model
End Property
Private Property Set IView_Model(ByVal value As Object)
Set Model = value
End Property
Private Property Get IView_IsCancelled() As Boolean
IView_IsCancelled = IsCancelled
End Property
Private Sub IView_Show()
Show vbModal
End Sub
Public Property Get Model() As MyModel
Set Model = viewModel
End Property
Public Property Set Model(ByVal value As MyModel)
Set viewModel = value
End Property
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Private Sub CancelButton_Click()
this.IsCancelled = True
Me.Hide
End Sub
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'"x-ing out" of the form is like clicking the Cancel button
If CloseMode = VbQueryClose.vbFormControlMenu Then
this.IsCancelled = True
End If
End Sub
Private Sub UserForm_Activate()
If viewModel Is Nothing Then
MsgBox "Model property must be assigned before the view can be displayed.", vbCritical, "Error"
Unload Me
Else
Me.TextBox1.Text = viewModel.MyProperty
Me.TextBox1.SetFocus
End If
End Sub
Private Sub TextBox1_Change()
'UI elements update the model properties
viewModel.MyProperty = Me.TextBox1.Text
End Sub
Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
If changedProperty = SomeOtherProperty Then
Frame1.Caption = SomeOtherProperty
End If
End Sub
模块:宏
假设您的电子表格有一个形状,并且您希望在单击它时运行该逻辑。您需要将宏附加到该形状 - 我喜欢将所有宏重新组合到一个名为“宏”的标准模块 (.bas) 中,该模块只包含看起来像这样的公共过程:
Option Explicit
Public Sub DoSomething()
Dim presenter As MyPresenter
Set presenter = New MyPresenter
Dim theModel As MyModel
Set theModel = New MyModel
Dim theView As IView
Set theView = New MyUserForm
Set presenter.Model = theModel
Set presenter.View = theView
presenter.Show
End Sub
现在,如果您想在不显示表单的情况下以编程方式测试您的演示者逻辑,您需要做的就是实现一个“假”视图,并编写一个测试方法来满足您的需求:
类:MyFakeView
Option Explicit
Implements IView
Private Type TFakeView
IsCancelled As Boolean
End Type
Private this As TFakeView
Private Property Get IView_Model() As Object
Set IView_Model = Model
End Property
Private Property Set IView_Model(ByVal value As Object)
Set Model = value
End Property
Private Property Get IView_IsCancelled() As Boolean
IView_IsCancelled = IsCancelled
End Property
Private Sub IView_Show()
IsCancelled = False
End Sub
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Let IsCancelled(ByVal value As Boolean)
this.IsCancelled = value
End Property
模块:TestModule1
可能还有其他工具,但由于我实际上编写了这个工具,而且我喜欢它的工作方式,而无需大量样板设置代码或包含可执行指令的 cmets,我将热烈推荐使用 Rubberduck 单元测试。下面是一个 [非常简单的] 测试模块的样子:
'@TestModule
Option Explicit
Option Private Module
Private Assert As New Rubberduck.AssertClass
'@TestMethod
Public Sub Model_SomePropertyInitializesEmpty()
On Error GoTo TestFail
'Arrange
Dim presenter As MyPresenter
Set presenter = New MyPresenter
Dim theModel As MyModel
Set theModel = New MyModel
Set presenter.Model = theModel
Set presenter.View = New MyFakeView
'Act
presenter.Show
'Assert
Assert.IsTrue theModel.SomeProperty = vbNullString
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Rubberduck 单元测试允许您使用此解耦代码来测试您想要测试的有关应用程序逻辑的所有内容 - 只要您保持该应用程序逻辑 解耦 并且您编写 可测试的代码,您将拥有记录 VBA 应用程序应该如何运行的单元测试,记录规范是什么的测试 - 就像您在 C# 或 Java 或任何其他 OOP 语言中拥有它们一样编写单元测试。
重点是,VBA 也可以。
矫枉过正?要看。规格一直在变化,代码也随之变化。在电子表格的代码隐藏中实现所有应用程序逻辑非常烦人,因为the Project Explorer doesn't drill down to module members,所以查找在哪里实现的内容很容易让人烦。
如果在表单的代码隐藏中实现逻辑,然后您有 Button_Click 处理程序进行数据库调用或电子表格操作,情况会更糟。
在具有尽可能少职责的对象中实现的代码,使代码可重用,并且更易于维护。
您的问题并不完全准确地说明“具有多个用户窗体的 Excel 文件”的确切含义,但如果需要,您可以有一个接收 4-5 个“子”演示者的“主”演示者类,每个人都负责与每个“子”表单相关的特定逻辑。
也就是说,如果您有工作代码(完全按预期工作),您想重构并提高效率,或者更易于阅读/维护,您可以将其发布到 Code Review Stack Exchange,这就是该站点的内容为。
【讨论】:
这取决于启动这些潜艇的原因。如果它们附加到按钮或形状(这是我在启动用户窗体时倾向于做的),那么将它们放在包含形状的工作表的模块中是有意义的。如果几张纸上的按钮/形状引用它 - 将它们放在通用代码模块中。我不知道这里是否真的有“最佳实践”。最重要的是保持一致性,这样你就不必去寻找东西了。
【讨论】: