Introdução a Módulos de Classe no VBA

O VBA suporta o uso de classes através de componentes chamados módulos de classe. Classes são usadas para criar objetos.

Introdução

Uma analogia bastante utilizada é dizermos que se um bolo é um objeto, sua receita é uma classe. Seguindo o raciocínio, é possível fazer vários bolos a partir de uma receita, ou vários objetos a partir de uma classe. Uma classe não aloca memória em tempo de execução, e um objeto sim, já que a classe possui apenas a definição do objeto que cria.

Uma classe descreve as propriedades e métodos de um objeto. Propriedades podem ser entendidas como características de um objeto, e métodos, ações que o mesmo promove. Por exemplo, considere o objeto Carro. Entre suas propriedades, podemos citar cor, quilometragem, chassi, marca, modelo, etc. Os métodos poderiam ser ações como dar a partida, acionar o para-brisa, frear, buzinar. Se fizermos a analogia que uma propriedade é um adjetivo, certamente um método é um verbo.

Declarar, Instanciar e Destruir um Objeto a Partir de uma Classe

Os objetos possuem um ciclo de vida. Primeiro, devem ser declarados. Então, são criados (tecnicamente é melhor falar instanciados), depois são utilizados (ou consumidos) e, for fim, são destruídos.

No VBA, um objeto é instanciado no momento em que a palavra chave New é utilizada.

Sub CriarObjeto()
 
    'Declarar:
    Dim oCarro As clsCarro
    
    'Instanciar:
    Set oCarro = New clsCarro
    
    'Consumir:
    '...
    
    'Destruir:
    Set oCarro = Nothing
 
End Sub

oCarro é o objeto e clsCarro é a classe. Repare que quando trabalhamos com objetos, as atribuições devem ser feitas com o uso da palavra chave Set, ao contrário de tipos de dados simples como Long, Integer, Date, etc., em que o uso da igualdade dá valor à variável.

Para que o exemplo acima funcione, é necessário que criemos a classe clsCarro no nosso projeto VBA:

Embora não seja obrigatório, é altamente recomendável destruir todos os objetos criados ao término da execução do seu programa. O gerenciamento de memória e coletor de lixo do VBE não são bons, e ao adotar essa prática você minimiza erros inesperados e até crashes no Excel.

Consumir Objetos

O exemplo a seguir mostra como acessar membros de um objeto de uma classe chamada clsEmpregado. Pelo código, podemos ver que a classe define três propriedades: Nome, Endereço e Salário. Cole o num Módulo de Classe chamado clsEmpregado:

'Esta Classe define três propriedades: Nome, Endereço e Salário.
'Seus valores são armazenados, respectivamente,
'nas variáveis aNome, aEndereço e aSalário.
 
Private aNome As String
Private aEndereço As String
Private aSalário As Double
    
'Abaixo seguem as declarações de propriedade de como são lidas e gravadas.
'Propriedade Nome:
Property Get Nome() As String
    Nome = aNome
End Property
Property Let Nome(pNome As String)
    aNome = pNome
End Property
 
'Propriedade Endereço:
Property Get Endereço() As String
    Endereço = aEndereço
End Property
Property Let Endereço(pEndereço As String)
    aEndereço = pEndereço
End Property
 
'Propriedade Salário:
Property Get Salário() As Double
    Salário = aSalário
End Property
Property Let Salário(pSalário As Double)
    aSalário = pSalário
End Property	
 
'Propriedade somente para leitura SalárioAnual:
Property Get SalárioAnual() As Double
    SalárioAnual = Salário * 12
End Property
 
Public Sub MostrarFolhaDePagamento()
    Dim sMensagem As String
    
    sMensagem = sMensagem & "Nome: " & Nome & vbCrLf
    sMensagem = sMensagem & "Data atual: " & Date & vbCrLf
    sMensagem = sMensagem & vbNewLine
    sMensagem = sMensagem & "Salário Mensal: " & Salário & vbCrLf
    sMensagem = sMensagem & "Salário Anual: " & SalárioAnual & vbCrLf
    MsgBox sMensagem, vbInformation
End Sub

Para cada uma das propriedades, há um procedimento Get e outro Let. Get é chamado quando se deseja ler o valor de uma propriedade e Let é chamado quando se deseja atribuir um valor a uma propriedade.

Para vermos nosso programa em funcionamento, devemos criar num módulo (comum, e não de classe) o seguinte código:

Sub UsoBásicoDeClasses()
    Dim oEmpregado As clsEmpregado
    Set oEmpregado = New clsEmpregado
    
    oEmpregado.Nome = "Felipe"
    oEmpregado.Endereço = "Rua Jardim, 20/603"
    oEmpregado.Salário = 1000
    
    Debug.Print "Nome: " & oEmpregado.Nome
    Debug.Print "Endereço: " & oEmpregado.Endereço
    Debug.Print "Salário: " & oEmpregado.Salário
    
    Set oEmpregado = Nothing
End Sub

Observe que ao escrever oEmpregado aparece o intellisense com todas as propriedades do objeto:

A vantagem imediata em usar classes é ter uma visualização completa dos membros de um objeto, além do ganho obtido em tempo de desenvolvimento.

Ao executar essa rotina, teremos como resultado na janela de verificação imediata (Ctrl+G):

Nome: Felipe
Endereço: Rua A, 12/901
Salário: 1000

Uso do With

Ao trabalhar com suas classes personalizadas, você pode usar o bloco With com os objetos que você criar:

Sub UsoBásicoDeClassesComWith()
    Dim oEmpregado As clsEmpregado
    
    Set oEmpregado = New clsEmpregado
    With oEmpregado
        .Nome = "Felipe"
        .Endereço = "Rua Jardim, 20/603"
        .Salário = 1000
        
        Debug.Print "Nome: " & .Nome
        Debug.Print "Endereço: " & .Endereço
        Debug.Print "Salário: " & .Salário
    End With
    
    Set oEmpregado = Nothing
End Sub

Atributos vs Propriedades vs Parâmetros

Dentro da classe, podemos observar, por exemplo Endereço, aEndereço e pEndereço. O que está havendo? Para definir uma simples propriedade, utilizei três membros diferentes com nomes semelhantes. Eles são necessários. Vamos rever a definição de atribuição de propriedade Endereço:

Property Let Endereço(pEndereço As String)
    aEndereço = pEndereço
End Property

Endereço é o nome da propriedade do objeto. A declaração de uma propriedade se assemelha bastante com a declaração de um subprocedimento e, como sabemos, um procedimento por si só não conseguem armazenar um valor após o término de sua execução. É para isso que utilizamos uma variável para armazenar o valor da propriedade, que é representada aqui por aEndereço (A letra a representa atributo). Ela é uma variável de nível de módulo, e retém os valores de um objeto enquanto o objeto estiver alocado na memória. Então, a propriedade Endereço é uma espécie de canal de comunicação entre a variável interna aEndereço e o ambiente externo.

pEndereço (A letra p representa parâmetro) é nada mais que o parâmetro de entrada do procedimento objeto. Ele possui o valor externo que será processado pela procedimento e atribuído à variável aEndereço. Por fim:

Property Get Endereço() As String
    Endereço = aEndereço
End Property

Endereço, nesse contexto, funciona igual uma função, que retorna o valor do atributo aEndereço ao procedimento que chamou a propriedade Endereço.

Simplificando Declaração de uma Propriedade

Se você não precisar inserir código no procedimento Get e Let de uma variável, você pode declarar a variável como pública que o VBE irá considera-lo como uma variável. Em outras palavras, o bloco de código:

Private aNome As String
 
Property Get Nome() As String
    Nome = aNome
End Property
Property Let Nome(pNome As String)
    aNome = pNome
End Property

É equivalente a simplesmente:

Public Nome As String
Note que ao utilizar a palavra-chave Public numa variável que está dentro de um módulo de classe, ela não é pública para todo projeto, como ocorre em declarações em módulos regulares. Public em módulos de classe significa que a variável declarada pode ser acessada por procedimentos que não estejam dentro da classe.

Propriedades Somente Como Leitura

No exemplo anterior, todas as propriedades possuíam a declaração Let e Get.

É possível criar uma propriedade somente como leitura. Para tal, poderíamos definir, por exemplo, uma propriedade chamada SalárioAnual, que seria dada pelo produto do salário mensal e 12:

'Propriedade somente para leitura SalárioAnual:
Property Get SalárioAnual() As Double
    SalárioAnual = Salário * 12
End Property
Note que poderíamos ter utilizado a variável de módulo (que aqui tem a função de atributo) aSalário ao invés da propriedade Salário. No entanto, as boas práticas de programação dizem que quando estamos numa classe, devemos utilizar o valor retornado pela propriedade, e não a variável que armazena o valor da propriedade. Essa boa prática tem um custo que é tornar a depuração do código mais trabalhosa, uma vez que todo acesso à propriedade Salário desvia o código para seu procedimento respectivo. Por outro lado, ao usar o valor da propriedade, você terá garantido que o valor atribuído já foi processado e validado por sua classe.

O fato dessa propriedade não possuir a instrução Let é o que a caracteriza somente como leitura, tornando impossível fazer uma atribuição direta a ela.

Restringindo Valores de Entrada de uma Propriedade

Uma grande vantagem em usar classes é possibilidade de tratar os dados de entrada e saída da classe. Para o exemplo da classe clsEmpregado, vamos adicionar código para ser impossível atribuir um valor negativo à propriedade Salário. Poderíamos adaptar sua propriedade Let na forma mostrada abaixo:

Property Let Salário(d As Double)
    'Restringir valores de entrada para Salário:
    If pSalário > 0 Then
        aSalário = pSalário
    Else
        'O valor do salário deve ser maior que zero!
        aSalário = 1
    End If 
End Property

Criar Métodos na Classe

Até agora foi mostrado apenas como trabalhar com propriedades nas classes. Você pode definir métodos também. Acrescente o bloco de código abaixo na classe clsEmpregado:

Public Sub MostrarFolhaDePagamento()
    Dim sMensagem As String
    
    sMensagem = sMensagem & "Nome: " & Nome & vbCrLf
    sMensagem = sMensagem & "Data atual: " & Date & vbCrLf
    sMensagem = sMensagem & vbNewLine
    sMensagem = sMensagem & "Salário Mensal: " & Salário & vbCrLf
    sMensagem = sMensagem & "Salário Anual: " & SalárioAnual & vbCrLf
    MsgBox sMensagem, vbInformation 
End Sub

Agora, coloque o código abaixo num módulo comum e execute:

Sub ExemploMétodo()
    Dim oEmpregado As clsEmpregado
    
    Set oEmpregado = New clsEmpregado
    oEmpregado.Nome = "Felipe"
    oEmpregado.Endereço = "Rua Jardim, 20/603"
    oEmpregado.Salário = 1000
    oEmpregado.MostrarFolhaDePagamento
    
    Set oEmpregado = Nothing
End Sub

O resultado será:

Referenciando Propriedades e Métodos da Classe

Quando escrevemos código dentro de uma classe, ao invés de escrever Nome, Salário e SalárioAnual, podemos escrever Me.Nome, Me.Salário e Me.SalárioAnual. Em outras palavras, no contexto dentro de uma classe, Me se refere a ela mesma. Uma vantagem de se usar o Me é o fato de aparecer o intellisense com todos os membros (isto é, propriedades, métodos, constantes e enumerações) da classe:

Note que ícones de propriedades são diferentes de ícones de métodos.

Eventos Padrões de uma Classe

Toda classe no VBA possui dois eventos padrões, de nome fixo. Um chama-se Class_Initialize, e é executado quando um objeto é instanciado. O outro, Class_Terminate, é executado quando o objeto é destruído. Você não é obrigado a colocar código nesses dois eventos.

Para exemplificar, crie uma classe chamada clsCasa com o código abaixo:

Private aEndereço As String
Private aDólares As Double
 
'Eventos
Private Sub Class_Initialize()
    Me.Endereço = "Rua das Flores, 105"
End Sub
Private Sub Class_Terminate()
    MsgBox "Um objeto cuja propriedade Endereço é " & Me.Endereço & " foi destruído.", vbInformation
End Sub
 
'Propriedades
Property Get Endereço() As String
    Endereço = aEndereço
End Property
Property Let Endereço(pEndereço As String)
    aEndereço = pEndereço
End Property
 
Property Get Dólares() As Double
    Dólares = aDólares
End Property
Property Let Dólares(pDólares As Double)
    aDólares = pDólares
End Property

Num módulo regular, coloque o código abaixo:

Sub ExemploEventos()
    Const VALOR_DÓLAR_DO_DIA = 3.03
    
    Dim oCasa As clsCasa
    
    'O evento Class_Initialize é chamado quando se cria o objeto:
    Set oCasa = New clsCasa
    
    oCasa.Dólares = 150000 * VALOR_DÓLAR_DO_DIA
    
    'O evento Class_Terminate é chamado quando se destrói o objeto:
    Set oCasa = Nothing
End Sub
Se você retirar a instrução Set oCasa = Nothing do código acima verá que o ponto de execução desviará de End Sub para o procedimento destrutor do objeto (ou seja, foi destruído implicitamente). Pode-se argumentar então que destruir um objeto explicitamente é desnecessário. Volto a insistir que a melhor forma de destruir um objeto é explicitamente, e que em muitos casos você, como desenvolvedor, terá menos problemas em sistemas mais complexos e melhor gerenciamento de memória. A título de exemplo, existem alguns cenários de crashes no Excel em formulários quando os mesmos não são destruídos explicitamente. Infelizmente, pelo fato do problema ser também de design do VBE, não consigo descrever um passo a passo para reproduzir esse tipo de problema. Além disso, um bom programa é aquele que encerra quando a instrução End Sub do método pai é executada, sem disparar toneladas de coletores de lixo dos objetos pendurados na memória.

Normalmente uso o evento Class_Initialize para definir valores iniciais e padrões de um objeto. No exemplo acima, ao criar um objeto clsCasa, define-se automaticamente que a propriedade Endereço do mesmo é Rua das Flores, 105.

Nesse sentido, você pode usar esse evento para definir propriedades padrão ao criar um objeto. Suponha que você crie vários objetos de uma classe clsVeículo para usar num ambiente em que os veículos são, predominantemente, carros. Se existir uma propriedade chamada QuantidadeRodas, você poderia atribuir 4 à essa propriedade dentro do evento Class_Initialize, e atribuir explicitamente oVeículo.QuantidadeRodas = 2 fora da classe apenas nos casos de quando o veículo é uma moto.

O Class_Terminate é utilizado para colocar códigos de limpeza no ato da destruição de um objeto. No nosso exemplo, mostra-se apenas uma notificação de que o objeto foi destruído.

Os Problemas da Auto Instanciação

Alternativamente, você pode criar um objeto dessa forma:

Sub CriarObjetoFormaNãoRecomendada()
 
    'Declarar e instanciar:
    Dim oCarro As New clsCarro
    
    'Consumir
    '...
    
    'Destruir
    Set oCarro = Nothing
 
End Sub

Foi feita a declaração e criada uma instância do objeto numa única instrução. O nome dessa técnica é auto instanciação de variável. No VBA, não é recomendável utilizá-la por dois motivos:

  • Aumenta o overhead do código porque cada chamada a um objeto criado dessa classe irá disparar o evento de inicialização do mesmo. Ao criar um objeto dessa forma e fazer uma simples atribuição como, por exemplo, oCarro.Cor = "Verde", o evento Class_Initialize será disparado, e isso é altamente indesejável.
  • Não há como testar se uma variável criada desse tipo é Nothing porque a própria instrução de teste irá criar uma instância do objeto, retornando, então, sempre False para o teste. Nesse exemplo, o teste If oCarro Is Nothing Then... sempre irá passar.

Criar mais de um Objeto com uma Classe

No exemplo a seguir, foram criados três objetos do mesmo tipo, mas com propriedades diferentes:

Sub VáriosObjetos()
    Dim oEmp1 As clsEmpregado
    Dim oEmp2 As clsEmpregado
    Dim oEmp3 As clsEmpregado
 
    Set oEmp1 = New clsEmpregado
    Set oEmp2 = New clsEmpregado
    Set oEmp3 = New clsEmpregado
    
    oEmp1.Nome = "Felipe"
    oEmp1.Endereço = "Rua Jardim, 20/603"
    oEmp1.Salário = 1000
    '---
    oEmp2.Nome = "Renata"
    oEmp2.Endereço = "Praça das Flores, 305"
    oEmp2.Salário = 1500
    '---
    oEmp3.Nome = "Rodrigo"
    oEmp3.Endereço = "Av. Castro, 50/101"
    oEmp3.Salário = 2000
    
    oEmp1.MostrarFolhaDePagamento
    oEmp2.MostrarFolhaDePagamento
    oEmp3.MostrarFolhaDePagamento
    
    Set oEmp1 = Nothing
    Set oEmp2 = Nothing
    Set oEmp3 = Nothing
End Sub

O VBA não mistura os valores das propriedades (na verdade atributos) dos objetos criados. Cada objeto aloca espaço na memória para guardar os próprios valores de cada propriedade.

Criar uma Coleção de Objetos

Você pode adicionar objetos em coleções. Suponha que você tenha a tabela abaixo:

Use o código a seguir para criar um objeto por linha da tabela, povoar as propriedades da tabela de acordo as colunas respectivas e, em seguida, mostrar os dados dos objetos na janela de verificação imediata:

Sub ColeçãoDeObjetos()
    Dim tblEmpregados As ListObject
    Dim oEmpregado As clsEmpregado
    Dim cEmpregados As Collection
    Dim iListRow As ListRow
    Dim iItem As Long
 
    'Definir tabela de Empregados
    Set tblEmpregados = ThisWorkbook.Worksheets("Coleções").ListObjects("Empregados")
    
    'Inicializar Coleção?
    Set cEmpregados = New Collection
    
    'Criar e popular objetos
    For Each iListRow In tblEmpregados.ListRows
        Set oEmpregado = New clsEmpregado
        oEmpregado.Nome = iListRow.Range(1)
        oEmpregado.Endereço = iListRow.Range(2)
        oEmpregado.Salário = iListRow.Range(3)
        
        'Adiciona objeto à coleção:
        cEmpregados.Add oEmpregado
    Next iListRow
    
    'Mostrar resultados
    For Each oEmpregado In cEmpregados
        Debug.Print oEmpregado.Nome, oEmpregado.Endereço, oEmpregado.Salário
    Next oEmpregado
    
    'Ou, com laço do tipo For...Next
    For iItem = 1 To cEmpregados.Count
        Set oEmpregado = cEmpregados(iItem)
        Debug.Print oEmpregado.Nome, oEmpregado.Endereço, oEmpregado.Salário
    Next iItem
        
End Sub

Você pode remover um item da coleção se desejar. A instrução abaixo remove o terceiro item da coleção do nosso exemplo:

cEmpregados.Remove 3

Gerador de Propriedades

Usando um “código para gerar código”, você pode tornar menos moroso o processo de escrever declarações de propriedades:

Sub GerarPropriedade()
    Dim sAtributo As String
    Dim sInstrução As String
    Dim sParâmetro As String
    Dim sPropriedade As String
    Dim sSaída As String
    Dim sTipoDeDados As String
    Dim vInstruções() As String
    
    sInstrução = InputBox(Prompt:="Digite a linha de declaração do atributo:", _
                          Default:="Private aNome As String")
    If sInstrução = "" Then Exit Sub
    
    vInstruções = Split(sInstrução)
    sAtributo = vInstruções(1)
    sPropriedade = Mid(sAtributo, 2)
    sParâmetro = "p" & sPropriedade
    sTipoDeDados = vInstruções(3)
    
    sSaída = ""
    sSaída = sSaída & "Property Get " & sPropriedade & "() As " & sTipoDeDados & vbNewLine
    sSaída = sSaída & vbTab & sPropriedade & " = " & sAtributo & vbNewLine
    sSaída = sSaída & "End Property" & vbNewLine & vbNewLine
    sSaída = sSaída & "Property Let " & sPropriedade & "(" & sParâmetro & " As " & sTipoDeDados & ")" & vbNewLine
    sSaída = sSaída & vbTab & sAtributo & " = " & sParâmetro & vbNewLine
    sSaída = sSaída & "End Property" & vbNewLine
    Debug.Print sSaída
End Sub

Se você entrar Private aNome As String na janela, obterá as declarações Get e Let na janela de verificação imediata:

Property Get Nome() As String
    Nome = aNome
End Property
Property Let Nome(pNome As String)
    aNome = pNome
End Property

Você pode criar versões mais sofisticadas desse código, como por exemplo: obter o esquema de uma tabela de um banco de dados e gerar classes no VBE mapeando cada um dos campos a uma propriedade. Use a criatividade. Não deixe de verificar também o MZ Tools, que possui um ótimo assistente para criar propriedades em classes.

Referências

Chip Pearson

Download

Para fazer download do arquivo de exemplo deste artigo, clique aqui.

Sobre Felipe Gualberto

Microsoft Most Valuable Professional (MVP) de Excel.
Esta entrada foi publicada em Tutoriais e marcada com a tag , , , , , , , , , . Adicione o link permanente aos seus favoritos.