Tratamento de erros

Tratamento de erros

Doggie Daddy

Índice das lições


Nesta lição vamos mostrar alguns conceitos básicos sobre tratamento de erros no Pharo Smalltalk sem entrar em tópicos mais avançados desnecessários no cotidiano costumeiro da programação.

Tratando erros

Quando um erro ocorre o comportamento normal fora da presença de um tratamento de erros no runtime é parar a execução e abrir o debugger. O tratamento de erro, se presente, intercepta e substitui este comportamento pela execução de um bloco onde o erro é tratado.

Estrutura básica para tratamento de erro

Smalltalk permite o tratamento de erros no programa sem recorrer a códigos de retorno. O modelo típico para tratar erros é envolver o código que pode lançar (throw) um erro num bloco como abaixo:

[ 
    self umCódigoPossivelmenteComErro
] 
on: umNomeDeClasseDeErro 
do: umBlocoParaTratamentoDeErro

Mais concretamente:

Transcript open.

Transcript clear.
[ 
    2/0.
    Transcript cr; show: 'Não deve ver esta mensagem'
] 
on: ZeroDivide 
do: [ :error | Transcript cr; show: 'Divisão por zero' ]

Vamos experimentar no Playground. Execute inicialmente com Do it.

Antes abra o Transcript usando o World Menu.

Pode também abrir o Transcript executando somente a primeira linha do script acima (Transcript open.).

O texto "Divisão por zero" vai surgir no Transcript. As mensagens logo depois da expressão que causou o erro e que estão no interior do primeiro bloco envolvente não vão ser executadas. Esta é a característica da estrutura de código apresentada.

O erro provoca que a execução salte para o bloco que é argumento do do:, onde o erro deve ser tratado.

O tratamento do erro pode ser bem variado. Desde informar os detalhes sobre o erro ao código cliente que invocou o trecho com erro até corrigir o erro, caso seja possível. No nosso caso de brinquedo optamos por escrever um texto no Transcript.

Quando a execução salta para o bloco após o do: o erro é considerado "tratado" mesmo que o bloco esteja vazio de código. Esta é uma forma de "varrer o erro para debaixo do tapete".
Quando a execução chega ao bloco depois do do: o erro só existe como informação que é passada através do argumento do bloco. Ele não causa mais nenhuma interrupção com a aparição do debugger. É como se o erro nunca tivesse acontecido.

Usando dados passados pela variável do bloco de tratamento de erro

Vamos modificar nosso script para imprimir a mensagem de erro no Transcript em vez de usarmos um texto nosso. Experimente selecionar executar com Do it.

O trecho abaixo mostra uma representação em string, retornada pela mensagem asString, do erro.

No exemplo abaixo provocamos um erro ao não fornecer o valor do argumento exigido pelo bloco. Usamos a mensagem messageText (uma das mensagens a que uma instância de qualquer subclasse de Exception responde) para obter o texto que mostramos no Transcript.

Quebrei manualmente a mensagem no Transcript para não ter que ampliá-lo e tornar a imagem muito larga.

Lançando erros

Já fizemos isto nos métodos Account>>withdraw: e SpecialAccount>>withdraw:. Veja abaixo:

Nos dois casos acima usamos a mensagem self error: aString. Selecione a mensagem e use o atalho do teclado CRTL+M ou CMD+M para ver os Implementors.

Vemos que self error: aString equivale a Error new signal: aString.

O lançamento de uma exceção (exception), que é o termo mais genérico para erro (error), pode ser feito seguindo os seguintes modelos:

anError signal

ou

anError signal: aString

No código do método Object>>error: o objeto que representa o erro é uma instância da classe Error criada com Error new.

A mensagem signal ou signal: é a responsável por lançar o erro.

Vamos trocar a expressão self error: 'Saldo insuficiente' por Error new signal: 'Saldo insuficiente', que lhe é equivalente, em Account>>withdraw: e SpecialAccount>>withdraw:*.

* Uma modificação do código que não muda o seu comportamento é chamada de refatoração.

Após a refatoração os métodos ficam como abaixo.

No script abaixo exemplificamos duas formas de se lidar com a situação em que o saldo na conta é insuficiente para o valor da retirada. Na primeira o código cliente verifica se há saldo suficiente na conta. Na segunda o código cliente trata um possivel erro.

Garantindo a execução

Se você requisita recursos que depois precisa liberar a ocorrência de um erro pode comprometer esta providência. Vamos ilustrar a partir do script inicial abaixo o que acontece e o que fazer para resolver o problema.

Não se preocupe com o funcionamento da implementação de alguns códigos que vão ser usados no script abaixo. Procure compreender a funcionalidade. O que o código faz e não como faz. O principal é compreender que o código arregimentou um recurso que depois precisa liberar.
Mas se quiser saber mais sobre manipulação de arquivos leia o Chapter 3 Files with FileSystem do livro Deep into Pharo (Free).

Vamos descrever cada expressão do script acima para facilitar a compreensão (Veja também os comentários acima de cada linha).

  1. file := 'test.txt' asFileReference. cria uma FileReference para o arquivo test.txt.
  2. file exists ifTrue: [ file delete ]. testa se o arquivo já existe e o remove caso exista.
  3. stream := file writeStream. cria um fluxo (stream) abrindo o arquivo para escrita.
Numa outra lição vamos tratar de Streams e manipulação de arquivos.

4. stream nextPutAll: 'Olá Mundo!'. escreve "Olá mundo!" no arquivo.

5. stream close. fecha o arquivo. Neste momento o manipulador de arquivo fornecido pelo sistema operacional é liberado e o fechamento do arquivo garante que as últimas gravações vão ser forçadas a ir do buffer para o disco (flush).

6. file contents. retorna o conteúdo do arquivo.

Vamos usar a numeração acima para nos referir a cada linha. Os experimentos a seguir serão feitos usando o atalho do teclado CTRL+G ou CMD+G (Do it and go) para executar cada linha. O efeito é mostrar ao lado o inspetor embutido no Playground. Queremos ver os efeitos de cada linha executada de forma independente e numa ordem que mostre o problema que queremos expor.

  • Você pode execcutar em sequência as linhas 1 e 2 (Sempre com Do it and go para poder ver o resultado no inspetor embutido no Playground).
  • Depois execute a linha 3 para abrir o arquivo.
  • Em seguida execute a linha 6 para ver o conteúdo (Ainda está vazio).
  • Execute agora a linha 4 que escreve no arquivo.
  • Volte a executar a linha 6 para ver o conteúdo do arquivo (Ainda continua vazio).
  • Execute a linha 5 para fechar o arquivo.
  • E finalmente execute a linha 6 para ver o conteúdo (Agora mostra "Olá Mundo!").

Então vemos que enquanto não fechamos o arquivo o conteúdo no disco ainda não contém o texto "Olá Mundo!".

Vamos inserir um erro no meio do caminho e executar o script abaixo como um todo.

Quando a linha com a mensagem self error é executada o script é interrompido e a janela do debugger aparece. Feche esta janela e execute a linha com a mensagem file contents. Verá que o texto "Olá Mundo!" não foi gravado lá.

Do script abaixo foram removidos os comentários e o código foi submetido a um tratamento de erro. Ao executar o script como um todo vemos que o arquivo não é fechado e que a gravação do texto "Olá Mundo!" no arquivo não acontece.

Vamos ver como resolver isto com a mensagem ensure:. Modificamos o script envolvendo o código com um bloco que recebe a mensgem ensure: cujo argumento é um bloco que sempre é executado independentemente do que houve no bloco receptor. Colocamos o fechamento do arquivo no bloco que é o argumento de ensure:. Assim o arquivo é sempre fechado e não perdemos a gravação do texto "Olá Mundo!" no mesmo. Se não houver erro (Comente a mensagem self error e execute o script) mesmo assim o arquivo é fechado.

Especificando qual tipo de erro tratar e como

Você deve ter observado que a mensagem aBlock on: anErrorClass do: anErrorHandlerBlock no seu argumento que vem logo após o on: especifica uma classe. Essa classe deve ser uma classe que faz parte de uma hierarquia de classe cujo ancestral é a classe Exception.

Vamos falar dessa hierarquia para entender melhor como funciona o tratamento de erros.

Hierarquia de exceções

A hierarquia de exceções é bem extensa. Execute o script abaixo no Playground. Ele mostra no Transcript um relatório que dá uma ideia da sua extensão.

Transcript open.
Transcript clear.

tabs := -1.
subs := [ :class | 
    tabs := tabs + 1.
    tabs timesRepeat: [ Transcript tab ].
    Transcript show: class superclass name, ' subclass: ', class name.
    Transcript cr.
    class subclasses do: [ :each | 
        subs value: each.
    ].
    tabs := tabs - 1
].

subs value: Exception.
Você tambem pode ver a hierarquia diretamente no System Browser. Digite e execute a expressão Exception browse no Playground. Ou então selecionar o nome da classe em algum editor e usar o atalho de teclado CTRL+B ou CMD+B. O expediente mostrado vale para ver qualquer classe no System Browser.

Nós já vimos algumas das classes listadas em nossos exemplos nas lições passadas. As classes abaixo foram usadas explicitamente:

  • Error
  • ArgumentsCountMismatch
  • ZeroDivide

Como pegar qualquer erro

Vamos examinar a seguinte hierarquia:

Exception
    Error
        ArithmeticError
            ZeroDivide

Já usamos um script similar ao abaixo (Execute com Do it all and go pressionando o botão no alto do Playground, que automaticamente seleciona todo o script antes de executá-lo):

[  
    2/0
]
on: ZeroDivide
do: [ 'Divisão por Zero' ]

Este erro representado pela classe ZeroDivide é exatamente o erro lançado pela expressão 2/0.

Mas vamos experimentar usar outras classes da hierarquia. Experimente os vários trechos do script abaixo para ver o que acontece.

Deve ter visto que o resultado é o mesmo.

Sempre que você usa uma classe mais no topo da hierarquia ela pode "pegar" (catch) vários erros abaixo na hierarquia. A classe ArithmeticError pode, além de "pegar" o erro ZeroDivide "pega" também DomainErrorFloatingPointException. Em alguns casos isto pode ser interessante.

Um efeito dessa característica é que quando for criada uma nova classe para representar algum novo erro abaixo de ArithmeticError na hierarquia o código "pegará" também esse "novo erro aritmético".

Qual são os prós e contras de pegar qualquer erro

Os dois trechos com tratamento de erro usando as classes mais genéricas Exception e Error dão o mesmo resultado. Ambas suprimem o erro causado pelo método inexistente para responder à mensagem missingMethod.

A classe erro causado pelo método inexistente é MessageNotUnderstood, como pode ser visto com a execução do script abaixo.

Pegar o erro MessageNotUnderstood numa estrutura de tratamento de erro não é desejável pois pode suprimir a aparição do debugger para criar a oportunidade de introduzir o método que está faltando para responder à mensagem.

A classe MessageNotUnderstood é uma subclasse de Error (E também, mas não diretamente, de Exception) como pode ver abaixo. Por isso a ausência do método (MessageNotUnderstood) é pega (catched) por uma estrutura para tratamento de erro que use uma das duas classes, Exception ou Error, como argumento de on:.

Um princípio geral é que é melhor, na maioria das vezes, usar classes de erro mais específicos sempre que não houver motivo para usar classes mais perto do topo da hierarquia de exceções.

Como pegar um erro específico

Nossos métodos Account>>withdraw: e SpecialAccount>>withdraw: lançam uma instância de Error quando o saldo é insuficiente. Vamos remediar isto fazendo com que lancem uma instância de InsufficientBalance (Que ainda não criamos).

Localize os métodos conforme aprendeu em Localizando um método.

Altere os métodos para ficarem como nas imagens abaixo:

Quando for recompilar (Accept) o debugger vai ser invocado dando falta da classe InsufficientBalance.

Edite o template de definição da classe InsufficientBalance para que seja uma subclasse de Error e clique em OK.

O método de mesmo nome em SpecialAccount vai recompilar (Accept) sem problemas pois a classe InsufficientBalance já está criada se você recompilou primeiro o método Account>>withdraw:.

Vamos criar um exemplo que usa uma classe de erro mais específica. Alteramos o script para usar a classe InsufficientBalance.

script prossegue na sua execução e o debugger abre com o erro MessageNotUnderstood pois o nosso tratamento de erro só pega InsufficientBalance ou erros que sejam instâncias de subclasses de InsufficientBalance, que não é o caso de MessageNotUnderstood. Com isso ainda poderíamos criar, se quiséssesmos, o método missingMethod (Com o botão Create).

Atenção! Se você alterar o template de criação da classe e se esquecer de fazer com que a classe InsufficientBalance seja uma subclasse na hierarquia abaixo de Exception o tratamento de erro não vai funcionar. Experimente ir até à definição da classe fazê-la subclasse de Object. Depois execute o script modificado para causar o erro InsuficcientBalance. Verá algo como abaixo.

Volte à definição da classe InsuficcientBalance e a deixe de novo como abaixo:

Error subclass: #InsufficientBalance
    instanceVariableNames: ''
    classVariableNames: ''
    package: 'MyBank-Core'

Como pegar vários erros

Vamos supor que um trecho de código tenha a possibilidade de lançar dois tipos de erro. Podemos usar um script como abaixo:

[  
    { ZeroDivide new. DomainError new } atRandom signal
]
on: Error
do: [ :error | error class name ].

O script acima usa o "pega tudo" Error. Mas se quiséssemos fazer um tratamento específico para cada tipo de erro, com o que sabemos até agora, seria necessário fazer um aninhamento das estruturas. Veja abaixo.

"Estrutura externa para tratar DomainError"
[
    "Estrutura aninhada para tratar ZeroDivide"
    [  
        { 
            ZeroDivide new. 
            DomainError new 
        } atRandom signal
    ]
    on: ZeroDivide
    do: [ 'ZeroDivide' ]
]
on: DomainError
do: [ 'DomainError' ].

Mas imagine usar estrturas aninhadas para mais que duas possiblidades. A coisa vai ficar bem feia e confusa. Prejudicaria muito a legibilidade do código.

Legibilidade do código é muito importante pois cerca de 70% da atividade do programador é na leitura de código de terceiros e os seus próprios e não de escrita de código com se poderia pensar.

Vamos rescrever nossa estrutura de tratamento de exceções acrescentando mais uma subclasse de Error, como abaixo:

[  
    { 
        ZeroDivide new. 
        DomainError new. 
        DateError new 
    } atRandom signal
]
on: ZeroDivide, DomainError, DateError
do: [ :error | error class name ]

Você pode notar que agora basta listar as classes de erro separadas por ,. Assim você evita um longo aninhamento.

O tratamento de exceções é um assunto bem complexo e caso queira saber mais leia o Chapter 13 Handling Exceptions do livro Deep into Pharo (Free). Tocamos os tópicos principais e acreditamos que o restante será raramente requisitado. Mas se você precisar de algo que não está nesta lição podemos preparar uma lição especial.

Encerrando

Encerre salvando a imagem.




Report Page