sql >> Base de Datos >  >> RDS >> Access

Escritura de código legible para VBA:patrón Try*

Escribir código legible para VBA:patrón Try*

Últimamente, me he encontrado usando el Try patrón cada vez más. Realmente me gusta este patrón porque hace que el código sea mucho más legible. Esto es especialmente importante cuando se programa en un lenguaje de programación maduro como VBA, donde el manejo de errores está entrelazado con el flujo de control. En general, encuentro que los procedimientos que se basan en el manejo de errores como un flujo de control son más difíciles de seguir.

Escenario

Comencemos con un ejemplo. El modelo de objetos DAO es un candidato perfecto debido a cómo funciona. Mira, todos los objetos DAO tienen Properties colección, que contiene Property objetos. Sin embargo, cualquiera puede agregar propiedades personalizadas. De hecho, Access agregará varias propiedades a varios objetos DAO. Por lo tanto, es posible que tengamos una propiedad que podría no existir y debemos manejar tanto el caso de cambiar el valor de una propiedad existente como el caso de agregar una nueva propiedad.

Usemos Subdatasheet propiedad como ejemplo. De forma predeterminada, todas las tablas creadas a través de la interfaz de usuario de Access tendrán la propiedad establecida en Auto , pero es posible que no queramos eso. Pero si tenemos tablas que se crean en código o de alguna otra manera, es posible que no tenga la propiedad. Entonces podemos comenzar con una versión inicial del código para actualizar la propiedad de todas las tablas y manejar ambos casos.

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

El código probablemente funcionará. Sin embargo, para entenderlo, probablemente tengamos que diagramar algún diagrama de flujo. La línea Set prp = tdf.Properties(SubDatasheetPropertyName) podría arrojar potencialmente un error 3270. En este caso, el control salta a la sección de manejo de errores. Luego creamos una propiedad y luego reanudamos en un punto diferente del ciclo usando la etiqueta Continue . Hay algunas preguntas...

  • ¿Qué sucede si se eleva 3270 en alguna otra línea?
  • Supongamos que la línea Set prp =... no lanza error 3270 pero en realidad algún otro error?
  • ¿Qué sucede si mientras estamos dentro del controlador de errores, ocurre otro error al ejecutar Append? o CreateProperty ?
  • ¿Debería esta función mostrar un Msgbox? ? Piense en funciones que se supone que funcionan en algo en nombre de formularios o botones. Si las funciones muestran un cuadro de mensaje, salga normalmente, el código de llamada no tiene idea de que algo salió mal y podría continuar haciendo cosas que no debería estar haciendo.
  • ¿Puedes mirar el código y entender lo que hace inmediatamente? No puedo. Tengo que mirarlo con los ojos entrecerrados, luego pensar en lo que debería suceder en caso de error y trazar mentalmente el camino. Eso no es fácil de leer.

Añadir una HasProperty procedimiento

¿Podemos hacerlo mejor? ¡Sí! Algunos programadores ya reconocen el problema con el uso del manejo de errores como lo ilustré y abstrajeron sabiamente esto en su propia función. Aquí hay una versión mejorada:

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

En lugar de mezclar el flujo de ejecución con el manejo de errores, ahora tenemos una función HasFunction que abstrae perfectamente la comprobación propensa a errores de una propiedad que puede no existir. Como consecuencia, no necesitamos el flujo complejo de manejo/ejecución de errores que vimos en el primer ejemplo. Esta es una gran mejora y hace que el código sea algo legible. Pero…

  • Tenemos una rama que usa la variable prp y tenemos otra rama que usa tdf.Properties(SubDatasheetPropertyName) que de hecho se refiere a la misma propiedad. ¿Por qué nos repetimos con dos formas diferentes de hacer referencia a la misma propiedad?
  • Estamos manejando mucho la propiedad. El HasProperty tiene que manejar la propiedad para averiguar si existe y luego simplemente devuelve un Boolean resultado, dejando que el código de llamada vuelva a intentar obtener la misma propiedad para cambiar el valor.
  • Del mismo modo, estamos manejando el NewValue mas que necesario. O lo pasamos en CreateProperty o establece el Value propiedad de la propiedad.
  • El HasProperty La función asume implícitamente que el objeto tiene Properties y lo llama enlazado en tiempo de ejecución, lo que significa que es un error de tiempo de ejecución si se le proporciona un tipo de objeto incorrecto.

Usar TryGetProperty en cambio

¿Podemos hacerlo mejor? ¡Sí! Ahí es donde tenemos que mirar el patrón Try. Si alguna vez ha programado con .NET, probablemente haya visto métodos como TryParse donde en lugar de generar un error en caso de falla, podemos establecer una condición para hacer algo para el éxito y otra cosa para la falla. Pero lo más importante, tenemos el resultado disponible para el éxito. Entonces, ¿cómo mejoraríamos HasProperty? ¿función? Por un lado, debemos devolver la Property objeto. Probemos este código:

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 pocos cambios, hemos obtenido pocas victorias importantes:

  • El acceso a Properties ya no está encuadernado en tiempo de ejecución. No tenemos que esperar que un objeto tenga una propiedad llamada Properties y es de DAO.Properties . Esto se puede verificar en tiempo de compilación.
  • En lugar de solo un Boolean resultado, también podemos obtener la Property recuperada objeto, pero sólo en el éxito. Si fallamos, la OutProperty el parámetro será Nothing . Todavía usaremos el Boolean resultado para ayudar a configurar el flujo ascendente como verá en breve.
  • Nombrando nuestra nueva función con Try prefijo, estamos indicando que esto está garantizado para no arrojar un error en condiciones normales de funcionamiento. Obviamente, no podemos evitar errores de falta de memoria o algo así, pero en ese punto, tenemos problemas mucho mayores. Pero en condiciones normales de funcionamiento, hemos evitado enredar nuestro manejo de errores con el flujo de ejecución. El código ahora se puede leer de arriba a abajo sin saltar hacia adelante o hacia atrás.

Tenga en cuenta que, por convención, prefijo la propiedad "out" con Out . Eso ayuda a aclarar que se supone que debemos pasar la variable a la función sin inicializar. También esperamos que la función inicialice el parámetro. Eso quedará claro cuando veamos el código de llamada. Entonces, configuremos el código de llamada.

Código de llamada revisado usando 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

El código ahora es un poco más legible con el primer patrón Try. Hemos logrado reducir el manejo del prp . Tenga en cuenta que pasamos el prp variable en el true , el prp se inicializará con la propiedad que queremos manipular. De lo contrario, el prp queda Nothing . Entonces podemos usar CreateProperty para inicializar el prp variables.

También invertimos la negación para que el código sea más fácil de leer. Sin embargo, realmente no hemos reducido el manejo de NewValue parámetro. Todavía tenemos otro bloque anidado para comprobar el valor. ¿Podemos hacerlo mejor? ¡Sí! Agreguemos otra función:

Agregar TrySetPropertyValue procedimiento

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

Como estamos garantizando que esta función no arrojará un error al cambiar el valor, la llamamos TrySetPropertyValue . Más importante aún, esta función ayuda a encapsular todos los detalles sangrientos que rodean el cambio del valor de la propiedad. Tenemos una manera de garantizar que el valor es el valor que esperábamos que fuera. Veamos cómo se cambiará el código de llamada con esta función.

Código de llamada actualizado usando ambos TryGetProperty y 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

Hemos eliminado un If completo cuadra. Ahora podemos simplemente leer el código e inmediatamente estamos tratando de establecer un valor de propiedad y si algo sale mal, simplemente seguimos adelante. Eso es mucho más fácil de leer y el nombre de la función es autodescriptivo. Un buen nombre hace que sea menos necesario buscar la definición de la función para entender lo que está haciendo.

Crear TryCreateOrSetProperty procedimiento

El código es más legible pero todavía tenemos eso Else bloque creando una propiedad. ¿Podemos hacerlo mejor aún? ¡Sí! Pensemos en lo que necesitamos lograr aquí. Tenemos una propiedad que puede o no existir. Si no es así, queremos crearlo. Ya sea que exista o no, necesitamos que se establezca en un cierto valor. Entonces, lo que necesitamos es una función que cree una propiedad o actualice el valor si ya existe. Para crear una propiedad, debemos llamar a CreateProperty que desafortunadamente no está en las Properties sino más bien diferentes objetos DAO. Por lo tanto, debemos enlazar tarde usando Object tipo de datos. Sin embargo, todavía podemos proporcionar algunas comprobaciones de tiempo de ejecución para evitar errores. Vamos a crear una TryCreateOrSetProperty función:

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

Algunas cosas a tener en cuenta:

  • Pudimos construir sobre el anterior Try* función que definimos, que ayuda a reducir la codificación del cuerpo de la función, lo que le permite centrarse más en la creación en caso de que no exista tal propiedad.
  • Esto es necesariamente más detallado debido a las comprobaciones de tiempo de ejecución adicionales, pero podemos configurarlo para que los errores no alteren el flujo de ejecución y aún podamos leer de arriba a abajo sin saltos.
  • En lugar de lanzar un MsgBox de la nada, usamos Err.Raise y devolver un error significativo. El manejo de errores real se delega al código de llamada, que luego puede decidir si mostrar un cuadro de mensaje al usuario o hacer otra cosa.
  • Debido a nuestro manejo cuidadoso y siempre que el SourceDaoObject el parámetro es válido, toda la ruta posible garantiza que cualquier problema con la creación o el establecimiento del valor de una propiedad existente se manejará y obtendremos un false resultado. Eso afecta el código de llamada como veremos en breve.

Versión final del código de llamada

Actualicemos el código de llamada para usar la nueva función:

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

Esa fue una gran mejora en la legibilidad. En la versión original, tendríamos que analizar una serie de If bloques y cómo el manejo de errores altera el flujo de ejecución. Tendríamos que averiguar qué estaba haciendo exactamente el contenido para concluir que estamos tratando de obtener una propiedad o crearla si no existe y establecerla en un valor determinado. Con la versión actual, todo está en el nombre de la función, TryCreateOrSetProperty . Ahora podemos ver lo que se espera que haga la función.

Conclusión

Quizás se esté preguntando, “pero agregamos muchas más funciones y muchas más líneas. ¿No es mucho trabajo? Es cierto que en esta versión actual, definimos 3 funciones más. Sin embargo, puede leer cada función de forma aislada y aun así entender fácilmente lo que debe hacer. También viste que TryCreateOrSetProperty la función podría acumularse en los otros 2 Try* funciones Eso significa que tenemos más flexibilidad para ensamblar la lógica.

Entonces, si escribimos otra función que hace algo con la propiedad de los objetos, no tenemos que escribirla por completo ni copiar y pegar el código de la EditTableSubdatasheetProperty original. en la nueva función. Después de todo, la nueva función podría necesitar algunas variantes diferentes y, por lo tanto, requerir una secuencia diferente. Finalmente, tenga en cuenta que los verdaderos beneficiarios son el código de llamada que necesita hacer algo. Queremos mantener el código de llamada en un nivel bastante alto sin perder detalles que pueden ser perjudiciales para el mantenimiento.

También puede ver que el manejo de errores se simplifica significativamente, aunque usamos On Error Resume Next . Ya no necesitamos buscar el código de error porque, en la mayoría de los casos, solo nos interesa saber si tuvo éxito o no. Más importante aún, el manejo de errores no cambió el flujo de ejecución donde tiene algo de lógica en el cuerpo y otra lógica en el manejo de errores. Esta última es una situación que definitivamente queremos evitar porque si hay un error en el controlador de errores, el comportamiento puede ser sorprendente. Es mejor evitar que eso sea una posibilidad.

Se trata de abstracción

Pero el puntaje más importante que ganamos aquí es el nivel de abstracción que podemos alcanzar ahora. La versión original de EditTableSubdatasheetProperty contenía una gran cantidad de detalles de bajo nivel sobre el objeto DAO que realmente no se trata del objetivo principal de la función. Piense en los días en los que ha visto un procedimiento que tiene cientos de líneas de largo con bucles o condiciones profundamente anidados. ¿Quieres depurar eso? Yo no.

Entonces, cuando veo un procedimiento, lo primero que realmente quiero hacer es separar las partes en su propia función, para poder elevar el nivel de abstracción de ese procedimiento. Al obligarnos a impulsar el nivel de abstracción, también podemos evitar grandes clases de errores en los que la causa es que un cambio en una parte del megaprocedimiento tiene ramificaciones no deseadas para las otras partes de los procedimientos. Cuando llamamos a funciones y pasamos parámetros, también reducimos la posibilidad de que efectos secundarios no deseados interfieran con nuestra lógica.

Por eso me encanta el patrón "Prueba*". Espero que también lo encuentre útil para sus proyectos.