Scrittura di codice leggibile per VBA:modello Prova*
Ultimamente mi sono ritrovato a usare il Try
modello sempre di più. Mi piace molto questo modello perché rende il codice molto più leggibile. Ciò è particolarmente importante quando si programma in un linguaggio di programmazione maturo come VBA in cui la gestione degli errori è intrecciata con il flusso di controllo. In genere, trovo più difficile seguire tutte le procedure che si basano sulla gestione degli errori come flusso di controllo.
Scenario
Iniziamo con un esempio. Il modello a oggetti DAO è un candidato perfetto per il modo in cui funziona. Vedi, tutti gli oggetti DAO hanno Properties
raccolta, che contiene Properties
oggetti. Tuttavia, chiunque può aggiungere proprietà personalizzate. In effetti, Access aggiungerà diverse proprietà a vari oggetti DAO. Pertanto, potremmo avere una proprietà che potrebbe non esistere e che dobbiamo gestire sia il caso di modifica del valore di una proprietà esistente sia il caso di aggiungere una nuova proprietà.
Usiamo Subdatasheet
proprietà come esempio. Per impostazione predefinita, tutte le tabelle create tramite l'interfaccia utente di Access avranno la proprietà impostata su Auto
, ma potremmo non volerlo. Ma se abbiamo tabelle create nel codice o in qualche altro modo, potrebbe non avere la proprietà. Quindi possiamo iniziare con una versione iniziale del codice per aggiornare tutte le proprietà delle tabelle e gestire entrambi i casi.
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 = "SubdatasheetName" On Error 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 'Not attached, or temp. Set prp = tdf.Properties(SubDatasheetPropertyName) If prp.Value <> NewValue Then prp.Value = NewValue End If End If End If Continue: Next ExitProc: Exit Sub ErrHandler: 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
Il codice probabilmente funzionerà. Tuttavia, per capirlo, probabilmente dobbiamo tracciare un diagramma di flusso. La riga Set prp = tdf.Properties(SubDatasheetPropertyName)
potrebbe potenzialmente generare un errore 3270. In questo caso, il controllo passa alla sezione di gestione degli errori. Quindi creiamo una proprietà e poi riprendiamo in un punto diverso del ciclo usando l'etichetta Continue
. Ci sono alcune domande...
- Cosa succede se viene generato 3270 su un'altra linea?
- Supponiamo che la riga
Set prp =...
non lancia errore 3270 ma in realtà qualche altro errore? - E se mentre siamo all'interno del gestore degli errori, si verifica un altro errore durante l'esecuzione di
Append
oCreateProperty
? - Questa funzione dovrebbe mostrare un
Msgbox
? Pensa alle funzioni che dovrebbero funzionare su qualcosa per conto di moduli o pulsanti. Se le funzioni mostrano una finestra di messaggio, quindi escono normalmente, il codice chiamante non ha idea che qualcosa sia andato storto e potrebbe continuare a fare cose che non dovrebbe fare. - Puoi dare un'occhiata al codice e capire cosa fa immediatamente? non posso. Devo strizzare gli occhi, poi pensare a cosa dovrebbe succedere in caso di errore e tracciare mentalmente il percorso. Non è facile da leggere.
Aggiungi un HasProperty
procedura
Possiamo fare di meglio? Sì! Alcuni programmatori riconoscono già il problema con l'utilizzo della gestione degli errori come ho illustrato e l'hanno saggiamente astratto nella sua stessa funzione. Ecco una versione migliore:
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 = "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 'Not attached, or temp. If Not 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 Next End Sub Public 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
Invece di confondere il flusso di esecuzione con la gestione degli errori, ora abbiamo una funzione HasFunction
che astrae ordinatamente il controllo soggetto a errori per una proprietà che potrebbe non esistere. Di conseguenza, non abbiamo bisogno del complesso flusso di gestione/esecuzione degli errori che abbiamo visto nel primo esempio. Questo è un grande miglioramento e rende il codice in qualche modo leggibile. Ma...
- Abbiamo un ramo che utilizza la variabile
prp
e abbiamo un altro ramo che utilizzatdf.Properties(SubDatasheetPropertyName)
che di fatto si riferisce alla stessa proprietà. Perché ci ripetiamo con due modi diversi per fare riferimento alla stessa proprietà? - Ci occupiamo molto della proprietà. Il
HasProperty
deve gestire la proprietà per scoprire se esiste, quindi restituisce semplicemente unBoolean
risultato, lasciando al codice chiamante il compito di riprovare a recuperare la stessa proprietà per modificare il valore. - Allo stesso modo, stiamo gestendo il
NewValue
più del necessario. Lo passiamo inCreateProperty
oppure imposta ilValue
proprietà della proprietà. - Il
HasProperty
La funzione presuppone implicitamente che l'oggetto abbia unaProperties
membro e lo chiama late-bound, il che significa che si tratta di un errore di runtime se viene fornito un tipo errato di oggetto.
Usa TryGetProperty
invece
Possiamo fare di meglio? Sì! È qui che dobbiamo guardare il modello Try. Se hai mai programmato con .NET, probabilmente hai visto metodi come TryParse
dove invece di generare un errore in caso di fallimento, possiamo impostare una condizione per fare qualcosa per il successo e qualcos'altro per il fallimento. Ma soprattutto, abbiamo il risultato disponibile per il successo. Quindi, come potremmo migliorare su HasProperty
funzione? Per prima cosa, dovremmo restituire la Properties
oggetto. Proviamo questo codice:
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 = Nothing End If On Error GoTo 0 TryGetProperty = (Not OutProperty Is Nothing) End Function
Con poche modifiche, abbiamo ottenuto alcune grandi vittorie:
- L'accesso alle
Properties
non è più in ritardo. Non dobbiamo sperare che un oggetto abbia una proprietà denominataProperties
ed è diDAO.Properties
. Questo può essere verificato in fase di compilazione. - Invece di un semplice
Boolean
risultato, possiamo anche ottenere laProperties
recuperata oggetto, ma solo sul successo. Se falliamo,OutProperty
il parametro saràNothing
. Useremo ancora ilBoolean
risultato per aiutare con l'impostazione del flusso come vedrai a breve. - Nominando la nostra nuova funzione con
Try
prefisso, stiamo indicando che questo è garantito per non generare un errore in normali condizioni operative. Ovviamente, non possiamo prevenire errori di memoria insufficiente o qualcosa del genere, ma a quel punto abbiamo problemi molto più grandi. Ma nelle normali condizioni operative, abbiamo evitato di confondere la nostra gestione degli errori con il flusso di esecuzione. Il codice ora può essere letto dall'alto verso il basso senza salti avanti o indietro.
Nota che per convenzione, antepongo alla proprietà "out" Out
. Ciò aiuta a chiarire che dovremmo passare la variabile alla funzione non inizializzata. Ci aspettiamo anche che la funzione inizializzi il parametro. Questo sarà chiaro quando esamineremo il codice di chiamata. Quindi, impostiamo il codice di chiamata.
Codice di chiamata rivisto utilizzando 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 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 'Not attached, or 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 If Next End Sub
Il codice è ora un po' più leggibile con il primo pattern Try. Siamo riusciti a ridurre la gestione del prp
. Nota che passiamo il prp
variabile nella prp
verrà inizializzato con la proprietà che vogliamo manipolare. Altrimenti, il prp
rimane Nothing
. Possiamo quindi utilizzare il CreateProperty
per inizializzare il prp
variabile.
Abbiamo anche capovolto la negazione in modo che il codice diventi più facile da leggere. Tuttavia, non abbiamo davvero ridotto la gestione di NewValue
parametro. Abbiamo ancora un altro blocco annidato per verificare il valore. Possiamo fare di meglio? Sì! Aggiungiamo un'altra funzione:
Aggiunta di TrySetPropertyValue
procedura
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) End If End Function
Poiché garantiamo che questa funzione non generi errori durante la modifica del valore, la chiamiamo TrySetPropertyValue
. Ancora più importante, questa funzione aiuta a incapsulare tutti i dettagli cruenti che circondano la modifica del valore della proprietà. Abbiamo un modo per garantire che il valore sia il valore che ci aspettavamo che fosse. Vediamo come verrà modificato il codice chiamante con questa funzione.
Codice di chiamata aggiornato utilizzando entrambi TryGetProperty
e 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 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 'Not attached, or temp. If 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 Next End Sub
Abbiamo eliminato un intero If
bloccare. Ora possiamo semplicemente leggere il codice e immediatamente che stiamo cercando di impostare un valore di proprietà e se qualcosa va storto, continuiamo ad andare avanti. È molto più facile da leggere e il nome della funzione è autodescrittivo. Un buon nome rende meno necessario cercare la definizione della funzione per capire cosa sta facendo.
Creazione di TryCreateOrSetProperty
procedura
Il codice è più leggibile ma abbiamo ancora quell'Else
bloccare la creazione di una proprietà. Possiamo fare ancora meglio? Sì! Pensiamo a cosa dobbiamo realizzare qui. Abbiamo una proprietà che può esistere o meno. In caso contrario, vogliamo crearlo. Indipendentemente dal fatto che esistesse già o meno, abbiamo bisogno che sia impostato su un certo valore. Quindi ciò di cui abbiamo bisogno è una funzione che creerà una proprietà o aggiornerà il valore se esiste già. Per creare una proprietà, dobbiamo chiamare CreateProperty
che purtroppo non è nelle Properties
ma oggetti DAO piuttosto diversi. Quindi, dobbiamo legare in ritardo usando Object
tipo di dati. Tuttavia, possiamo comunque fornire alcuni controlli di runtime per evitare errori. Creiamo un TryCreateOrSetProperty
funzione:
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 Is DAO.TableDef, _ TypeOf SourceDaoObject Is DAO.QueryDef, _ TypeOf SourceDaoObject Is DAO.Field, _ TypeOf SourceDaoObject Is DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertyName, OutProperty) Then TryCreateOrSetProperty = TrySetPropertyValue(OutProperty, PropertyValue) Else On Error Resume Next Set OutProperty = SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Then Set OutProperty = Nothing End If On Error GoTo 0 TryCreateOrSetProperty = (OutProperty Is Nothing) End If Case Else Err.Raise 5, , "Invalid object provided to the SourceDaoObject parameter. It must be an DAO object that contains a CreateProperty member." End Select End Function
Poche cose da notare:
- Siamo stati in grado di sviluppare il precedente
Try*
funzione che abbiamo definito, che aiuta a ridurre la codifica del corpo della funzione, consentendole di concentrarsi maggiormente sulla creazione nel caso non ci sia tale proprietà. - Questo è necessariamente più dettagliato a causa dei controlli di runtime aggiuntivi, ma siamo in grado di impostarlo in modo che gli errori non alterino il flusso di esecuzione e possiamo comunque leggere dall'alto verso il basso senza salti.
- Invece di lanciare un
MsgBox
dal nulla, usiamoErr.Raise
e restituisce un errore significativo. L'effettiva gestione degli errori è delegata al codice chiamante che può quindi decidere se mostrare una messagebox all'utente o fare qualcos'altro. - A causa della nostra attenta gestione e fornitura che il
SourceDaoObject
parametro è valido, tutto il percorso possibile garantisce che eventuali problemi con la creazione o l'impostazione del valore di una proprietà esistente verranno gestiti e otterremo unfalse
risultato. Ciò influisce sul codice di chiamata come vedremo a breve.
Versione finale del codice di chiamata
Aggiorniamo il codice chiamante per utilizzare la nuova funzione:
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 = "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 'Not attached, or temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If Next End Sub
Questo è stato un bel miglioramento nella leggibilità. Nella versione originale, dovremmo esaminare un certo numero di If
blocchi e come la gestione degli errori altera il flusso di esecuzione. Dovremmo capire cosa stava facendo esattamente il contenuto per concludere che stiamo cercando di ottenere una proprietà o crearla se non esiste e impostarla su un certo valore. Con la versione attuale, è tutto lì nel nome della funzione, TryCreateOrSetProperty
. Ora possiamo vedere cosa dovrebbe fare la funzione.
Conclusione
Ti starai chiedendo, “ma abbiamo aggiunto molte più funzioni e molte più linee. Non è un sacco di lavoro?" È vero che in questa versione attuale abbiamo definito altre 3 funzioni. Tuttavia, puoi leggere ogni singola funzione isolatamente e comunque capire facilmente cosa dovrebbe fare. Hai anche visto che il TryCreateOrSetProperty
la funzione potrebbe accumularsi sugli altri 2 Try*
funzioni. Ciò significa che abbiamo più flessibilità nell'assemblare la logica.
Quindi, se scriviamo un'altra funzione che fa qualcosa con la proprietà degli oggetti, non dobbiamo scriverla dappertutto né copiare e incollare il codice dall'originale EditTableSubdatasheetProperty
nella nuova funzione. Dopotutto, la nuova funzione potrebbe richiedere alcune varianti diverse e quindi richiedere una sequenza diversa. Infine, tieni presente che i veri beneficiari sono il codice di chiamata che deve fare qualcosa. Vogliamo mantenere il codice di chiamata abbastanza alto senza essere impantanati nei dettagli che possono essere dannosi per la manutenzione.
Puoi anche vedere che la gestione degli errori è notevolmente semplificata, anche se abbiamo usato On Error Resume Next
. Non abbiamo più bisogno di cercare il codice di errore perché nella maggior parte dei casi, ci interessa solo sapere se è riuscito o meno. Ancora più importante, la gestione degli errori non ha modificato il flusso di esecuzione in cui è presente una logica nel corpo e altra logica nella gestione degli errori. Quest'ultima è una situazione che vogliamo assolutamente evitare perché se si verifica un errore all'interno del gestore degli errori, il comportamento può essere sorprendente. È meglio evitare che questa sia una possibilità.
Si tratta di astrazione
Ma il punteggio più importante che otteniamo qui è il livello di astrazione che ora possiamo raggiungere. La versione originale di EditTableSubdatasheetProperty
conteneva molti dettagli di basso livello sull'oggetto DAO in realtà non riguarda l'obiettivo principale della funzione. Pensa ai giorni in cui hai visto una procedura lunga centinaia di righe con cicli o condizioni profondamente nidificati. Vorresti eseguire il debug di questo? Io no.
Quindi, quando vedo una procedura, la prima cosa che voglio davvero fare è estrarre le parti nella loro funzione, in modo da poter aumentare il livello di astrazione per quella procedura. Sforzandoci di aumentare il livello di astrazione, possiamo anche evitare grandi classi di bug la cui causa è che un cambiamento in una parte della mega-procedura ha ramificazioni indesiderate per le altre parti delle procedure. Quando chiamiamo funzioni e passiamo parametri, riduciamo anche la possibilità che effetti collaterali indesiderati interferiscano con la nostra logica.
Ecco perché amo il modello "Try*". Spero che lo trovi utile anche per i tuoi progetti.