The ability to implement interfaces allows completely decoupling the application logic from the UI, or from the database, or from this or that worksheet.
Say you have an ISomeView
interface that the form itself implements:
Option Explicit
Public Property Get IsCancelled() As Boolean
End Property
Public Property Get Model() As ISomeModel
End Property
Public Property Set Model(ByVal value As ISomeModel)
End Property
Public Sub Show()
End Sub
The form's code-behind could look like this:
Option Explicit
Implements ISomeView
Private Type TView
IsCancelled As Boolean
Model As ISomeModel
End Type
Private this As TView
Private Property Get ISomeView_IsCancelled() As Boolean
ISomeView_IsCancelled = this.IsCancelled
End Property
Private Property Get ISomeView_Model() As ISomeModel
Set ISomeView_Model = this.Model
End Property
Private Property Set ISomeView_Model(ByVal value As ISomeModel)
Set this.Model = value
End Property
Private Sub ISomeView_Show()
Me.Show vbModal
End Sub
Private Sub SomeOtherSettingInput_Change()
this.Model.SomeOtherSetting = CBool(SomeOtherSettingInput.Value)
End Sub
'...other event handlers...
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub CancelButton_Click()
this.IsCancelled = True
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
this.IsCancelled = True
Me.Hide
End If
End Sub
But then, nothing forbids creating another class module that implements the ISomeView
interface without being a user form - this could be a SomeViewMock
class:
Option Explicit
Implements ISomeView
Private Type TView
IsCancelled As Boolean
Model As ISomeModel
End Type
Private this As TView
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Let IsCancelled(ByVal value As Boolean)
this.IsCancelled = value
End Property
Private Property Get ISomeView_IsCancelled() As Boolean
ISomeView_IsCancelled = this.IsCancelled
End Property
Private Property Get ISomeView_Model() As ISomeModel
Set ISomeView_Model = this.Model
End Property
Private Property Set ISomeView_Model(ByVal value As ISomeModel)
Set this.Model = value
End Property
Private Sub ISomeView_Show()
'do nothing
End Sub
And now we can change the code that works with a UserForm
and make it work off the ISomeView
interface, e.g. by giving it the form as a parameter instead of instantiating it:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Because the DoSomething
method depends on an interface (i.e. an abstraction) and not a concrete class (e.g. a specific UserForm
), we can write an automated unit test that ensures that ProcessUserData
isn't executed when view.IsCancelled
is True
, by making our test create a SomeViewMock
instance, setting its IsCancelled
property to True
, and passing it to DoSomething
.
Writing unit tests in VBA can be done, there are add-ins out there that even integrate it into the IDE. But when code is tightly coupled with a worksheet, a database, a form, or the file system, then the unit test starts requiring an actual worksheet, database, form, or file system - and these dependencies are new out-of-control failure points that testable code should isolate, so that unit tests don't require an actual worksheet, database, form, or file system.
By writing code against interfaces, in a way that allows test code to inject stub/mock implementations (like the above SomeViewMock
example), you can write tests in a "controlled environment", and simulate what happens when every single one of the 42 possible permutations of user interactions on the form's data, without even once displaying a form and manually clicking on a form control.