Boas práticas sobre exceções

June 13, 2010

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

Spam protection by WP Captcha-Free