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.