Access
 sql >> Base de données >  >> RDS >> Access

Écriture de code lisible pour VBA - Modèle Try*

Écriture de code lisible pour VBA – Modèle Try*

Dernièrement, je me suis retrouvé à utiliser le Try modèle de plus en plus. J'aime beaucoup ce modèle car il rend le code beaucoup plus lisible. Ceci est particulièrement important lors de la programmation dans un langage de programmation mature comme VBA où la gestion des erreurs est étroitement liée au flux de contrôle. En général, je trouve que toutes les procédures qui reposent sur la gestion des erreurs comme flux de contrôle sont plus difficiles à suivre.

Scénario

Commençons par un exemple. Le modèle d'objet DAO est un candidat parfait en raison de son fonctionnement. Voir, tous les objets DAO ont des Properties collection, qui contient Property objets. Cependant, n'importe qui peut ajouter une propriété personnalisée. En fait, Access ajoutera plusieurs propriétés à divers objets DAO. Par conséquent, nous pouvons avoir une propriété qui n'existe pas et doit gérer à la fois le cas de la modification de la valeur d'une propriété existante et le cas de l'ajout d'une nouvelle propriété.

Utilisons Subdatasheet propriété à titre d'exemple. Par défaut, toutes les tables créées via Access UI auront la propriété définie sur Auto , mais nous pourrions ne pas vouloir cela. Mais si nous avons des tables créées dans le code ou d'une autre manière, il se peut qu'elles n'aient pas la propriété. Nous pouvons donc commencer avec une version initiale du code pour mettre à jour toutes les propriétés des tables et gérer les deux cas.

Public Sub EditTableSubdatasheetProperty( _ Facultatif NewValue As String ="[Aucun]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" En cas d'erreur GoTo ErrHandler Set db =CurrentDb For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Non attaché, ou temp . Set prp =tdf.Properties(SubDatasheetPropertyName) If prp.Value <> NewValue Then prp.Value =NewValue End If End If End IfContinue:NextExitProc:Quitter SubErrHandler:If Err.Number =3270 Then Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Resume Continue End If MsgBox Err.Number &":" &Err.Description Resume ExitProc End Sub

Le code fonctionnera probablement. Cependant, pour le comprendre, nous devons probablement schématiser un organigramme. La ligne Set prp = tdf.Properties(SubDatasheetPropertyName) pourrait potentiellement générer une erreur 3270. Dans ce cas, le contrôle passe à la section de gestion des erreurs. Nous créons ensuite une propriété puis reprenons à un autre point de la boucle en utilisant l'étiquette Continue . Il y a des questions…

  • Et si 3270 est augmenté sur une autre ligne ?
  • Supposons que la ligne Set prp =... ne lance pas erreur 3270 mais en fait une autre erreur ?
  • Et si, alors que nous sommes dans le gestionnaire d'erreurs, une autre erreur se produit lors de l'exécution de Append ou CreateProperty ?
  • Cette fonction devrait-elle même afficher une Msgbox ? Pensez aux fonctions qui sont censées fonctionner sur quelque chose au nom de formulaires ou de boutons. Si les fonctions affichent une boîte de message, puis se terminent normalement, le code appelant n'a aucune idée que quelque chose s'est mal passé et peut continuer à faire des choses qu'il ne devrait pas faire.
  • Pouvez-vous jeter un coup d'œil au code et comprendre immédiatement ce qu'il fait ? Je ne peux pas. Je dois loucher, puis réfléchir à ce qui devrait se passer en cas d'erreur et esquisser mentalement le chemin. Ce n'est pas facile à lire.

Ajouter un HasProperty procédure

Peut-on faire mieux ? Oui! Certains programmeurs reconnaissent déjà le problème de l'utilisation de la gestion des erreurs comme je l'ai illustré et l'ont judicieusement résumé dans sa propre fonction. Voici une meilleure version :

Public Sub EditTableSubdatasheetProperty( _ Facultatif NewValue As String ="[Aucun]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =CurrentDb For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Non attaché, ou temp. Si non HasProperty(tdf, SubDatasheetPropertyName) Then Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp Else If tdf.Properties(SubDatasheetPropertyName) <> NewValue Then tdf.Properties(SubDatasheetPropertyName) =NewValue End If End If End If End If NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Ignored As Variant On Error Resume Next Ignored =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End Function 

Au lieu de mélanger le flux d'exécution avec la gestion des erreurs, nous avons maintenant une fonction HasFunction qui résume parfaitement la vérification sujette aux erreurs pour une propriété qui peut ne pas exister. Par conséquent, nous n'avons pas besoin du flux complexe de gestion des erreurs/d'exécution que nous avons vu dans le premier exemple. C'est une grande amélioration et rend le code quelque peu lisible. Mais…

  • Nous avons une branche qui utilise la variable prp et nous avons une autre branche qui utilise tdf.Properties(SubDatasheetPropertyName) qui se réfère en fait à la même propriété. Pourquoi nous répétons-nous avec deux manières différentes de référencer la même propriété ?
  • Nous gérons beaucoup la propriété. Le HasProperty doit gérer la propriété afin de savoir si elle existe puis renvoie simplement un Boolean résultat, laissant au code appelant le soin d'essayer à nouveau d'obtenir à nouveau la même propriété pour modifier la valeur.
  • De même, nous gérons la NewValue plus que nécessaire. Soit nous le passons dans le CreateProperty ou définissez la Value propriété de la propriété.
  • Le HasProperty la fonction suppose implicitement que l'objet a un Properties membre et l'appelle à liaison tardive, ce qui signifie qu'il s'agit d'une erreur d'exécution si un mauvais type d'objet lui est fourni.

Utilisez TryGetProperty à la place

Peut-on faire mieux ? Oui! C'est là que nous devons examiner le modèle Try. Si vous avez déjà programmé avec .NET, vous avez probablement déjà vu des méthodes comme TryParse où, au lieu de générer une erreur en cas d'échec, nous pouvons définir une condition pour faire quelque chose en cas de succès et autre chose en cas d'échec. Mais plus important encore, nous avons le résultat disponible pour le succès. Alors, comment pourrions-nous améliorer le HasProperty une fonction? D'une part, nous devrions retourner la Property objet. Essayons ce code :

Public Function TryGetProperty( _ ByVal SourceProperties As DAO.Properties, _ ByVal PropertyName As String, _ ByRef OutProperty As DAO.Property _) As Boolean On Error Resume Next Set OutProperty =SourceProperties(PropertyName) If Err.Number Then Set OutProperty =Rien End If On Error GoTo 0 TryGetProperty =(Not OutProperty Is Nothing)End Function

Avec peu de changements, nous avons remporté quelques gros gains :

  • L'accès aux Properties n'est plus lié tardivement. Nous n'avons pas à espérer qu'un objet possède une propriété nommée Properties et c'est de DAO.Properties . Cela peut être vérifié au moment de la compilation.
  • Au lieu d'un simple Boolean résultat, nous pouvons également obtenir la Property récupérée objet, mais seulement sur le succès. Si nous échouons, le OutProperty le paramètre sera Nothing . Nous utiliserons toujours le Boolean résultat pour vous aider à configurer le flux ascendant, comme vous le verrez bientôt.
  • En nommant notre nouvelle fonction avec Try préfixe, nous indiquons qu'il est garanti de ne pas générer d'erreur dans des conditions de fonctionnement normales. Évidemment, nous ne pouvons pas empêcher les erreurs de mémoire insuffisante ou quelque chose comme ça, mais à ce stade, nous avons des problèmes beaucoup plus importants. Mais dans des conditions de fonctionnement normales, nous avons évité d'emmêler notre gestion des erreurs avec le flux d'exécution. Le code peut maintenant être lu de haut en bas sans aucun saut en avant ou en arrière.

Notez que par convention, je préfixe la propriété "out" avec Out . Cela aide à préciser que nous sommes censés transmettre la variable à la fonction non initialisée. Nous nous attendons également à ce que la fonction initialise le paramètre. Cela sera clair lorsque nous examinerons le code d'appel. Alors, configurons le code d'appel.

Code d'appel révisé à l'aide de TryGetProperty

Public Sub EditTableSubdatasheetProperty( _ Facultatif NewValue As String ="[Aucun]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =CurrentDb For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Non attaché, ou temp. If TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then If prp.Value <> NewValue Then prp.Value =NewValue End If Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp End If End If End Si SuivantEnd Sub

Le code est maintenant un peu plus lisible avec le premier modèle Try. Nous avons réussi à réduire la manipulation du prp . Notez que nous passons le prp variable dans true , le prp sera initialisé avec la propriété que nous voulons manipuler. Sinon, le prp reste Nothing . Nous pouvons ensuite utiliser le CreateProperty pour initialiser le prp variables.

Nous avons également inversé la négation afin que le code devienne plus facile à lire. Cependant, nous n'avons pas vraiment réduit la gestion de NewValue paramètre. Nous avons encore un autre bloc imbriqué pour vérifier la valeur. Peut-on faire mieux ? Oui! Ajoutons une autre fonction :

Ajout de TrySetPropertyValue procédure

Public Function TrySetPropertyValue( _ ByVal SourceProperty As DAO.Property, _ ByVal NewValue As Variant_) As Boolean If SourceProperty.Value =PropertyValue Then TrySetPropertyValue =True Else On Error Resume Next SourceProperty.Value =NewValue On Error GoTo 0 TrySetPropertyValue =( SourceProperty.Value =NewValue) Fin de la fonction IfEnd

Parce que nous garantissons que cette fonction ne générera pas d'erreur lors de la modification de la valeur, nous l'appelons TrySetPropertyValue . Plus important encore, cette fonction aide à encapsuler tous les détails sanglants entourant la modification de la valeur de la propriété. Nous avons un moyen de garantir que la valeur correspond à la valeur à laquelle nous nous attendions. Regardons comment le code d'appel sera modifié avec cette fonction.

Mise à jour du code d'appel utilisant à la fois TryGetProperty et TrySetPropertyValue

Public Sub EditTableSubdatasheetProperty( _ Facultatif NewValue As String ="[Aucun]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =CurrentDb For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Non attaché, ou temp. Si TryGetProperty(tdf, SubDatasheetPropertyName, prp) Then TrySetPropertyValue prp, NewValue Else Set prp =tdf.CreateProperty(SubDatasheetPropertyName , dbText, NewValue) tdf.Properties.Append prp End If End If End If NextEnd Sub

Nous avons éliminé un If entier bloc. Nous pouvons maintenant simplement lire le code et immédiatement que nous essayons de définir une valeur de propriété et si quelque chose ne va pas, nous continuons à avancer. C'est beaucoup plus facile à lire et le nom de la fonction est auto-descriptif. Un bon nom rend moins nécessaire de rechercher la définition de la fonction pour comprendre ce qu'elle fait.

Création de TryCreateOrSetProperty procédure

Le code est plus lisible mais nous avons toujours ce Else bloquer la création d'une propriété. Peut-on faire mieux encore ? Oui! Réfléchissons à ce que nous devons accomplir ici. Nous avons une propriété qui peut exister ou non. Si ce n'est pas le cas, nous voulons le créer. Qu'il existe déjà ou non, nous avons besoin qu'il soit fixé à une certaine valeur. Nous avons donc besoin d'une fonction qui créera une propriété ou mettra à jour la valeur si elle existe déjà. Pour créer une propriété, nous devons appeler CreateProperty qui n'est malheureusement pas sur les Properties mais plutôt des objets DAO différents. Ainsi, nous devons effectuer une liaison tardive en utilisant Object Type de données. Cependant, nous pouvons toujours fournir des vérifications d'exécution pour éviter les erreurs. Créons un TryCreateOrSetProperty fonction :

Public Function TryCreateOrSetProperty( _ ByVal SourceDaoObject As Object, _ ByVal PropertyName As String, _ ByVal PropertyType As DAO.DataTypeEnum, _ ByVal PropertyValue As Variant, _ ByRef OutProperty As DAO.Property _) As Boolean Select Case True Case TypeOf SourceDaoObject Est DAO.TableDef, _ TypeOf SourceDaoObject est DAO.QueryDef, _ TypeOf SourceDaoObject est DAO.Field, _ TypeOf SourceDaoObject est DAO.Database Si TryGetProperty(SourceDaoObject.Properties, PropertyName, OutProperty) Alors TryCreateOrSetProperty =TrySetPropertyValue(OutProperty, PropertyValue) Sinon activé Erreur Reprendre Suivant Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Then Set OutProperty =Nothing End If On Erreur GoTo 0 TryCreateOrSetProperty =(OutProperty Is Nothing) End If Case Else Err.Raise 5, , "Objet non valide fourni au paramètre SourceDaoObject. Il doit s'agir d'un objet DAO qui contient un membre CreateProperty." End SelectEnd Function

Quelques points à noter :

  • Nous avons pu nous appuyer sur le précédent Try* fonction que nous avons définie, ce qui permet de réduire le codage du corps de la fonction, lui permettant de se concentrer davantage sur la création dans le cas où une telle propriété n'existe pas.
  • Ceci est nécessairement plus détaillé en raison des vérifications d'exécution supplémentaires, mais nous pouvons le configurer de sorte que les erreurs n'altèrent pas le flux d'exécution et que nous puissions toujours lire de haut en bas sans saut.
  • Au lieu de lancer une MsgBox de nulle part, nous utilisons Err.Raise et renvoie une erreur significative. La gestion réelle des erreurs est déléguée au code appelant qui peut alors décider d'afficher une boîte de message à l'utilisateur ou de faire autre chose.
  • En raison de notre traitement soigneux et de la fourniture que le SourceDaoObject est valide, tous les chemins possibles garantissent que tout problème de création ou de définition de la valeur d'une propriété existante sera traité et nous obtiendrons un false résultat. Cela affecte le code d'appel comme nous le verrons bientôt.

Version finale du code d'appel

Mettons à jour le code d'appel pour utiliser la nouvelle fonction :

Public Sub EditTableSubdatasheetProperty( _ Facultatif NewValue As String ="[Aucun]" _) Dim db As DAO.Database Dim tdf As DAO.TableDef Dim prp As DAO.Property Const SubDatasheetPropertyName As String ="SubdatasheetName" Set db =CurrentDb For Each tdf In db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 And (Not tdf.Name Like "~*") Then 'Non attaché, ou temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub

C'était une nette amélioration de la lisibilité. Dans la version originale, nous aurions à scruter un certain nombre de If blocs et comment la gestion des erreurs modifie le flux d'exécution. Nous aurions à comprendre ce que faisait exactement le contenu pour conclure que nous essayons d'obtenir une propriété ou de la créer si elle n'existe pas et qu'elle soit définie sur une certaine valeur. Avec la version actuelle, tout est là dans le nom de la fonction, TryCreateOrSetProperty . Nous pouvons maintenant voir ce que la fonction est censée faire.

Conclusion

Vous vous demandez peut-être « mais nous avons ajouté beaucoup plus de fonctions et beaucoup plus de lignes. N'est-ce pas beaucoup de travail ?" Il est vrai que dans cette version actuelle, nous avons défini 3 fonctions supplémentaires. Cependant, vous pouvez lire chaque fonction isolément et toujours comprendre facilement ce qu'elle doit faire. Vous avez également vu que le TryCreateOrSetProperty fonction pourrait s'accumuler sur les 2 autres Try* les fonctions. Cela signifie que nous avons plus de flexibilité pour assembler la logique.

Donc, si nous écrivons une autre fonction qui fait quelque chose avec la propriété des objets, nous n'avons pas à tout réécrire ni à copier-coller le code de l'original EditTableSubdatasheetProperty dans la nouvelle fonction. Après tout, la nouvelle fonction peut avoir besoin de différentes variantes et donc nécessiter une séquence différente. Enfin, gardez à l'esprit que les véritables bénéficiaires sont le code appelant qui doit faire quelque chose. Nous voulons garder le code d'appel à un niveau assez élevé sans être embourbé dans des détails qui peuvent être mauvais pour la maintenance.

Vous pouvez également voir que la gestion des erreurs est considérablement simplifiée, même si nous avons utilisé On Error Resume Next . Nous n'avons plus besoin de rechercher le code d'erreur car dans la majorité des cas, nous ne nous intéressons qu'à savoir s'il a réussi ou non. Plus important encore, la gestion des erreurs n'a pas modifié le flux d'exécution où vous avez une certaine logique dans le corps et une autre logique dans la gestion des erreurs. Ce dernier est une situation que nous voulons absolument éviter car s'il y a une erreur dans le gestionnaire d'erreurs, le comportement peut être surprenant. Il vaut mieux éviter que cela ne soit une possibilité.

Tout est une question d'abstraction

Mais le score le plus important que nous gagnons ici est le niveau d'abstraction que nous pouvons maintenant atteindre. La version originale de EditTableSubdatasheetProperty contenait beaucoup de détails de bas niveau sur l'objet DAO ne concerne vraiment pas l'objectif principal de la fonction. Pensez aux jours où vous avez vu une procédure longue de centaines de lignes avec des boucles ou des conditions profondément imbriquées. Souhaitez-vous déboguer cela ? Je ne sais pas.

Ainsi, lorsque je vois une procédure, la première chose que je veux vraiment faire est de décomposer les parties en leur propre fonction, afin de pouvoir élever le niveau d'abstraction de cette procédure. En nous forçant à pousser le niveau d'abstraction, nous pouvons également éviter de grandes classes de bogues dont la cause est qu'un changement dans une partie de la méga-procédure a des ramifications imprévues pour les autres parties des procédures. Lorsque nous appelons des fonctions et transmettons des paramètres, nous réduisons également la possibilité d'effets secondaires indésirables interférant avec notre logique.

C'est pourquoi j'aime le modèle "Essayer *". J'espère que vous le trouverez également utile pour vos projets.