Szukaj…


Abstrakcja

Poziomy abstrakcji pomagają określić, kiedy podzielić rzeczy.

Abstrakcję uzyskuje się poprzez wdrożenie funkcjonalności z coraz bardziej szczegółowym kodem. Punktem wejściowym makra powinna być mała procedura o wysokim poziomie abstrakcji, która ułatwia zrozumienie, co się dzieje:

Public Sub DoSomething()
    With New SomeForm
        Set .Model = CreateViewModel
        .Show vbModal            
        If .IsCancelled Then Exit Sub
        ProcessUserData .Model
    End With
End Sub

Procedura DoSomething ma wysoki poziom abstrakcji : możemy stwierdzić, że wyświetla formularz i tworzy jakiś model, i przekazuje ten obiekt do procedury ProcessUserData , która wie, co z tym zrobić - sposób tworzenia modelu jest zadaniem innej procedury:

Private Function CreateViewModel() As ISomeModel
    Dim result As ISomeModel
    Set result = SomeModel.Create(Now, Environ$("UserName"))
    result.AvailableItems = GetAvailableItems
    Set CreateViewModel = result
End Function

Funkcja CreateViewModel jest odpowiedzialna tylko za utworzenie instancji ISomeModel . Częścią tej odpowiedzialności jest pozyskanie szeregu dostępnych elementów - sposób ich pozyskania to szczegół implementacji, który jest streszczony za procedurą GetAvailableItems :

Private Function GetAvailableItems() As Variant
    GetAvailableItems = DataSheet.Names("AvailableItems").RefersToRange
End Function

Tutaj procedura odczytuje dostępne wartości z nazwanego zakresu w arkuszu DataSheet . Równie dobrze może to być odczytanie ich z bazy danych, lub wartości mogą być zakodowane na stałe: jest to szczegół implementacji, który nie ma znaczenia dla żadnego z wyższych poziomów abstrakcji.

Kapsułkowanie

Hermetyzacja ukrywa szczegóły implementacji przed kodem klienta.

Przykład obsługi QueryClose pokazuje enkapsulację: formularz ma kontrolę pola wyboru, ale kod klienta nie działa z nim bezpośrednio - pole wyboru jest szczegółem implementacji , kod klienta musi wiedzieć, czy ustawienie jest włączone, czy nie.

Gdy wartość pola wyboru zmienia się, program obsługi przypisuje prywatnego członka pola:

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

A gdy kod klienta chce odczytać tę wartość, nie musi się martwić o pole wyboru - zamiast tego po prostu używa właściwości SomeOtherSetting :

Public Property Get SomeOtherSetting() As Boolean
    SomeOtherSetting = this.SomeOtherSetting
End Property

Właściwość SomeOtherSetting hermetyzuje stan pola wyboru; kod klienta nie musi wiedzieć, że w grę wchodzi pole wyboru, a jedynie ustawienie z wartością logiczną. Przez enkapsulacji Boolean wartość, dodaliśmy warstwę abstrakcji wokół wyboru.


Używanie interfejsów w celu wymuszenia niezmienności

Push Powiedzmy, że o krok dalej enkapsulacji modelu postaci w dedykowanym module klasy. Ale jeśli zrobiliśmy Public Property dla UserName i Timestamp , mielibyśmy narażać Property Let akcesorów, dzięki czemu właściwości zmienne, a nie chcemy kod klienta, aby mieć możliwość zmiany tych wartości po ich zestaw.

Funkcja CreateViewModel w przykładzie Abstrakcja zwraca klasę ISomeModel : to jest nasz interfejs i wygląda ISomeModel tak:

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

Wskazówka Timestamp i UserName właściwości tylko narazić Property Get dostępowej. Teraz klasa SomeModel może implementować ten interfejs:

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

Wszystkie elementy interfejsu są Private , a wszystkie elementy interfejsu muszą zostać zaimplementowane, aby kod został skompilowany. Członkowie Public nie są częścią interfejsu i dlatego nie są narażeni na kod napisany na interfejsie ISomeModel .


Wykorzystanie metody fabrycznej do symulacji konstruktora

Za pomocą atrybutu VB_PredeclaredId możemy sprawić, że klasa SomeModel ma instancję domyślną i napisać funkcję, która działa jak element typu poziom ( Shared w VB.NET, static w języku C #), który kod klienta może wywoływać bez konieczności wcześniejszego tworzenia instancja, tak jak tutaj zrobiliśmy:

Private Function CreateViewModel() As ISomeModel
    Dim result As ISomeModel
    Set result = SomeModel.Create(Now, Environ$("UserName"))
    result.AvailableItems = GetAvailableItems
    Set CreateViewModel = result
End Function

Metoda ta fabryka przypisuje wartości właściwości, które są tylko do odczytu, gdy dostępne z ISomeModel interfejs, tutaj Timestamp i 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

A teraz możemy kod przeciwko ISomeModel interfejs, który naraża Timestamp i UserName jako właściwości tylko do odczytu, które nigdy nie mogą być przypisane (pod warunkiem, że kod jest napisany na interfejsie).

Wielopostaciowość

Polimorfizm to możliwość przedstawienia tego samego interfejsu dla różnych podstawowych implementacji.

Możliwość implementacji interfejsów pozwala całkowicie oddzielić logikę aplikacji od interfejsu użytkownika, bazy danych lub tego arkusza roboczego.

Załóżmy, że masz interfejs ISomeView , który implementuje sam formularz:

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

Kod w formularzu może wyglądać następująco:

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

Ale nic nie zabrania tworzenia kolejnego modułu klasy, który implementuje interfejs ISomeView bez bycia formularzem użytkownika - może to być klasa 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

A teraz możemy zmienić kod, który działa z UserForm i sprawić, by działał z interfejsu ISomeView , np. ISomeView nadanie mu formularza jako parametru zamiast tworzenia go:

Public Sub DoSomething(ByVal view As ISomeView)
    With view
        Set .Model = CreateViewModel
        .Show
        If .IsCancelled Then Exit Sub
        ProcessUserData .Model
    End With
End Sub

Ponieważ metoda DoSomething zależy od interfejsu (tj. Abstrakcji ), a nie konkretnej klasy (np. Konkretnego UserForm ), możemy napisać automatyczny test jednostkowy, który ProcessUserData że ProcessUserData nie zostanie wykonany, gdy view.IsCancelled jest True , poprzez utworzenie przetestuj instancję SomeViewMock , ustawiając jej właściwość IsCancelled na True i przekazując ją do DoSomething .


Testowalny kod zależy od abstrakcji

Można napisać testy jednostkowe w VBA, istnieją dodatki, które nawet integrują go z IDE. Ale kiedy kod jest ściśle połączony z arkuszem, bazą danych, formularzem lub systemem plików, wówczas test jednostkowy rozpoczyna się od rzeczywistego arkusza roboczego, bazy danych, formy lub systemu plików - a te zależności są nowym niepowodzeniem wymykającym się spod kontroli wskazuje, że kod do przetestowania powinien izolować, aby testy jednostkowe nie wymagały rzeczywistego arkusza roboczego, bazy danych, formularza lub systemu plików.

Pisząc kod dla interfejsów, w sposób umożliwiający kodowi testowemu wstrzykiwanie implementacji kodu pośredniczącego / próbnego (jak powyższy przykład SomeViewMock ), możesz pisać testy w „kontrolowanym środowisku” i symulować, co dzieje się, gdy każdy z 42 możliwych permutacje interakcji użytkownika z danymi formularza, nawet bez wyświetlania formularza i ręcznego klikania kontrolki formularza.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow