Boas práticas sobre exceções
O suporte a exceções se tornou comum nas linguagens de programação modernas. Este mecanismo permite tratar erros e situações inesperadas de forma muito elegante.
Sem esse recurso, as funções sinalizavam erros através do seu retorno ou pelo uso de alguma variável global, era mais ou menos assim:
const
ERRO_DIVISAO_POR_ZERO = 0;
function Dividir(Dividendo, Divisor: Integer): Extended;
begin
if Divisor = 0 then
Result := ERRO_DIVISAO_POR_ZERO
else
Result := Dividendo/Divisor;
end;
Ou seja, convencionou-se um valor que ao ser retornado pela função, indica uma condição de divisão por zero.
Toda vez que a função for chamada, devemos testar esse valor. No final, nosso código vira algo como:
procedure Teste;
begin
Retorno = FazAlgo;
if Retorno = ERRO then
begin
ShowMessage('Ocorreu um erro');
Exit;
end;
end;
Eu estou lendo o livro “The Pragmatic Progammer – From journeyman to mater” (Em português, “O Programador Pragmático – De aprendiz a Mestre”). Hoje cheguei num ponto onde ele fala justamente do que eu escrevi acima. Vou transcrever o exemplo do livro:
if (socket.read(name) != OK) {
retcode = BAD_READ;
}
else {
processName(name);
if (socket.read(address) != OK) {
retcode = BAD_READ;
}
else {
processAddress(address);
if (socket.read(telNo) != OK) {
retcode = BAD_READ;
}
else {
// etc, etc...
}
}
}
return retcode;
Como diz o livro: “A lógica normal do seu programa pode acabar sendo totalmente obscurecida por tratamentos de erros”.
Usando exceções, isso poderia ser reescrito assim: (Novamente, transcrevendo código do livro)
retcode = OK;
try {
socket.read(name);
process(name);
socket.read(address);
processAddress(address);
socket.read(telNo);
// etc, etc
}
catch (IOException e) {
retcode = BAD_READ;
Logger.log("Error reading individual: " + e.getMessage());
}
return retcode;
Eu iria um pouco mais além: Repare que a rotina acima ainda usa códigos de retorno para sinalizar erros. Ou seja, quem estiver chamando ela, terá que fazer um tratamento de condições de erro semelhante ao que vimos antes, o que poderia acabar levando ao mesmo tipo de código confuso do primeiro exemplo do livro. Eu adotaria uma das duas estratégias seguintes:
- Não capturar IOException. Ou seja, quando alguma daquelas chamadas gerar um IOException, a exceção poderia ser capturada pelo chamador da rotina. Ficaria mais ou menos assim:
public void processIndividualData(Socket socket) { socket.read(name); process(name); socket.read(address); processAddress(address); socket.read(telNo); // etc, etc } public void teste() { Socket socket = .... try { processIndividualData(socket); } catch (IOException e) { // trata a exceção aqui } } - Encapsular IOException em alguma outra exceção. Ficaria assim:
public void processIndividualData(Socket socket) { try { socket.read(name); process(name); socket.read(address); processAddress(address); socket.read(telNo); // etc, etc } catch (IOException e) { throw new OutraException(e); } } public void teste() { Socket socket = .... try { processIndividualData(socket); } catch (OutraException e) { // trata a exceção aqui } }Isso pode parecer desnecessario, mas vou explicar a importancia de esconder certas exceções:
Imagine que você implementou um DAO (Data Access Object) para abstrair a persistencia de objetos Cliente no seu sistema. Você teria uma interface assim:public interface ClienteDao { Cliente[] listarTodos(); }E digamos que você tenha uma implementação desse DAO que usa um banco de dados relacional como mecanismo de persistencia. Para acessar o banco de dados, utiliza JDBC.
public class ClienteDaoJdbcImpl implements ClienteDao { public Cliente[] listarTodos() { // Implementação aqui } }As chamadas ao JDBC podem gerar exceções do tipo SQLException. Então o seu método ClienteDaoJdbcImpl vai repassar essa exceção para o chamador? Isso significa que o usuário do seu DAO saberá que o DAO é implementado para bancos de dados relacionais. O que aconteceria se você quisesse utilizar uma implementação da interface que usa arquivos Xml para persistir as informações? Não haveria nenhum SQLException mais, mas aí como ficaria o seu código que já conta com diversos tratamentos de SQLException onde o antigo DAO era utilizado? Uma das grandes vantagens do DAO é abstrair o mecanismo de peristencia (mas isso não é assunto para este post), só que esta vantagem foi perdida nesse caso. Para resolver este problema, podemos fazer algo assim:
public class ClienteDaoJdbcImpl implements ClienteDao { public Cliente[] listarTodos() { try { // Implementação aqui } catch (SQLException e) { throw new DaoException(e); } } }Assim, não importa o que a implementação do DAO faça, você irá sempre se preocupar apenas em tratar DaoException.
Como vimos, o uso de exceções ajuda a melhorar a clareza do nosso código ao separar o tratamento de erros do fluxo normal do código.
Para finalizar, algumas recomendações:
- Jamais “engula” exceções. Se você capturou uma exceção e não é capaz de trata-la, repasse-a para o chamador
- Tenha um conjunto rico de exceções.
- Exceções de baixo nível não devem ser passadas para camadas de mais alto nível. É o exemplo do ClienteDao acima.


Leave a Reply