Access
 sql >> Base de Dados >  >> RDS >> Access

Escrevendo código legível para VBA – padrão Try*

Escrevendo código legível para VBA – padrão Try*




Ultimamente, tenho me encontrado usando o Try padrão cada vez mais. Eu realmente gosto desse padrão porque ele torna o código muito mais legível. Isso é especialmente importante ao programar em uma linguagem de programação madura como VBA, onde o tratamento de erros está entrelaçado com o fluxo de controle. Geralmente, acho que qualquer procedimento que dependa do tratamento de erros como um fluxo de controle seja mais difícil de seguir.

Cenário


Vamos começar com um exemplo. O modelo de objeto DAO é um candidato perfeito por causa de como funciona. Veja, todos os objetos DAO têm Properties coleção, que contém Property objetos. No entanto, qualquer pessoa pode adicionar propriedades personalizadas. Na verdade, o Access adicionará várias propriedades a vários objetos DAO. Portanto, podemos ter uma propriedade que pode não existir e deve lidar tanto com o caso de alterar o valor de uma propriedade existente quanto com o caso de anexar uma nova propriedade.

Vamos usar Subdatasheet propriedade como exemplo. Por padrão, todas as tabelas criadas via Access UI terão a propriedade definida como Auto , mas podemos não querer isso. Mas se tivermos tabelas criadas em código ou de alguma outra forma, talvez não tenha a propriedade. Assim, podemos começar com uma versão inicial do código para atualizar a propriedade de todas as tabelas e lidar com os dois casos.
Public Sub EditTableSubdatasheetProperty( _ Opcional 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 Definir db =CurrentDb para cada tdf em db.TableDefs If (tdf.Attributes And dbSystemObject) =0 Then If Len(tdf.Connect) =0 E (Not tdf.Name Like "~*") Then 'Not attach, or temp . Set prp =tdf.Properties(SubDatasheetPropertyName) Se prp.Value <> NewValue Then prp.Value =NewValue End If End If End IfContinue:NextExitProc:Sai 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

O código provavelmente funcionará. No entanto, para entendê-lo, provavelmente teremos que diagramar algum fluxograma. A linha Set prp = tdf.Properties(SubDatasheetPropertyName) poderia potencialmente lançar um erro 3270. Nesse caso, o controle salta para a seção de tratamento de erros. Em seguida, criamos uma propriedade e retomamos em um ponto diferente do loop usando o rótulo Continue . Existem algumas perguntas…
  • E se 3270 for aumentado em alguma outra linha?
  • Suponha que a linha Set prp =... não joga erro 3270, mas na verdade algum outro erro?
  • E se, enquanto estivermos dentro do manipulador de erros, outro erro ocorrer ao executar o Append ou CreateProperty ?
  • Esta função deve estar mostrando uma Msgbox ? Pense em funções que deveriam funcionar em algo em nome de formulários ou botões. Se as funções mostrarem uma caixa de mensagem e sairem normalmente, o código de chamada não tem ideia de que algo deu errado e pode continuar fazendo coisas que não deveria estar fazendo.
  • Você consegue dar uma olhada no código e entender o que ele faz imediatamente? Não posso. Eu tenho que olhar de soslaio para ele, então pensar sobre o que deveria acontecer no caso de um erro e esboçar mentalmente o caminho. Isso não é fácil de ler.

Adicione uma HasProperty procedimento


Podemos fazer melhor? Sim! Alguns programadores já reconhecem o problema de usar o tratamento de erros como eu ilustrei e sabiamente abstraí isso em sua própria função. Aqui está uma versão melhor:
Public Sub EditTableSubdatasheetProperty( _ Opcional 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 E (Not tdf.Name Like "~*") Then 'Not attach, 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 NextEnd SubPublic Function HasProperty(TargetObject As Object, PropertyName As String) As Boolean Dim Ignorado Como Variante No Erro Continuar Próximo Ignorado =TargetObject.Properties(PropertyName) HasProperty =(Err.Number =0)End Function 
Em vez de misturar o fluxo de execução com o tratamento de erros, agora temos uma função HasFunction que abstrai ordenadamente a verificação propensa a erros para uma propriedade que pode não existir. Como consequência, não precisamos do fluxo complexo de manipulação/execução de erros que vimos no primeiro exemplo. Esta é uma grande melhoria e torna o código um pouco legível. Mas…
  • Temos uma ramificação que usa a variável prp e temos outra ramificação que usa tdf.Properties(SubDatasheetPropertyName) que de fato se refere à mesma propriedade. Por que estamos nos repetindo com duas maneiras diferentes de referenciar a mesma propriedade?
  • Estamos lidando bastante com a propriedade. A HasProperty tem que manipular a propriedade para descobrir se ela existe, então simplesmente retorna um Boolean resultado, deixando para o código de chamada tentar novamente e obter a mesma propriedade novamente para alterar o valor.
  • Da mesma forma, estamos lidando com o NewValue mais do que o necessário. Nós o passamos no CreateProperty ou defina o Value propriedade da propriedade.
  • A HasProperty função assume implicitamente que o objeto tem uma Properties member e o chama de late-bound, o que significa que é um erro de tempo de execução se um tipo errado de objeto for fornecido a ele.

Use TryGetProperty em vez disso


Podemos fazer melhor? Sim! É aí que precisamos olhar para o padrão Try. Se você já programou com .NET, provavelmente já viu métodos como TryParse onde em vez de gerar um erro em caso de falha, podemos configurar uma condição para fazer algo para o sucesso e outra coisa para o fracasso. Mas, mais importante, temos o resultado disponível para o sucesso. Então, como podemos melhorar o HasProperty função? Por um lado, devemos retornar a Property objeto. Vamos tentar este código:
Função pública TryGetProperty( _ ByVal SourceProperties como DAO.Properties, _ ByVal PropertyName como String, _ ByRef OutProperty como DAO.Property _) As Boolean On Error Resume Next Set OutProperty =SourceProperties(PropertyName) Se Err.Number Then Set OutProperty =Nothing End If On Error GoTo 0 TryGetProperty =(Not OutProperty Is Nothing)End Function

Com poucas mudanças, conseguimos algumas grandes vitórias:
  • O acesso a Properties não é mais atrasado. Não temos que esperar que um objeto tenha uma propriedade chamada Properties e é de DAO.Properties . Isso pode ser verificado em tempo de compilação.
  • Em vez de apenas um Boolean resultado, também podemos obter a Property recuperada objeto, mas apenas no sucesso. Se falharmos, a OutProperty parâmetro será Nothing . Ainda usaremos o Boolean resultado para ajudar na configuração do fluxo, como você verá em breve.
  • Nomeando nossa nova função com Try prefixo, estamos indicando que isso é garantido para não gerar um erro em condições normais de operação. Obviamente, não podemos evitar erros de falta de memória ou algo assim, mas nesse ponto, temos problemas muito maiores. Mas sob a condição de operação normal, evitamos confundir nosso tratamento de erros com o fluxo de execução. O código agora pode ser lido de cima para baixo sem pular para frente ou para trás.

Observe que, por convenção, prefixo a propriedade “out” com Out . Isso ajuda a deixar claro que devemos passar a variável para a função não inicializada. Também esperamos que a função inicialize o parâmetro. Isso ficará claro quando examinarmos o código de chamada. Então, vamos configurar o código de chamada.

Código de chamada revisado usando TryGetProperty

Public Sub EditTableSubdatasheetProperty( _ Opcional 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 E (Not tdf.Name Like "~*") Then 'Not attach, 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 Se NextEnd Sub

O código agora está um pouco mais legível com o primeiro padrão Try. Conseguimos reduzir o manuseio do prp . Observe que passamos o prp variável no true , o prp será inicializado com a propriedade que queremos manipular. Caso contrário, o prp permanece Nothing . Podemos então usar o CreateProperty para inicializar o prp variável.

Também invertemos a negação para que o código fique mais fácil de ler. No entanto, não reduzimos o tratamento de NewValue parâmetro. Ainda temos outro bloco aninhado para verificar o valor. Podemos fazer melhor? Sim! Vamos adicionar outra função:

Adicionando TrySetPropertyValue procedimento

Função Pública 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) Fim da função IfEnd

Como estamos garantindo que essa função não gerará um erro ao alterar o valor, a chamamos de TrySetPropertyValue . Mais importante, essa função ajuda a encapsular todos os detalhes sangrentos que envolvem a alteração do valor da propriedade. Temos uma maneira de garantir que o valor seja o valor que esperávamos. Vamos ver como o código de chamada será alterado com esta função.

Código de chamada atualizado usando tanto TryGetProperty e TrySetPropertyValue

Public Sub EditTableSubdatasheetProperty( _ Opcional 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 E (Not tdf.Name Like "~*") Then 'Not attach, 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 NextEnd Sub

Eliminamos um If inteiro quadra. Agora podemos simplesmente ler o código e imediatamente que estamos tentando definir um valor de propriedade e se algo der errado, continuamos seguindo em frente. Isso é muito mais fácil de ler e o nome da função é autodescritivo. Um bom nome torna menos necessário procurar a definição da função para entender o que ela está fazendo.

Criando TryCreateOrSetProperty procedimento


O código é mais legível, mas ainda temos esse Else bloquear a criação de uma propriedade. Podemos fazer melhor ainda? Sim! Vamos pensar sobre o que precisamos realizar aqui. Temos uma propriedade que pode ou não existir. Se não, queremos criá-lo. Se já existia ou não, precisamos que seja definido para um determinado valor. Então, o que precisamos é de uma função que crie uma propriedade ou atualize o valor se ela já existir. Para criar uma propriedade, devemos chamar CreateProperty que infelizmente não está nas Properties mas sim objetos DAO diferentes. Assim, devemos vincular tarde usando Object tipo de dados. No entanto, ainda podemos fornecer algumas verificações de tempo de execução para evitar erros. Vamos criar uma TryCreateOrSetProperty função:
Função pública TryCreateOrSetProperty( _ ByVal SourceDaoObject como objeto, _ ByVal PropertyName como string, _ ByVal PropertyType como DAO.DataTypeEnum, _ ByVal PropertyValue como variante, _ ByRef OutProperty como DAO.Property _) como booleano Select Case True Case TypeOf SourceDaoObject É DAO.TableDef, _ TypeOf SourceDaoObject É DAO.QueryDef, _ TypeOf SourceDaoObject É DAO.Field, _ TypeOf SourceDaoObject É DAO.Database If TryGetProperty(SourceDaoObject.Properties, PropertyName, OutProperty) Then TryCreateOrSetProperty =TrySetPropertyValue(OutProperty, PropertyValue) Else On Erro Resume Next Set OutProperty =SourceDaoObject.CreateProperty(PropertyName, PropertyType, PropertyValue) SourceDaoObject.Properties.Append OutProperty If Err.Number Then Set OutProperty =Nothing End If On Erro GoTo 0 TryCreateOrSetProperty =(OutProperty Is Nothing) End If Case Else Err.Raise 5, , "Objeto inválido fornecido ao parâmetro SourceDaoObject. Deve ser um objeto DAO que contém um membro CreateProperty." End SelectEnd Function

Algumas coisas a serem observadas:
  • Conseguimos desenvolver o anterior Try* função que definimos, o que ajuda a reduzir a codificação do corpo da função, permitindo que ela se concentre mais na criação caso não exista tal propriedade.
  • Isso é necessariamente mais detalhado devido às verificações de tempo de execução adicionais, mas podemos configurá-lo para que os erros não alterem o fluxo de execução e ainda possamos ler de cima para baixo sem saltos.
  • Em vez de lançar uma MsgBox do nada, usamos Err.Raise e retornar um erro significativo. O tratamento de erros real é delegado ao código de chamada, que pode decidir se mostra uma caixa de mensagem ao usuário ou faz outra coisa.
  • Devido ao nosso tratamento cuidadoso e ao fornecimento de que o SourceDaoObject for válido, todo o caminho possível garante que quaisquer problemas com a criação ou configuração do valor de uma propriedade existente serão tratados e obteremos um false resultado. Isso afeta o código de chamada, como veremos em breve.

Versão final do código de chamada


Vamos atualizar o código de chamada para usar a nova função:
Public Sub EditTableSubdatasheetProperty( _ Opcional 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 E (Not tdf.Name Like "~*") Then 'Not attach, or temp. TryCreateOrSetProperty tdf, SubDatasheetPropertyName, dbText, NewValue End If End If NextEnd Sub

Isso foi uma grande melhoria na legibilidade. Na versão original, teríamos que examinar uma série de If blocos e como o tratamento de erros altera o fluxo de execução. Teríamos que descobrir o que exatamente o conteúdo estava fazendo para concluir que estamos tentando obter uma propriedade ou criá-la se ela não existir e configurá-la para um determinado valor. Com a versão atual, está tudo lá no nome da função, TryCreateOrSetProperty . Agora podemos ver o que se espera que a função faça.

Conclusão


Você pode estar se perguntando, “mas adicionamos muito mais funções e muito mais linhas. Não é muito trabalho?” É verdade que nesta versão atual, definimos mais 3 funções. No entanto, você pode ler cada função isoladamente e ainda entender facilmente o que ela deve fazer. Você também viu que o TryCreateOrSetProperty a função pode se acumular nos outros 2 Try* funções. Isso significa que temos mais flexibilidade na montagem da lógica.

Portanto, se escrevermos outra função que faz algo com a propriedade de objetos, não precisamos escrever tudo de novo nem copiar e colar o código do EditTableSubdatasheetProperty original na nova função. Afinal, a nova função pode precisar de algumas variantes diferentes e, portanto, exigir uma sequência diferente. Finalmente, tenha em mente que os verdadeiros beneficiários são o código de chamada que precisa fazer alguma coisa. Queremos manter o código de chamada de alto nível sem ficar atolado em detalhes que podem ser ruins para manutenção.

Você também pode ver que o tratamento de erros é significativamente simplificado, embora tenhamos usado On Error Resume Next . Não precisamos mais procurar o código de erro porque, na maioria dos casos, estamos interessados ​​apenas em saber se ele foi bem-sucedido ou não. Mais importante, o tratamento de erros não alterou o fluxo de execução onde você tem alguma lógica no corpo e outra lógica no tratamento de erros. A última é uma situação que definitivamente queremos evitar, porque se houver um erro no manipulador de erros, o comportamento pode ser surpreendente. É melhor evitar que isso seja uma possibilidade.

É tudo uma questão de abstração


Mas a pontuação mais importante que ganhamos aqui é o nível de abstração que agora podemos atingir. A versão original de EditTableSubdatasheetProperty continha muitos detalhes de baixo nível sobre o objeto DAO realmente não é sobre o objetivo principal da função. Pense nos dias em que você viu um procedimento com centenas de linhas com loops ou condições profundamente aninhados. Você gostaria de depurar isso? Eu não.

Então, quando vejo um procedimento, a primeira coisa que realmente quero fazer é separar as partes em sua própria função, para que eu possa aumentar o nível de abstração desse procedimento. Forçando-nos a aumentar o nível de abstração, também podemos evitar grandes classes de bugs onde a causa é que uma mudança em parte do megaprocedimento tem ramificações não intencionais para as outras partes dos procedimentos. Quando estamos chamando funções e passando parâmetros, também reduzimos a possibilidade de efeitos colaterais indesejados interferirem em nossa lógica.

Daí porque eu amo o padrão “Try*”. Espero que seja útil para seus projetos também.