Наслаждавам се на добър пъзел толкова, колкото и на следващия човек. Има нещо удовлетворяващо в това да започнете с купчина привидно произволни парчета и да гледате как картината бавно оживява, докато възстановявате реда в хаоса.
Въпреки това спрях да правя пъзели. Вероятно са минали, о, вече 13 години. Нека направя сметката. Имам четири деца; най-възрастната е на 15. Да, на две години е точно времето, когато е била достатъчно възрастна, за да се скита до недовършен пъзел, да се укрие с едно от парчетата и да го захрани с кучето, топлинния регистър или тоалетната.
И колкото и удовлетворяващо да е да поставите последното парче в пъзел, толкова съкрушително е да поставите предпоследното парче в пъзела и да осъзнаете, че последното парче липсва.
Така се чувствах за моя код за обработка на грешки.
Инспектор на променливи на vbWatchdog
Ако използвате vbWatchdog за вашата обработка на грешки (трябва), тогава трябва да сте запознати с една от най-мощните му функции:инспектора на променливи. Този обект осигурява достъп до всяка променлива в обхват на всяко ниво на стека на повикванията. Това ниво на детайлност е цифрово злато, когато дойде време за отстраняване на грешки.
През годините разработих усъвършенстван модул за обработка на грешки, който записва цялата тази информация. Докато фино настройвах обработката на грешки, един недостатък започна да се откроява. Макар че можех да извличам стойностите на повечето от моите променливи, всичко, което можех да извлека от обектните променливи, беше или „Нищо“ или „{Object}“.
Това не е почукване на vbWatchdog. Обект може да бъде всичко. Каква друга стойност би могла да покаже? И все пак това липсващо парче от пъзела ме изгриза. Усещах как Вселената ми се смее, когато отстранявах някакъв бъг и ключът към разрешаването му беше скрит зад тази една влудяващо кофти дума „{Object}“.
Само ако имах някакъв начин да знам едно или две от идентифициращите свойства на този обект, бих могъл да разбера какво точно се случва.
Първи опит
Първият ми опит за разрешаване на проблема е инструментът за всеки разочарован програмист:груба сила. В моя глобален манипулатор на грешки добавих Select...Case изявление около .TypeDesc
.
Например, имам клас за създаване на SQL, който наричам clsSQL . Едно от свойствата в този клас е .LastSQL
. Това свойство съдържа последния SQL израз, който класът е изградил или изпълнил. Може да бъде оператор SELECT или INSERT/UPDATE/DELETE/и т.н. (Взаимствах идеята от обекта DAL на web2py. )
Ето част от моя глобален манипулатор на грешки:
Select Case .TypeDesc
Case "clsSQL"
If Not .Value Is Nothing Then
ThisVar = .Name & ".LastSQL = " & .Value.LastSQL
End If
С течение на времето започнах да добавям допълнителни персонализирани типове обекти към този списък. С всеки персонализиран тип ще трябва да извличам различно персонализирано свойство.
Имах последното си парче от пъзела. Проблемът е, че го намерих да плува в купата с вода на кучето, цялата сдъвкана от едната страна. Предполагам, че може да се каже, че моят пъзел е завършен, но това беше Пирова победа.
Лечение, което прави повече вреда, отколкото полза
Бързо разбрах, че това решение няма да има мащаб. Имаше много проблеми. Първо, моят глобален код за обработка на грешки щеше да се раздуе. Поддържам кода си за обработка на грешки в един стандартен модул в моята библиотека с кодове. Това означава, че всеки път, когато искам да добавя поддръжка за модул за клас, този код ще бъде добавен към всеки един от моите проекти. Това беше вярно, дори ако модулът клас беше използван само в един проект.
Следващият проблем е, че въвеждах външни зависимости в моя код за обработка на грешки. Ами ако смених моя clsSQL клас някой ден и преименувайте или премахнете .LastSQL
метод? Какви са шансовете да разбера, че такава зависимост е съществувала, докато работех в моя clsSQL клас? Този подход бързо би рухнал под собствената си тежест, освен ако не измисля алтернатива.
Търсим Python за решение
Разбрах, че това, което наистина исках, е някакъв начин да определя канонично представяне на обект отвътре този обект . Исках да мога да реализирам това представяне толкова просто или сложно, колкото е необходимо. Исках начин да гарантирам, че няма да се взриви по време на изпълнение. Исках да е напълно незадължителен за всеки модул на класа.
Това изглежда като дълъг списък с желания, но успях да удовлетворя всеки елемент от него с решението, което намерих.
За пореден път взаимствах идея от Python. Всички обекти на Python имат специално свойство, известно като ._repr
. Това свойство е низовото представяне на обекта. По подразбиране той ще върне името на типа и адреса на паметта на екземпляра на обекта. Програмистите на Python обаче могат да дефинират .__repr__
метод за отмяна на поведението по подразбиране. Това е сочно нещо, което исках за моите VBA класове.
Най-накрая намерих идеалното си решение. За съжаление го намерих на друг език, където решението всъщност е характеристика на самия език . Как това трябва да ми помогне във VBA? Оказва се, че идеята е важна част; Просто трябваше да бъда малко креативен с внедряването.
Интерфейси за спасяване
За да пренеса тази концепция на Python във VBA, се обърнах към една рядко използвана функция на езика:интерфейси и оператор TypeOf. Ето как работи.
Създадох модул за клас, който нарекох iRepresentation . Интерфейсите в повечето езици са наречени с водещо "i" по конвенция. Разбира се, можете да назовете модулите си както искате. Ето пълния код за моето iRepresentation клас.
iRepresentation.cls
`--== iRepresentation ==-- class module
Option Compare Database
Option Explicit
Public Property Get Repr() As String
End Property
Трябва да отбележа, че няма нищо особено в модул за клас, който служи като интерфейс във VBA. С това имам предвид, че няма ключова дума на ниво модул или скрит атрибут, който трябва да зададем. Можем дори да инстанцираме нов обект, използвайки този тип, макар че няма да има много смисъл (едно изключение е тестването, но това е тема за различен ден). Например, следният би бил валиден код:
Dim Representation As iRepresentation
Set Representation = New iRepresentation
Debug.Print Representation.Repr
Сега да кажем, че имам персонализиран модул за клас с име oJigsawPuzzle . Модулът на класа има няколко свойства и методи, но ние искаме такъв, който да ни помогне да идентифицираме с кой обект JigsawPuzzle имаме работа, когато се появи грешка. Един очевиден кандидат за такава работа е SKU, който уникално идентифицира пъзела като продукт на рафтовете на магазините. Разбира се, в зависимост от нашата ситуация, може да искаме да включим и друга информация в нашето представяне.
oJigsawPuzzle.cls
'--== oJigsawPuzzle ==-- class module
Option Compare Database
Option Explicit
Implements iRepresentation ' <-- We need this line...
Private mSKU As String
Private mPieceCount As Long
Private mDesigner As String
Private mTitle As String
Private mHeightInInches As Double
Private mWidthInInches As Double
'... and these three lines
Private Property Get iRepresentation_Repr() As String
iRepresentation_Repr = mSKU
End Property
Ето къде идва магията. Когато си проправяме път през обекта на инспектора на променливи, сега можем да тестваме всяка променлива на обекта, за да видим дали прилага този интерфейс. И ако е така, можем да вземем тази стойност и да я регистрираме заедно с останалите стойности на нашите променливи.
Откъс от манипулатора на грешки
' --== Global Error Handler excerpt ==--
'Include Repr property value for classes that
' implement the iRepresentation interface
If TypeOf .Value Is iRepresentation Then
Dim ObjWithRepr As iRepresentation
Set ObjWithRepr = .Value
ThisVar = .Name & ".Repr = " & ObjWithRepr.Repr
End If
И с това моят пъзел за обработка на грешки вече е завършен. Всички парчета се отчитат. Няма следи от ухапване. Нито едно от парчетата не се отлепва. Няма празни места.
Най-накрая възстанових реда в хаоса.