Access
 sql >> Database >  >> RDS >> Access

Scrittura di codice leggibile per VBA – modello Try*

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 o CreateProperty ?
  • 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 utilizza tdf.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 un Boolean 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 in CreateProperty oppure imposta il Value proprietà della proprietà.
  • Il HasProperty La funzione presuppone implicitamente che l'oggetto abbia una Properties 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à denominata Properties ed è di DAO.Properties . Questo può essere verificato in fase di compilazione.
  • Invece di un semplice Boolean risultato, possiamo anche ottenere la Properties recuperata oggetto, ma solo sul successo. Se falliamo, OutProperty il parametro sarà Nothing . Useremo ancora il Boolean 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 true , il 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, usiamo Err.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 un false 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.