Type-safe null usando Option


Reconhecendo o erro bilionário

Em 1965, Sir Charles Antony Richard Hoare (Tony Hoare para os chegados), inventava o null, as referências nulas, enquanto projetava a linguagem orientada a objetos ALGOL W.

Muitos anos depois, em 2009 ele mesmo retoma esse fato em uma palestra:

I call it my billion-dollar mistake.

Ele chama o null de seu erro bilionário! Fonte de inúmero erros, vulnerabilidades e problemas em sistemas no mundo todo. De fato uma grande cagada!

O Problema e sua Consequências

Certamente o null é tratado com muita cautela por nós programadores quando escrevemos nosso código. Sendo profissionais cautelosos, sempre pensamos que uma ou outra referência pode estar nula, desconfiados de toda e qualquer variável.

O programador Java incauto, faria coisas do tipo:

Animal elefante = animais.get("elefante"); 
elefante.anda(); // Pode dar merda
Pessoa p = PessoaDAO.findByName("Felipe");
Documento d = p.getDocumento(); // Vai dar merda
d.getNumero(); // Já deu merda

Já o programador cauteloso e experiente não se arriscaria a tomar um NullPointerException na cara:

Animal elefante = animais.get("elefante");
if(elefante != null)
  elefante.anda();  // Agora é seguro
Pessoa p = PessoaDAO.findByName("Felipe");
if(p != null) {
  Documento d = p.getDocumento();
  if(d != null) { 
    d.getNumero();  
  }
}

Ser desconfiado dá trabalho, e deixa nosso código cheio de if-guards, que são feios e chatos de ler.

Claro que, com um bom design, é possível diminuir a quantidade de ifs, mas o grande problema é que eventualmente alguém vai esquecer do maldito if e um NullPointerException (ou Segmentation Fault) poderá explodir a qualquer momento em produção!

O problema é que o compilador não nos diz nada. Se fosse possível fazer o compilador trabalhar para gente, e pegar todos esses erros em compilação seria ótimo…

Evitando o erro

Muitas pessoas inteligentes perceberam o risco de se ficar manipulando nulls e inventaram maneiras para evitar seu uso.

Null Objects

Um exemplo é o pattern Null Object, que sugere a construção de objetos “sem comportamento” como uma alternativa.

O grande problema desses Null Objects é que se forem usado sem cuidado, podem introduzir bugs difíceis de se encontrar, pois eles de certa forma disfarçam o problema. Imagine: você tem um objeto na mão, chama alguns métodos sem nenhum problema só que na verdade esses métodos não estão fazendo nada.

Option

Um conceito muito interessante e bastante simples, trazido diretamente do mundo da programação funcional é o do tipo Option, também chamado de Maybe em algumas linguagens.

O Option é um tipo abstrato parametrizado (portanto Option[T]) que tem apenas dois filhos: Some[T] e None. De maneira muito simplificada, em Java seria algo do tipo:

public abstract class Option<T> {
	  public static <T> Option<T> of(final T conteudo) {
		if(conteudo == null) {
			return new None<T>();
		} else {
			return new Some<T>() {
		    	public T get() {
		    		return conteudo;
		    	}
		    };	
		}
	  }
}

abstract class Some<T> extends Option<T> {
  public abstract T get();
}

class None<T> extends Option<T> {}

Usamos o Option assim:

Option<String> some = Option.of("Tem coisa"); // devolve Some<String>
Option<String> none = Option.of(null); // devolve None

Portanto, sempre que uma função tem a possibilidade de devolver um valor inválido ou inexistente para determinado argumento, podemos embrulhar o resultado em um Option.

Por exemplo:

Option<Pessoa> pessoa = pessoaDAO.findById(1234L); 

Legal, dessa maneira pessoa nunca será null, mesmo que o registro 1234 não exista no banco de dados.

Só que quando eu quiser o conteúdo do Option, teria que fazer isso:

if(pessoa instanceof Some<?>) { 
  String nome = ((Some<Pessoa>) pessoa).get().getNome(); 
  System.out.println("Encontrei pessoa com nome: " +  nome); 
} else {
  System.out.println("Não encontrei ninguém");
}

Esse código, apesar de terrivelmente feio, é mais type-safe, já que o compilador nos força a fazer uma verificação antes de usar a instância de Pessoa.

Outra desvantagem dessa abordagem é que, além de ser desengonçado, é mais prático fazer um if(pessoa != null) . Por isso é difícil convencer um programador a usar isso. E agora?

Bom, em Scala e em outras linguagens mais funcionais temos Pattern Matching para melhorar as coisas:

Option<Pessoa> pessoa = pessoaDAO.findByPK(1234L); 
pessoa match {
  case Some(p) => println("Encontrei pessoa com nome: " + p.nome)
  case None => println("Não encontrei ninguém")
}

Ainda um pouco verboso, mas bem melhor!

Pattern Matching é ideal quando temos dois fluxos completamente diferentes dependendo se o Option é Some ou None. Para casos em que só se quer passar o valor para uma função, ou manipulá-lo de maneiras mais simples, podemos usar as construções apresentadas na próxima seção.

Indo um pouco além do óbvio

Option surgiu no mundo funcional, e é muito mais do que um simples container de coisas.

Na realidade, é possível manipular um Option de maneiras muito práticas e poderosas, especialmente em linguagens em que conseguimos passar funções como parâmetro. Essas construções geralmente são mais enxutas do que fazer Pattern Matching.

Tony Morris fez um post em seu blog mostrando algumas formas mais funcionais de se substituir diversos casos implementados com Pattern Matching, usando a própria API do Option.

Veja alguns exemplos em Scala:

Option<Pessoa> pessoa = pessoaDAO.findByPK(1234L);  // mesmo exemplo

// foreach
// Se for Some executa a função passada, se for None não faz nada.
pessoa.foreach(p => println(p.nome))

// map
// Se for Some executa a função e embrulha o retorno em um Option. Se for None devolve None.
val endereco:Option[Endereco] = pessoa.map(_.endereco) 

// getOrElse
// Se for Some devolve o conteúdo, se for None devolve o resultado do bloco passado como parâmetro
val p = pessoa.getOrElse(new Pessoa())

Note que muitos métodos de Option são os mesmos que existem nas Collection em Scala.
E pasmem, é possível usar Option em for-comprehensions, podendo até misturar listas com options.

val lista:List[Integer] = List(1,2,3)
val option:Option[Integer] = Some(1)

// Conhecemos for em listas
for( num <- lista) {
  println(num)
}

// Mas é possível usar Option
for(num <- option) {
  println(num)
}

Imagine que você quer navegar por uma estrutura de objetos cujos métodos retornam Option. É possível usar uma construção for sofisticada em Scala:

for { 
  pessoa <- pessoaDAO.findByPK(1234L)  
  endereco <- pessoa.getEndereco
  numero <- endereco.getNumero 
} {
  println("O número da casa da pessoa de ID 1234 é " + numero)
}

Se em alguma dessas linhas, o objeto for um None, o corpo do for não será executado. É como se um Option fosse uma lista de um único elemento.

Por fim…

Option é uma poderosa abstração para substituir o famigerado null, causa de tantos NullPointerExceptions inesperados.

Seu objetivo é tornar o código mais type-safe e, mesmo adicionando uma camada extra para alcançar isso, sua API engenhosa permite que o programador manipule os Option de maneiras muito eficientes, concisas e poderosas.

Quando estiver projetando uma API, considere o uso de Option em funções que podem ou não devolver um objeto. Use para tratar os casos de exceção, como por exemplo quando um registro não é encontrado no banco de dados, ou quando uma função não está definida para determinado argumento.

Anúncios

2 comentários sobre “Type-safe null usando Option

  1. Muito legal o uso de Options. Em java eu gosto muito de usar o Null Object. É um padrão extremamente simples, mas pouco difundido. Em linguagens dinamicas esse tratamento de nulos é mais simples. Em Groovy vc tem o Safe Navigation, que é algo como “variable?.field”, “variable?.method()”. Se variable for null, a expressão volta null e nao da erro.

    • Interessante esse esquema de Safe Navigation. O que me preocupa no caso de Null Objects é o programador passar batido pelo caso de exceção. Option pelo menos te força a pensar em algo.

Os comentários estão desativados.