The Handling QueryClose example demonstrates encapsulation: the form has a checkbox control, but its client code doesn't work with it directly - the checkbox is an implementation detail, what the client code needs to know is whether the setting is enabled or not.
When the checkbox value changes, the handler assigns a private field member:
Private Type TView IsCancelled As Boolean SomeOtherSetting As Boolean 'other properties skipped for brievety End Type Private this As TView '... Private Sub SomeOtherSettingInput_Change() this.SomeOtherSetting = CBool(SomeOtherSettingInput.Value) End Sub
And when the client code wants to read that value, it doesn't need to worry about a checkbox - instead it simply uses the SomeOtherSetting
property:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
The SomeOtherSetting
property encapsulates the checkbox' state; client code doesn't need to know that there's a checkbox involved, only that there's a setting with a Boolean value. By encapsulating the Boolean
value, we've added an abstraction layer around the checkbox.
Let's push that a step further by encapsulating the form's model in a dedicated class module. But if we made a Public Property
for the UserName
and Timestamp
, we would have to expose Property Let
accessors, making the properties mutable, and we don't want the client code to have the ability to change these values after they're set.
The CreateViewModel
function in the Abstraction example returns an ISomeModel
class: that's our interface, and it looks something like this:
Option Explicit
Public Property Get Timestamp() As Date
End Property
Public Property Get UserName() As String
End Property
Public Property Get AvailableItems() As Variant
End Property
Public Property Let AvailableItems(ByRef value As Variant)
End Property
Public Property Get SomeSetting() As String
End Property
Public Property Let SomeSetting(ByVal value As String)
End Property
Public Property Get SomeOtherSetting() As Boolean
End Property
Public Property Let SomeOtherSetting(ByVal value As Boolean)
End Property
Notice Timestamp
and UserName
properties only expose a Property Get
accessor. Now the SomeModel
class can implement that interface:
Option Explicit
Implements ISomeModel
Private Type TModel
Timestamp As Date
UserName As String
SomeSetting As String
SomeOtherSetting As Boolean
AvailableItems As Variant
End Type
Private this As TModel
Private Property Get ISomeModel_Timestamp() As Date
ISomeModel_Timestamp = this.Timestamp
End Property
Private Property Get ISomeModel_UserName() As String
ISomeModel_UserName = this.UserName
End Property
Private Property Get ISomeModel_AvailableItems() As Variant
ISomeModel_AvailableItems = this.AvailableItems
End Property
Private Property Let ISomeModel_AvailableItems(ByRef value As Variant)
this.AvailableItems = value
End Property
Private Property Get ISomeModel_SomeSetting() As String
ISomeModel_SomeSetting = this.SomeSetting
End Property
Private Property Let ISomeModel_SomeSetting(ByVal value As String)
this.SomeSetting = value
End Property
Private Property Get ISomeModel_SomeOtherSetting() As Boolean
ISomeModel_SomeOtherSetting = this.SomeOtherSetting
End Property
Private Property Let ISomeModel_SomeOtherSetting(ByVal value As Boolean)
this.SomeOtherSetting = value
End Property
Public Property Get Timestamp() As Date
Timestamp = this.Timestamp
End Property
Public Property Let Timestamp(ByVal value As Date)
this.Timestamp = value
End Property
Public Property Get UserName() As String
UserName = this.UserName
End Property
Public Property Let UserName(ByVal value As String)
this.UserName = value
End Property
Public Property Get AvailableItems() As Variant
AvailableItems = this.AvailableItems
End Property
Public Property Let AvailableItems(ByRef value As Variant)
this.AvailableItems = value
End Property
Public Property Get SomeSetting() As String
SomeSetting = this.SomeSetting
End Property
Public Property Let SomeSetting(ByVal value As String)
this.SomeSetting = value
End Property
Public Property Get SomeOtherSetting() As Boolean
SomeOtherSetting = this.SomeOtherSetting
End Property
Public Property Let SomeOtherSetting(ByVal value As Boolean)
this.SomeOtherSetting = value
End Property
The interface members are all Private
, and all members of the interface must be implemented for the code to compile. The Public
members are not part of the interface, and are therefore not exposed to code written against the ISomeModel
interface.
Using a VB_PredeclaredId attribute, we can make the SomeModel
class have a default instance, and write a function that works like a type-level (Shared
in VB.NET, static
in C#) member that the client code can call without needing to first create an instance, like we did here:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
This factory method assigns the property values that are read-only when accessed from the ISomeModel
interface, here Timestamp
and UserName
:
Public Function Create(ByVal pTimeStamp As Date, ByVal pUserName As String) As ISomeModel
With New SomeModel
.Timestamp = pTimeStamp
.UserName = pUserName
Set Create = .Self
End With
End Function
Public Property Get Self() As ISomeModel
Set Self = Me
End Property
And now we can code against the ISomeModel
interface, which exposes Timestamp
and UserName
as read-only properties that can never be reassigned (as long as the code is written against the interface).