Access
 sql >> база данни >  >> RDS >> Access

Писане на четим код за VBA – Опитайте* шаблон

Писане на четим код за 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 променлива в true , 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 обекта наистина не е за основната цел на функцията. Помислете за дни, в които сте виждали процедура, дълга стотици редове с дълбоко вложени цикли или условия. Бихте ли искали да отстраните това? Не го правя.

Така че, когато видя процедура, първото нещо, което наистина искам да направя, е да извадя частите в тяхната собствена функция, така че да мога да повиша нивото на абстракция за тази процедура. Като се принуждаваме да повишим нивото на абстракция, ние също можем да избегнем големи класове грешки, при които причината е, че една промяна в част от мегапроцедурата има непреднамерени последици за другите части на процедурите. Когато извикваме функции и предаваме параметри, ние също така намаляваме възможността нежелани странични ефекти да пречат на нашата логика.

Ето защо обичам модела „Опитайте*“. Надявам се да го намерите полезен и за вашите проекти.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как индексите влияят върху производителността на базата данни?

  2. Оптимизиране на Microsoft Access със SQL Server IndyPass – 21.05.19

  3. Какво е плоска файлова база данни? Как се различава от релационна база данни?

  4. Разделяне на данни за широкомащабни приложения

  5. Достъп до мнението на експертите за срещата на върха за MVP 2020