Поиск…


абстракция

Уровни абстракции помогают определить, когда разделить вещи.

Абстракция достигается за счет внедрения функциональности со все более подробным кодом. Точкой входа макроса должна быть небольшая процедура с высоким уровнем абстракции, которая позволяет легко понять, что происходит:

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 возможных перестановки пользовательских взаимодействий на данные формы, даже не отображая форму и вручную щелкая на элементе управления формой.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow