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.