Uma abordagem para evitar NullPointerException

Invocar métodos que podem ou não retornar o objeto esperado é algo bem comum no nosso dia-a-dia de programação. Abaixo segue um exemplo bem simples:

public class Agenda {

private Map contatos = new HashMap();

public void adiciona(Contato contato) {
contatos.put(contato.getEmail(), contato);
}

public Contato buscaPor(String email){
return contatos.get(email);
}
}

O método que realiza a busca retorna o contato encontrado ou, no meu caso, retorna null caso não encontre nada. Agora imagine que temos uma classe que faz uso dessa agenda. Por exemplo a que segue abaixo:

public class TesteAgenda {

public static void main(String[] args) {
Agenda agenda = new Agenda();
agenda.adiciona(new Contato("alberto","teste@teste.com.br"));

Contato contato = agenda.buscaPor("teste2@teste.com.br");
System.out.println(contato.getNome());
}
}

Mesmo sendo um exemplo muito simples, caimos num problema bem comum. O método buscaPor(email) vai retornar null porque não encontrou nada e o cliente que está usando o código toma o velho NullPointerException. No caso exposto, o código que utiliza este tipo de método, e eles se espalham que nem uma praga pelo sistema, começa a defender-se utilizando as velhas checagens com ifs para verificar se o objeto retornado é null ou não. Abaixo o exemplo:

if(contato!=null){
System.out.println(contato.getNome());
}
else{
System.out.println("Não encontrado");
}

O grande mal dessa abordagem é que você tem que ficar lembrando o tempo todo de ter que verificar isso. Exato, o problema não é colocar os ifs, if ou não, algum código vai ter de ser colocado para verificar isso.

Uma abordagem para tentar minimizar o problema da lembrança é usar o pattern Null Object, onde um objeto com semântica Nula é retornado para o invocador do método. É uma maneira um pouco mais elegante de resolver o problema.  Por exemplo, para o caso da agenda, poderia fazer uma classe chamada ContatoNaoEncontrado. O exemplo segue abaixo:

public class ContatoNaoEncontrado extends Contato {

@Override
public String getNome() {
return "Não encontrado";
}

@Override
public String getEmail() {
return "Não encontrado";
}

}

Fariamos uma alteração no método buscaPor(email) para que faça a verificação, ficando com ele da seguinte forma:

public Contato buscaPor(String email){
Contato contato = contatos.get(email);
if(contato!=null){
return contato;
}
return new ContatoNaoEncontrado();
}

E no código que utiliza este método podemos substituir os ifs pelo classico polimorfismo:

Contato contato = agenda.buscaPor("teste2@teste.com.br");
System.out.println(contato.getNome());
System.out.println(contato.getEmail());

Caso a checagem ainda seja necessária, pode-se adicionar um método na classe mãe indicando se foi encontrado ou não. Dessa forma o Null Object sempre retornaria false. Aplicando essa alteração ficamos com o seguinte código:

if(contato.foiAchado()){
System.out.println("Achamos seu contato... => "+contato.getNome());
}
else{
System.out.println("Não foi possivel...");
}

O ponto, um tanto quanto questionavel, é o fato de usar herança para implementar o Null Object. Poderia ser utilizado a mesma classe setando tudo para os valores apropriados, mas aí seria perdido a idéia da semântica do objeto nulo.

Pelo menos na minha opinião, não existe uma regra certinha para desenvolver, a que tento seguir é a de sempre deixar o código mais auto-explicativo possivel e de fácil manutenção. Caso você tenha apenas um lugar para fazer a checagem do objeto nulo, é claro que não precisa gastar seu tempo implementando isso. Agora, se essa checagem começar a se espalhar muito, essa abordagem pode salvar você de tomar uns NPE pelo caminho. No próximo post vou mostrar a mesma abordagem utilizando a linguagem Scala, a qual já possui implementado uma abordagem para resolver este mesmo problema.

Advertisements