Uma característica importante do paradigma funcional é o projeto dos tipos das linguagens. O uso de tipos como listas, mapas, árvores e conjuntos são enfatizados para a manipulação de dados. Nas linguagens mais puras os valores são imutáveis, logo as variáveis devem ser inicializadas com valores válidos. Note que isto é muito importante. Se precisamos inicializar nossas variáveis com valores válidos isso significa que nunca teremos variáveis nulas. O próprio inventor do conceito de nulo reconhece que isso é um erro.
Caso ainda esteja resistente a reconhecer este erro, lembre-se de quantas NullPointerException seu código de produção gerou, ou ao menos quanta complexidade foi adicionada ao código através de condicionais você teve de fazer verificando se algo é diferente de null.
Claro que podemos apelar para o Null Object Pattern, mas nem sempre é trivial e envolve muito trabalho em alguns casos.
O ideal seria o sistema de tipos da nossa linguagem nos ajudar com isso. Vamos ver um exemplo:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public abstract class Option<T> { | |
public abstract boolean hasValue(); | |
public abstract T get(); | |
public T getOrElse(T alternative) { | |
return hasValue() == true ? get() : alternative; | |
} | |
} |
Vamos ao subtipo que indica existência de valor:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public final class Some<T> extends Option<T> { | |
private final T value; | |
public Some(T value) { this.value = value; } | |
public boolean hasValue() { return true; } | |
public T get() { return value; } | |
@Override | |
public String toString() { return "Some("+value+")"; } | |
@Override | |
public boolean equals(Object other) { | |
if (other == null || other.getClass() != Some.class) | |
return false; | |
Some<?> that = (Some<?>) other; | |
Object thatValue = that.get(); | |
return value.equals(thatValue); | |
} | |
@Override | |
public int hashCode() { return 37 * value.hashCode(); } | |
} |
Agora o subtipo que representa ausência de valor:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public final class None<T> extends Option<T> { | |
public static class NoneHasNoValue extends RuntimeException {} | |
public None() {} | |
public boolean hasValue() { return false; } | |
public T get() { throw new NoneHasNoValue(); } | |
@Override | |
public String toString() { return "None"; } | |
@Override | |
public boolean equals(Object other) { | |
return (other == null || other.getClass() != None.class) ? false : true; | |
} | |
@Override | |
public int hashCode() { return -1; } | |
} |
Agora vamos fazer uso do nosso novo sistema de tipos:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Before | |
public void setup() { | |
names = new ArrayList<Option<String>>(); | |
names.add(new Some<String>("Renan")); | |
names.add(new None<String>()); | |
names.add(new Some<String>("Paulo")); | |
} | |
@Test | |
public void usandoGetOrElse() { | |
String[] expected = { "Renan", "Valor alternativo!", "Paulo"}; | |
System.out.println("*** Usando getOrElse:"); | |
for (int i = 0; i < names.size(); i++) { | |
Option<String> option = names.get(i); | |
String value = option.getOrElse("Valor alternativo!"); | |
System.out.println(option + " = " + value); | |
assertEquals(expected[i], option); | |
} | |
} | |
// Saída: | |
// Some(Renan) = Renan | |
// None(Valor alternativo) = Valor alternativo | |
// Some(Paulo) = Paulo |
Também podemos voltar á abordagem mais clássica, perguntando se existe valor no Option:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Test | |
public void verificaSePossuiValor() { | |
String[] expected = { "Renan", null, "Paulo"}; | |
for (int i = 0; i < names.size(); i++) { | |
Option<String> option = names.get(i); | |
if (option.hasValue()) { | |
String value = option.get(); | |
System.out.println(option + " = " + value); | |
assertEquals(expected[i], value); | |
} | |
} | |
} |
Por fim, vamos ver como fica o código dos nossos métodos que podem retornar um valor ou ausência de valor e seu uso:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public Option<String> fazAlgo() { | |
if(se tenho valor retornarei um Some com o valor) { | |
return new Some<String>(valor); | |
} else { | |
// não tenho valor, ao invés de retornar nulo | |
//e correr o risco do código cliente chamar um método em null, retornarei um None | |
return new None<String>(); | |
} | |
} | |
// Option<String> option = fazAlgo(); |
Desenvolvendo nossas próprias classes para melhorar o sistema de tipos nesse caso, parece overhead, uma solução é o uso do Guava, uma lib que possui recursos interessantes que dão um "sabor"funcional para nossos projetos.
Aqui tem uma rápida introdução de como usar esses recursos com o Guava.
Nenhum comentário:
Postar um comentário