17 de novembro de 2010

Trabalhando com Exceções em Delphi

Construir um programa que implemente regras de negócio previamente projetadas é um processo relativamente simples. As coisas começam a mudar de figura quando percebemos que não basta ir encadeando as regras pois há muitos pontos em que as operações podem falhar, seja porque um valor inválido foi informado pelo usuário, ou porque o banco de dados ficou de repente inacessível, ou um outro motivo qualquer. É natural nos preocuparmos em como o sistema deve funcionar e deixarmos de lado as exceções, isto é, aquelas situações imprevistas que não fazem parte do processo de negócio que estamos modelando para uso no computador.

Por outro lado, incluir tratamento dessas exceções diretamente no código das regras de negócio pode deixar o código complicado, abarrotado de IFs aninhados e outros desvios. Isso torna o código difícil de ser lido e de dar manutenção. Portanto, planejar desde o início de um projeto como é que o sistema deve se portar diante das situações de erro o tornará mais robusto, com um código fonte mais limpo e mais fácil de manter.

As linguagens de programação de hoje têm um mecanismo especial para tratar tais situações: as Exceptions. Com elas, podemos proteger trechos do nosso código de quaisquer tipos de erro. Se um erro for ocasionado dentro do trecho protegido, a execução do código será automaticamente desviada para um ponto que determinarmos, dando a oportunidade de tomar as providências necessárias, tais como fechar arquivos, encerrar uma transação com o banco de dados, devolver memória em uso ou notificar o usuário.

O quadro abaixo mostra a abordagem sem o uso de Exceptions para uma situação hipotética: ler um arquivo XML e importá-lo no banco de dados:
procedure TWNotaFiscal.ImportaXML (pNomeArq: String);
var lXML: TXMLDocument;
lCodCli: TCodCliente;
begin
lXML := TXMLDocument.Create(Self);

if FileExists (pNomeArq) then
begin
lXML.LoadFromFile (pNomeArq);
lCodCli := ExtrairCodCliente (lXML);

if ClienteExiste (lCodCli) then
begin
DB.StartTransaction;
CriaVinculoDbXML (lCodCli, lXML);
DB.Commit;
end
else
MostraErro ('Cliente não existe: ' + lCodCli.AsString);
end
else
MostraErro ('Arquivo não encontrado: ' + pNomeArq);

lXML.Free;
end;

O trecho de exemplo é simples mas ilustra bem como é fácil aumentar a complexidade do código ao encadear o tratamento de erros. Mesmo assim, muitas situações ainda estão desprotegidas. Por exemplo, o que aconteceria se existir um erro de formação no XML ou se o banco de dados estiver fora do ar ? No mínimo, o programa será interrompido sem que tenha chance de aplicar o Free presente na última linha da função, deixando memória sem desalocar.

Em Delphi, podemos interceptar uma exceção de 2 maneiras: usando um bloco try..except..end ou um bloco try..finally..end. Basicamente, no primeiro formato, a linha seguinte ao except é executada somente se uma exceção ocorrer no código entre o try e o except. No outro caso, o código após o finally sempre é executado, mesmo que não tenha ocorrido uma exceção entre o try e o finally.

Reescrevendo o código anterior para usar exceções:
procedure TWNotaFiscal.ImportaXML (pNomeArq: String);
var lXML: TXMLDocument;
lCodCli: TCodCliente;
begin
lXML := TXMLDocument.Create(Self);

try
lXML.LoadFromFile (pNomeArq);
lCodCli := ExtrairCodCliente (lXML);

DB.StartTransaction;
CriaVinculoDbXML (lCodCli, lXML);
DB.Commit;
except
MostraErro ('Erro na importação do XML.');
end;
lXML.Free;
end;

Agora, qualquer erro que ocorra no código direcionará o fluxo do programa para a cláusula except, que então mostrará uma mensagem. Nesta solução, a memória usada pelo XML é sempre liberada e o código ficou mais fácil de se ler.

O tipo básico de exceção possui apenas uma propriedade, que é uma descrição da situação encontrada. Podemos extrair o valor desta propriedade e exibí-lo para o usuário, melhorando o tratamento da exceção. Obter a descrição exige uma nova sintaxe:
try
{ ... }
except
on Exc : Exception do
MostraErro ('Erro na importação do XML.' + exc.Message);
end;

Essa sintaxe permite-nos recuperar a instância da classe Exception com os dados sobre a exceção ocorrida. Com ela, diferentes tipos de exceção que ocorram podem ter tratamento individualizado. Por exemplo, um erro associado ao banco de dados pode exigir a chamada a um Rollback para desfazer a transação enquanto para um erro de entrada/saída bastaria limpar algumas variáveis.
try
{ ... }
except
on ExcDB : EDatabaseError do begin
DB.RollBack;
MostraErro ('Erro no banco de dados.' + excDB.Message);
end;
on ExcIO : EInOutError do begin
_Salvou := false;;
MostraErro ('Erro no arquivo.' + excIO.Message);
end;
on Exc : Exception do
MostraErro ('Erro na importação do XML.' + exc.Message);
end;

De acordo com o tipo da exceção ocorrida, o programa é desviado para o bloco ON correspondente. Esses blocos são varridos de cima para baixo até que a classe indicada no ON seja compatível com a classe da exceção ocorrida - mais ou menos como num bloco Case..Of. Como Exception é a classe base para todas as outras exceções, um bloco ON que faça referência a este tipo deve ser colocado no final; caso contrário todas as exceções geradas serão desviadas para este bloco, sempre ignorando os demais.

E o que acontece se nenhum dos tipos tratados combinar com a exceção gerada ? A exceção é relançada como se tivesse ocorrida fora do bloco protegido; se este bloco estiver aninhado em outro bloco protegido capaz de tratar a exceção, ela será considerada normalmente; se não o programa será interrompido. Em algumas situações, mesmo após o tratamento da exceção o programa não deve mais seguir seu fluxo natural - por exemplo, não pode ter continuidade uma carga de arquivo onde o arquivo informado não existe. Consegue-se esse efeito de desvio com o comando Raise num bloco Except ou Finally, que apenas lança de novo a mesma exceção. Isso exige que esse bloco também esteja protegido e que resulte num desvio para um ponto no qual o programa possa continuar sua execução sem problemas.

Exceções podem ser geradas tanto pela VCL quanto pelo sistema operacional mas há também uma forma de as levantarmos manualmente sempre que for necessário. Podemos, então, criar nossas próprias hierarquias de exceções para representar as situações de erro do nosso sistema. Novamente, usamos a palavra chave Raise, agora acompanhada da construção de uma instância da classe de exceção, como ilustrado abaixo:
{ ... }
lCodCli := ExtrairCodCliente (lXML);
if (not ExisteCliente (lCodCli)) then
Raise EClienteNaoExiste.Create (lCodCli);

Assim, um código protegido pode testar pela ocorrência da exceção EClienteNaoExiste e tomar providências condizentes com o cenário onde ela está inserida. Veja que o constructor dela aceita um código como parâmetro ao invés do texto. Isso permite criar notificações mais completas, incluindo outras informações relevantes para auxiliar na detecção e solução de problemas inesperados dentro do sistema.

2 comentários :

Anônimo disse...

Muito bom Luís. Foi de bastante ajuda aqui.
Apesar de programar a 12 anos, não interceptava a excessão dentro do Try/Excep.

Obrigado pela dica
Wilson Rabelo - @putzmeu

Bruno disse...

Boa Matéria, parabéns!

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.