Писане на четим код за VBA – Опитайте* шаблон
Напоследък откривам, че използвам Try
модел все повече и повече. Наистина ми харесва този модел, защото прави много по-четлив код. Това е особено важно при програмиране на зрял език за програмиране като VBA, където обработката на грешки е преплетена с контролния поток. Като цяло намирам, че всички процедури, които разчитат на обработка на грешки като контролен поток, са по-трудни за следване.
Сценарий
Да започнем с пример. DAO обектният модел е перфектен кандидат поради начина, по който работи. Вижте, всички DAO обекти имат Properties
колекция, която съдържа Property
обекти. Всеки обаче може да добави персонализирано свойство. Всъщност Access ще добави няколко свойства към различни DAO обекти. Следователно може да имаме свойство, което може да не съществува и трябва да се справи както със случая на промяна на стойността на съществуващо свойство, така и със случая на добавяне на ново свойство.
Нека използваме Subdatasheet
собственост като пример. По подразбиране всички таблици, създадени чрез потребителския интерфейс на Access, ще имат свойството, зададено на Auto
, но може да не искаме това. Но ако имаме таблици, които са създадени в код или по някакъв друг начин, те може да нямат свойството. Така че можем да започнем с първоначална версия на кода, за да актуализираме свойството на всички таблици и да обработваме и двата случая.
Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="GoToSub" ErtaorNameler Задайте db =CurrentDb за всеки tdf в db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Тогава Ако Len(tdf.Connect) =0 И (Не tdf.Name като "~*") Тогава 'Not attached, or temp . Задайте prp =tdf.Properties(SubDatasheetPropertyName) Ако prp.Value <> NewValue Тогава prp.Value =NewValue Край Ако Край Ако Край IfContinue:NextExitProc:Exit SubErrHandler:Ако Err.Number =3270 Then Set prp.SreDaProsheet(SreDataProsheet, Set prp.SreDaProsheet, dbText, NewValue) tdf.Properties.Append prp Възобновяване Продължаване Край Ако MsgBox Err.Number &":" &Err.Description Възобновяване ExitProc Край Sub
Кодът вероятно ще работи. Въпреки това, за да го разберем, вероятно трябва да диаграмираме някаква блок-схема. Редът Set prp = tdf.Properties(SubDatasheetPropertyName)
може потенциално да изведе грешка 3270. В този случай контролата прескача към секцията за обработка на грешки. След това създаваме свойство и след това продължаваме в друга точка от цикъла, използвайки етикета Continue
. Има някои въпроси...
- Ами ако 3270 е повдигнато на друга линия?
- Да предположим, че редът
Set prp =...
не хвърля грешка 3270, но всъщност някаква друга грешка? - Ами ако докато сме вътре в манипулатора на грешки, се случи друга грешка при изпълнение на
Append
илиCreateProperty
? - Трябва ли тази функция дори да показва
Msgbox
? Помислете за функции, които би трябвало да работят върху нещо от името на формуляри или бутони. Ако функциите покажат поле за съобщение, след което излезте нормално, извикващият код няма представа, че нещо се е объркало и може да продължи да прави неща, които не трябва да прави. - Можете ли да погледнете кода и веднага да разберете какво прави? не мога. Трябва да примижа, след това да помисля какво трябва да се случи в случай на грешка и мислено да скицирам пътя. Това не е лесно за четене.
Добавете HasProperty
процедура
Можем ли да се справим по-добре? Да! Някои програмисти вече разпознават проблема с използването на обработка на грешки, както илюстрирах, и мъдро абстрахираха това в неговата собствена функция. Ето по-добра версия:
Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName като db Currentlist ="NameDbda" За всеки tdf в db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Тогава Ако Len(tdf.Connect) =0 И (Не tdf.Name като "~*") Тогава „Not attached, or temp. Ако не HasProperty(tdf, SubDatasheetPropertyName) Тогава задайте prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Else Ако tdf.Properties(SubDatasheet)Thenf. Край, ако Край, ако Край, ако NextEnd Подпублична функция има свойство(TargetObject като обект, PropertyName като низ) Като булев Dim игнориран като вариант при грешка Възобновяване Следващ игнориран =TargetObject.Properties(PropertyName) HasProperty =(Err.Enumber Function =0) предварително>Вместо да смесваме потока на изпълнение с обработката на грешки, сега имаме функция
HasFunction
който спретнато абстрахира предразположената към грешки проверка за свойство, което може да не съществува. В резултат на това не се нуждаем от сложен поток за обработка на грешки/изпълнение, който видяхме в първия пример. Това е голямо подобрение и прави кода донякъде четим. Но…
- Имаме един клон, който използва променливата
prp
и имаме друг клон, който използваtdf.Properties(SubDatasheetPropertyName)
което всъщност се отнася за един и същ имот. Защо се повтаряме с два различни начина за позоваване на едно и също свойство? - Доста се справяме с имота.
HasProperty
трябва да обработва свойството, за да разбере дали съществува, след което просто връщаBoolean
резултат, оставяйки на кода за повикване да опита отново да получи същото свойство отново, за да промени стойността. - По подобен начин обработваме
NewValue
повече от необходимото. Ние или го предаваме вCreateProperty
или задайтеValue
собственост на имота. HasProperty
функцията имплицитно предполага, че обектът имаProperties
член и го нарича късно обвързан, което означава, че е грешка по време на изпълнение, ако му бъде предоставен грешен вид обект.
Използвайте TryGetProperty
вместо това
Можем ли да се справим по-добре? Да! Това е мястото, където трябва да разгледаме модела Try. Ако някога сте програмирали с .NET, вероятно сте виждали методи като TryParse
където вместо да издигаме грешка при неуспех, можем да зададем условие да направим нещо за успех и нещо друго за провал. Но по-важното е, че имаме резултат за успех. И така, как бихме подобрили HasProperty
функция? Първо, трябва да върнем Property
обект. Нека опитаме този код:
Публична функция TryGetProperty( _ ByVal SourceProperties като DAO.Properties, _ ByVal PropertyName като низ, _ ByRef OutProperty като DAO.Property _) Като булева при грешка Възобновяване Следваща Set OutProperty =SourceProperties(PropertyName) След това Set OutProperty. =Нищо не свършва, ако при грешка Отидете до 0 TryGetProperty =(Not OutProperty е нищо) Крайна функция
С няколко промени постигнахме няколко големи победи:
- Достъпът до
Properties
вече не е закъснял. Не е нужно да се надяваме, че даден обект има свойство с имеProperties
и е наDAO.Properties
. Това може да се провери по време на компилиране. - Вместо само
Boolean
резултат, можем също да получим извлеченотоProperty
обект, но само на успеха. Ако не успеем,OutProperty
параметърът ще бъдеNothing
. Все още ще използвамеBoolean
резултат, който ще ви помогне да настроите потока нагоре, както ще видите скоро. - Като именуваме новата ни функция с
Try
префикс, ние указваме, че това гарантирано няма да доведе до грешка при нормални работни условия. Очевидно не можем да предотвратим грешки при липса на памет или нещо подобно, но в този момент имаме много по-големи проблеми. Но при нормално работно състояние сме избегнали заплитането на нашата обработка на грешки с потока на изпълнение. Кодът вече може да се чете отгоре надолу без прескачане напред или назад.
Имайте предвид, че по конвенция поставям префикс на свойството „out“ с Out
. Това помага да стане ясно, че трябва да предадем променливата на неинициализираната функция. Също така очакваме функцията да инициализира параметъра. Това ще стане ясно, когато погледнем кода за повикване. И така, нека настроим кода за повикване.
Ревизиран код за повикване с помощта на TryGetProperty
Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName като db Currentlist ="NameDbda" За всеки tdf в db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Тогава Ако Len(tdf.Connect) =0 И (Не tdf.Name като "~*") Тогава „Not attached, or temp. Ако TryGetProperty(tdf, SubDatasheetPropertyName, prp) Тогава If prp.Value <> NewValue Тогава prp.Value =NewValue Край Ако иначе Задайте prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText Endperties, NewValue End If.Af Ако NextEnd Sub
Кодът вече е малко по-четим с първия модел Try. Успяхме да намалим обработката на prp
. Имайте предвид, че предаваме prp
променлива в prp
ще бъде инициализиран със свойството, което искаме да манипулираме. В противен случай prp
остава Nothing
. След това можем да използваме CreateProperty
за да инициализирате prp
променлива.
Ние също така обърнахме отрицанието, така че кодът да стане по-лесен за четене. Въпреки това, ние наистина не сме намалили обработката на NewValue
параметър. Все още имаме друг вложен блок, за да проверим стойността. Можем ли да се справим по-добре? Да! Нека добавим още една функция:
Добавяне на TrySetPropertyValue
процедура
Публична функция TrySetPropertyValue( _ ByVal SourceProperty като DAO.Property, _ ByVal NewValue като Variant_) Като булева If SourceProperty.Value =PropertyValue Then TrySetPropertyValue =True Else On SourceProperty New ErrS. SourceProperty.Value =NewValue) Край на функцията IfEnd
Тъй като гарантираме, че тази функция няма да изведе грешка при промяна на стойността, ние я наричаме TrySetPropertyValue
. По-важното е, че тази функция помага да се капсулират всички кървави подробности около промяната на стойността на имота. Имаме начин да гарантираме, че стойността е стойността, която очаквахме да бъде. Нека да разгледаме как кодът за повикване ще бъде променен с тази функция.
Актуализиран код за повикване с помощта на TryGetProperty
и TrySetPropertyValue
Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName като db Currentlist ="NameDbda" За всеки tdf в db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Тогава Ако Len(tdf.Connect) =0 И (Не tdf.Name като "~*") Тогава „Not attached, or temp. Ако TryGetProperty(tdf, SubDatasheetPropertyName, prp) Тогава TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.SetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.SetPropertyValue prp.Премахнахме цял
If
блок. Сега можем просто да прочетем кода и веднага, че се опитваме да зададем стойност на свойството и ако нещо се обърка, просто продължаваме напред. Това е много по-лесно за четене и името на функцията се самоописва. Доброто име прави по-малко необходимо да се търси дефиницията на функцията, за да се разбере какво прави.Създаване на
TryCreateOrSetProperty
процедураКодът е по-четлив, но все още имаме този
Else
блокирайте създаване на свойство. Можем ли още по-добре? Да! Нека помислим какво трябва да постигнем тук. Имаме имот, който може да съществува или не. Ако не, искаме да го създадем. Независимо дали вече съществува или не, трябва да бъде настроена на определена стойност. Така че това, от което се нуждаем, е функция, която или ще създаде свойство, или ще актуализира стойността, ако вече съществува. За да създадем свойство, трябва да извикамеCreateProperty
което за съжаление не е вProperties
а по-скоро различни DAO обекти. По този начин трябва късно да обвържем, като използвамеObject
тип данни. Все пак можем да предоставим някои проверки по време на изпълнение, за да избегнем грешки. Нека създадемTryCreateOrSetProperty
функция:Обществена функция TryCreateOrSetProperty( _ ByVal SourceDaoObject като обект, _ ByVal PropertyName като низ, _ ByVal PropertyType като DAO.DataTypeEnum, _ ByVal PropertyValue като вариант, _ ByVal PropertyValue като вариант, _ ByVal SourceDaoObject като обект, _ ByVal PropertyName като низ, _ ByVal PropertyType като DAO.DataTypeEnum, _ ByVal PropertyValue като вариант, _ _ ByVal PropertyValue като вариант, _ _ ByVal PropertyValue като вариант, _ _ ByRef Източник на изходния случай. Е DAO.TableDef, _ TypeOf SourceDaoObject е DAO.QueryDef, _ TypeOf SourceDaoObject е DAO.Field, _ TypeOf SourceDaoObject е DAO.Database Ако TryGetProperty(SourceDaoObject.Properties, PropertyName, OutPropertyOluper) TryaV PropertyVTryeS PropertyV Грешка Възобновяване Следващ Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty Ако Err.Number След това задайте OutProperty =Нищо Край, ако е включено Грешка GoTo 0 TryCreateOrSetProperty =(OutProperty не е нищо) Край, ако случай друг Err.Raise 5, , "Невалиден обект, предоставен на параметъра SourceDaoObject. Трябва да е DAO обект, който съдържа член на CreateProperty." Край на функцията SelectEndНяколко неща за отбелязване:
- Успяхме да надградим предишния
Try*
дефинирахме функция, която помага да се намали кодирането на тялото на функцията, което й позволява да се съсредоточи повече върху създаването, в случай че няма такова свойство. - Това непременно е по-подробно поради допълнителните проверки по време на изпълнение, но ние сме в състояние да го настроим така, че грешките да не променят потока на изпълнение и да можем да четем отгоре надолу без прескачане.
- Вместо да хвърляте
MsgBox
от нищото използвамеErr.Raise
и върне смислена грешка. Действителната обработка на грешки се делегира на кода за повикване, който след това може да реши дали да покаже кутия за съобщения на потребителя или да направи нещо друго. - Поради внимателното ни боравене и при условие, че
SourceDaoObject
параметърът е валиден, целият възможен път гарантира, че всички проблеми със създаването или настройката на стойността на съществуващо свойство ще бъдат обработени и ще получимfalse
резултат. Това се отразява на кода за повикване, както ще видим скоро.
Окончателна версия на кода за повикване
Нека актуализираме кода за повикване, за да използваме новата функция:
Public Sub EditTableSubdatasheetProperty( _ Optional NewValue As String ="[None]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName като db Currentlist ="NameDbda" За всеки tdf в db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Тогава Ако Len(tdf.Connect) =0 И (Не tdf.Name като "~*") Тогава „Not attached, or temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue Край, ако Край, ако NextEnd Sub
Това беше значително подобрение в четливостта. В оригиналната версия ще трябва да разгледаме много If
блокове и как обработката на грешки променя потока на изпълнение. Ще трябва да разберем какво точно прави съдържанието, за да заключим, че се опитваме да получим свойство или да го създадем, ако то не съществува и да го настроим на определена стойност. С текущата версия всичко е там в името на функцията, TryCreateOrSetProperty
. Сега можем да видим какво се очаква да направи функцията.
Заключение
Може би се чудите, „но добавихме много повече функции и много повече линии. Това не е ли много работа?" Вярно е, че в настоящата версия сме дефинирали още 3 функции. Въпреки това, можете да прочетете всяка отделна функция изолирано и все пак лесно да разберете какво трябва да прави. Също така видяхте, че TryCreateOrSetProperty
функцията може да се натрупа върху другите 2 Try*
функции. Това означава, че имаме повече гъвкавост при сглобяването на логиката.
Така че, ако напишем друга функция, която прави нещо със свойството на обекти, не е нужно да я пишем изцяло, нито да копираме и поставяме кода от оригиналния EditTableSubdatasheetProperty
в новата функция. В крайна сметка новата функция може да се нуждае от различни варианти и следователно да изисква различна последователност. И накрая, имайте предвид, че истинските бенефициенти са повикващият код, който трябва да направи нещо. Искаме да запазим кода за повикване на доста високо ниво, без да се затъват в подробности, което може да е лошо за поддръжката.
Можете също да видите, че обработката на грешки е значително опростена, въпреки че използвахме On Error Resume Next
. Вече не е необходимо да търсим кода за грешка, защото в по-голямата част от случаите се интересуваме само дали е успял или не. По-важното е, че обработката на грешки не промени потока на изпълнение, където имате някаква логика в тялото и друга логика в обработката на грешки. Последното е ситуация, която определено искаме да избегнем, защото ако има грешка в манипулатора на грешки, тогава поведението може да бъде изненадващо. Най-добре е това да не е възможно.
Всичко е за абстракция
Но най-важният резултат, който печелим тук, е нивото на абстракция, което можем да постигнем сега. Оригиналната версия на EditTableSubdatasheetProperty
съдържа много подробности от ниско ниво за DAO обекта наистина не е за основната цел на функцията. Помислете за дни, в които сте виждали процедура, дълга стотици редове с дълбоко вложени цикли или условия. Бихте ли искали да отстраните това? Не го правя.
Така че, когато видя процедура, първото нещо, което наистина искам да направя, е да извадя частите в тяхната собствена функция, така че да мога да повиша нивото на абстракция за тази процедура. Като се принуждаваме да повишим нивото на абстракция, ние също можем да избегнем големи класове грешки, при които причината е, че една промяна в част от мегапроцедурата има непреднамерени последици за другите части на процедурите. Когато извикваме функции и предаваме параметри, ние също така намаляваме възможността нежелани странични ефекти да пречат на нашата логика.
Ето защо обичам модела „Опитайте*“. Надявам се да го намерите полезен и за вашите проекти.