/ Getting Started

Clean Code - Tratamento de erros

Olá pessoal, este é nosso último artigo sobre o Clean Code. Neste artigo são ressaltadas uma série de técnicas e considerações que você pode usar para criar um código que seja limpo de robusto, que trate erros com elegância e estilo. Vamos ao nosso primeiro tópico.

Use exceções ao invés de retornar código de erro

No passado haviam muitas linguagens que não suportavam exceções, forçando os desenvolvedores a utilizar blocos de comparação para determinar o tratamento e alerta sobre erros. Neste período você criava uma flag de erro ou retornava um código de erro que fosse possível verificar na chamada.

Essas técnicas sobrecarregam as chamadas, que devia verificar erros imediatamente após a chamada. Por esse motivo é melhor lançar uma exceção quando um erro for encontrado, o código de chamada fica mais limpo e sua lógica não é ofuscada pelo tratamento de erro.

Ruim:

public class DeviceController {
    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        // Check the state of the device
        if (handle != DeviceHandle.INVALID) {
            // Save the device status to the record field
            retrieveDeviceRecord(handle);
            // If not suspended, shut down
            if (record.getStatus() != DEVICE_SUSPENDED) {
                pauseDevice(handle);
                clearDeviceWorkQueue(handle);
                closeDevice(handle);
            } else {
                logger.log("Device suspended. 
                Unable to shut down");
            }
        } else {
            logger.log("Invalid handle for: " 
            + DEV1.toString());
          }
     }
}

Bom:

public class DeviceController {
    public void sendShutDown() {
        try {
            tryToShutDown();
        } catch (DeviceShutDownError e) {
            logger.log(e);
          }
        }
    private void tryToShutDown() 
    throws DeviceShutDownError{
        DeviceHandle handle = getHandle(DEV1);
        DeviceRecord record = retrieveDeviceRecord(handle);
        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }
        private DeviceHandle getHandle(DeviceID id) {
        throw new DeviceShutDownError("Invalid handle 
        for: " + id.toString());
    }
}

Crie primeiro sua estrutura try-catch-finally

Uma das coisas mais interessantes sobre exceções é que elas definem um escopo dentro do seu código. O bloco try funciona como uma transação que pode ser cancelada a qualquer momento e deve continuar no bloco catch. O bloco catch deve deixar seu programa num estado mais consistente sem se importar com o que aconteça no try. Por isso é recomendável que as funcionalidades que possuem a probabilidade de gerar erros sejam iniciadas por blocos try-catch, isso ajuda a definir o que o usuário deve esperar, independente do que ocorre no try.

As IDEs atualmente auxiliam nesta tarefa, quando é necessário utilizar ações da linguagem que possuem uma grande probabilidade de ocorrer uma exceção, a IDE recomenda a utilização dos blocos try-catch ou lança a exceção para tratamento posterior. Aceite sempre essas recomendações.
Defina o fluxo normal

Tudo no seu código se inicia em um papel ou documento de texto, os pensamentos sobre módulos, funcionalidades, regras de negócio, classes e os fluxos do programa. Por esse motivo deve ser bem claro o que será permitido ao seu usuário, por isso devemos manipular e criar exceções segundo as necessidades das chamadas.

O padrão de caso especial, criado por Martin fowler, define a criação de exceções para o tratamento de um caso especial para você, que saiba lidar com comportamentos diferentes e não seja transmitido ao cliente.

A partir da definição do fluxo normal, muito provavelmente você terá visão sobre os possíveis fluxos alternativos. Para estes fluxos crie exceções personalizadas e não as ignore. Sempre tenha um planejamento de como contornar estes fluxos alternativos sem que o usuário seja atingido, nunca resolva a exceção apenas com um System.out.println() ou um console.log().

Caso não tenha uma alternativa para tratar o erro durante a execução, dispare um alerta com a exceção para algum responsável pelo sistema e tenha uma abordagem para amenizar o erro.

Ruim:

try {
  FuncaoDeExcecao();
} catch (error) {
  console.log(error);
}

Bom:

try {
  FuncaoDeExcecao();
} catch (error) {
  // Uma opção (mais chamativa que console.log):
  console.error(error);
  // Outra opção:
  NotificarUsuario(error);
  // Outra opção:
  ReportarParaOServico(error);
  // OU as três!
}

Não passe ou retorne Null

Quando retornamos null estamos criando problemas para a execução, basta esquecer uma verificação e tudo irá por água abaixo. Verifique o seguinte trecho de código:

public void registerItem(Item item) {
    if (item != null) {
        ItemRegistry registry = persistentStore
        .getItemRegistry();
        if (registry != null) {
            Item existing = registry
            .getItem(item.getID());
            if (existing.getBillingPeriod()
            .hasRetailOwner()) {
                existing.register(item);
            }
        }
    }
}

Em uma primeira visualização não identificamos nenhum erro, porém analisando melhor podemos notar que o primeiro bloco if faz a instância de um objeto ItemRegistry, e se caso o persistentStore for null? Teríamos uma NullPointerException em tempo de execução que seria capturada em um nível mais alto, ou não.

E, na verdade o problema do código não é a verificação sobre elementos null e sim necessitar o controle da mesma regra para vários elementos, para situações como essa cria um objeto de caso especial e submeta seus elementos lançando as exceções necessárias.

Não passe null, a menos que esteja trabalhando com uma API que espere receber null. Passando valores null a única certeza que temos é de que será lançada uma NullPointerException. Na maioria das linguagens não há uma boa forma de lidar com valores nulos passados acidentalmente para uma chamada.

Utilize Exceções não verificadas

As exceções checadas surgiram com a primeira versão do java, de fato era uma ótima ideia carregar uma lista de exceções juntamente com a assinatura de um método. Desta maneira todas as assinaturas de exceções conhecidas pelo sistema serão notificadas ao usuário sempre que fossem necessárias. Você nota todas as vezes que precisou usar throws IOException?

O preço de se utilizar exceções checadas é a necessidade de assinalar ao Caller todas as vezes que for utilizar o método, assim poluindo a interface. Uma saída para o problema em utilizar exceções checkadas é transformá-las em exceções não checadas por meio da RuntimeException.
RuntimeException são subclasses de Exception porém são tratadas durante a execução e não na compilação.

Exemplo:

public class BancoDadosException extends RuntimeException {
	private static final long serialVersionUID = -7557684255525324440L;
	
    public BancoDadosException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

Após utilizar essa classe de exceção podemos apenas lançar exceções nos blocos try/catch.

try{
    ...
}

catch(Exception e){
    throw new BancoDadosException('mensagem de erro', e);
}

Essa melhoria permite que a exceção só seja chamada neste trecho e não seja necessária nas classes de níveis mais baixos que a utilizam.

Conclusão

Um código limpo precisa ser legível e robusto, esses atributos não são conflitantes. Podemos criar softwares limpos e robustos com a visão de que o tratamento de erro é uma preocupação à parte, algo que seja visível independente da nossa lógica principal.

Referências

Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship.
Disponível em https://www.investigatii.md/uploads/resurse/Clean_Code.pdf.

Augusto, Felipe. Conceitos de Código Limpo.
Disponível em https://github.com/felipe-augusto/clean-code-javascript#Índice