VBA
Объектно-ориентированный VBA
Поиск…
абстракция
Уровни абстракции помогают определить, когда разделить вещи.
Абстракция достигается за счет внедрения функциональности со все более подробным кодом. Точкой входа макроса должна быть небольшая процедура с высоким уровнем абстракции, которая позволяет легко понять, что происходит:
Public Sub DoSomething()
With New SomeForm
Set .Model = CreateViewModel
.Show vbModal
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Процедура DoSomething
имеет высокий уровень абстракции : мы можем сказать, что она отображает форму и создает некоторую модель и передает этот объект некоторой процедуре ProcessUserData
, которая знает, что с ней делать - то, как создается модель, - это работа другой процедуры:
Private Function CreateViewModel() As ISomeModel
Dim result As ISomeModel
Set result = SomeModel.Create(Now, Environ$("UserName"))
result.AvailableItems = GetAvailableItems
Set CreateViewModel = result
End Function
Функция CreateViewModel
отвечает только за создание экземпляра ISomeModel
. Часть этой ответственности состоит в том, чтобы получить массив доступных элементов - как эти предметы были приобретены - это деталь реализации, которая абстрагируется после процедуры GetAvailableItems
:
Private Function GetAvailableItems() As Variant
GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function
Здесь процедура считывает доступные значения из именованного диапазона на листе DataSheet
. Это также можно было бы прочитать из базы данных, или значения могут быть жестко закодированы: это деталь реализации, которая не вызывает беспокойства ни для одного из более высоких уровней абстракции.
Инкапсуляция
Инкапсуляция скрывает детали реализации из кода клиента.
Пример обработки QueryClose демонстрирует инкапсуляцию: форма имеет элемент управления флажком , но его клиентский код не работает с ним напрямую - флажок - это деталь реализации , что должен знать код клиента, является ли параметр включен или нет.
Когда значение флажка изменяется, обработчик назначает частный член поля:
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
И когда клиентский код хочет прочитать это значение, ему не нужно беспокоиться о флажке - вместо этого он просто использует свойство SomeOtherSetting
:
Public Property Get SomeOtherSetting() As Boolean SomeOtherSetting = this.SomeOtherSetting End Property
Свойство SomeOtherSetting
инкапсулирует состояние флажка; клиентский код не обязательно должен знать, что есть флажок, только что существует параметр с логическим значением. Инкапсулируя Boolean
значение, мы добавили слой абстракции вокруг флажка.
Использование интерфейсов для обеспечения неизменности
Давайте толкать , что шаг инкапсулируя модель формы в специальном модуле класса. Но если мы создали Public Property
для UserName
и Timestamp
, нам нужно было бы открыть Property Let
accessors, изменив свойства, и мы не хотим, чтобы клиентский код имел возможность изменять эти значения после их установки.
Функция CreateViewModel
в примере абстракции возвращает класс ISomeModel
: это наш интерфейс , и он выглядит примерно так:
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
Значения Timestamp
и UserName
отображают только атрибут Property Get
. Теперь класс SomeModel
может реализовать этот интерфейс:
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
Все члены интерфейса являются Private
, и все члены интерфейса должны быть реализованы для компиляции кода. Public
члены не являются частью интерфейса и поэтому не подвергаются действию кода, написанного против интерфейса ISomeModel
.
Использование фабричного метода для моделирования конструктора
Используя атрибут VB_PredeclaredId , мы можем сделать класс SomeModel
экземпляром по умолчанию и написать функцию, которая работает как член уровня ( Shared
in VB.NET, static
in C #), который клиентский код может вызывать без необходимости сначала создавать экземпляр, как мы здесь делали:
Private Function CreateViewModel() As ISomeModel Dim result As ISomeModel Set result = SomeModel.Create(Now, Environ$("UserName")) result.AvailableItems = GetAvailableItems Set CreateViewModel = result End Function
Этот заводский метод присваивает значения свойств, доступные только для чтения, при доступе от интерфейса ISomeModel
, здесь Timestamp
и 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
И теперь мы можем ISomeModel
интерфейс ISomeModel
, который предоставляет Timestamp
и UserName
как свойства только для чтения, которые никогда не могут быть переназначены (пока код написан против интерфейса).
Полиморфизм
Полиморфизм - это способность представить один и тот же интерфейс для разных базовых реализаций.
Возможность реализации интерфейсов позволяет полностью развязать логику приложения из пользовательского интерфейса или из базы данных или из того или иного рабочего листа.
Скажем, у вас есть интерфейс ISomeView
который реализует сама форма:
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
Кодировка кода формы может выглядеть так:
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
Но тогда ничто не запрещает создание другого модуля класса, который реализует интерфейс ISomeView
без пользовательской формы - это может быть класс SomeViewMock
:
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
И теперь мы можем изменить код, который работает с UserForm
и заставить его работать с интерфейсом ISomeView
, например, предоставив ему форму как параметр, а не создавая ее:
Public Sub DoSomething(ByVal view As ISomeView)
With view
Set .Model = CreateViewModel
.Show
If .IsCancelled Then Exit Sub
ProcessUserData .Model
End With
End Sub
Поскольку метод DoSomething
зависит от интерфейса (то есть абстракции ), а не от конкретного класса (например, определенного UserForm
), мы можем написать автоматизированный модульный тест, который гарантирует, что ProcessUserData
не будет выполняться, когда view.IsCancelled
is True
, сделав нашу test создайте экземпляр SomeViewMock
, установив свойство IsCancelled
в значение True
и передав его в DoSomething
.
Тестовый код зависит от абстракций
Пробные тесты в VBA можно сделать, есть надстройки, которые даже интегрируют его в среду IDE. Но когда код тесно связан с рабочим листом, базой данных, формой или файловой системой, тогда в модульном тесте начинает требоваться фактический рабочий лист, база данных, форма или файловая система - и эти зависимости являются новым отказом вне контроля указывает, что тестируемый код должен изолироваться, так что модульные тесты не требуют фактического рабочего листа, базы данных, формы или файловой системы.
Путем написания кода с интерфейсов таким образом, чтобы тестовый код мог вводить реализацию заглушки / макета (например, вышеприведенный пример SomeViewMock
), вы можете писать тесты в «контролируемой среде» и имитировать, что происходит, когда каждый из 42 возможных перестановки пользовательских взаимодействий на данные формы, даже не отображая форму и вручную щелкая на элементе управления формой.