Introdução
Existe uma boa prática de programação que recomenda que se faça aninhamento excessivo de condições if-else, para evitar códigos conforme da figura abaixo.
Algumas soluções e padrões de projeto podem ser aplicados, desde uso de estrutura de dados (Maps/Dicionários), a padrão de projetos (strategy. explicado no excelente post de @ivanqueiroz).
Recursos da linguagens observadores e experimentados
Nessa seção, pretendo falar de alguns recursos que pude experimentar durante o curso, e pratiquei em um projeto piloto que está disponível em um repositório no meu github onde pretendo colocar os demos do que ando testando com Kotlin. Esse repositório é o kotlin-lab.
Um pouco de teoria
O if/else demasiadamente, não deve ser evitado apenas por questões de legibilidade mas também porque impacta na performance e na complexidade do algorítimo. A complexidade sintomática é uma métrica de software utilizada para definir a complexidade de um programa. Ela é medida como a medida quantitativa do número linar de caminhos que um programa pode percorrer através de seu código-fonte. Explicações mais detalhadas sobre o tema pode ser obtidas nos links 1,2 e 3. Existe toda uma teoria matemática que fundamenta essa explicação e de maneira “grosseira” porém, objetiva temos um resumo da seguinte forma:
- Se temos entre 0 e 5 condições - Provávelmente o programa é ok
- Se temos entre 6 e 10 - Devemos pensar em simplificar essa rotina
- Se temos mais de 10 - devemos quebrar essa rotina em subrotinas
Falando exclusivamente pelo viés da performance, a tendência é que o algorítimo do primeiro anexo de código, seja mais performático do que o segundo.if("visa".equals(transaction.getNetwork())){
//do visa stuff
} else if ("diners".equals(transaction.getNetwork())) {
//do diners stuff
} else if ("mastercard".equals(transaction.getNetwork())) {
//do mastercard stuff
} else if ("amex".equals(transaction.getNetwork())) {
//do amex stuff
}
//common stuff in there
return transaction;
Implementação 2:if("visa".equals(transaction.getNetwork())){
//do visa stuff
return doCommonStuff(transaction);
}
if ("diners".equals(transaction.getNetwork())) {
//do diners stuff
return doCommonStuff(transaction);
}
if ("mastercard".equals(transaction.getNetwork())) {
//do mastercard stuff
return doCommonStuff(transaction);
}
if ("amex".equals(transaction.getNetwork())) {
//do amex stuff
return doCommonStuff(transaction);
}
Quando temos um contexto onde é necessário se testar essas condições, o que podemos usar?
Em computação poucas perguntas não podem ser respondidas com a simples palavra depende. Existem contextos e estruturas onde é necessário trabalhar com diversas condicionais, então o que as linguagens oferecem como recurso mais elegante que o if/else. A grande vantagem de utilizar recursos nativos da linguagem, é que a mesma oferece uma implementação para reduzir a complexidade sintomática. Instruções como Switch (Java) e When (Kotlin), reduzem significativamente o impacto do grafo de decisão do algoritimo pois a condição da expressão é avaliada apenas uma vez. O compilador normalmente trata e garante uma otimização no uso desse tipo de instrução.
Java
Em Java, temos a instrução switch/case, que desde a release 8 vem sofrendo diversas melhorias mas apresenta algumas limitações, como por exemplo, no passado não suportava Strings dentro das condições.
Sintaxe da instrução switch/case em Java.switch(expression) {
case x:
// code block
break;
case y:
// code block
break;
default:
// code block
}
Como isso funciona:
- A instrução boolean dentro do switch é avaliada uma única vez.
- O resultado dessa expressão é avaliado com cada case.
- Se algum acontece o match, o bloco dentro do case é executado.
- É opcional utilização de break e default.
Evolução Switch/case no Java vem acontecendo e ele atualmente suporta comparação com Strings (desde o Java 7) e outros recursos que vou tentar enumerar logo abaixo:
- Switch Expressions - Preview Java 12
- Switch Expressions - Preview Java 13
- Switch Expressions - Standard Java 14
O que é possível com esses recursos:boolean result = switch (ternaryBool) {
case TRUE -> true;
case FALSE -> false;
case FILE_NOT_FOUND -> throw new UncheckedIOException(
"This is ridiculous!",
new FileNotFoundException());
// as we'll see in "Exhaustiveness", `default` is not necessary
default -> throw new IllegalArgumentException("Seriously?! 🤬");
//exemplo 2
enum Person {
Mozart, Picasso, Goethe, Dostoevsky, Prokofiev, Dali
}
static void print(Person person) {
String title = switch (person) {
case Dali, Picasso -> "painter";
case Mozart, Prokofiev -> "composer";
case Goethe, Dostoevsky -> "writer";
};
System.out.printf("%s was a %s%n", person, title);
}
//exemplo 3:
static int factorial(int n) {
return switch (n) {
case 0, 1 -> 1;
case 2 -> 2;
default -> factorial(n - 1) * n;
};
}
Kotlin
Mas e como o Kotlin fornece trata esse problema? Eu particulamente achei muito simples e eficiente a forma como Kotlin traz isso. Como é uma linguagem mais nova e não precisa pensar em retrocompatibilidade, já nasceu da maneira otimizada e baseada em linguagens menos verbosas que o Java.
When
A instrução foi pensada para compensar diversos recursos não suportados nativamente pelo switch/case do Java.
A primeira grande vantagem é que o when já nasceu como uma expressão (suporta um valor a ser calculado em tempo de execução). A instrução é executada quando uma das condições é satisfeita. When pode ser usada tanto como expressão, como comando. When suporta um bloco else que tem comportamento similar a um else de uma instrução if.
When suporta ranges e expressões como condicionais, também é possível utilizar a função is.
Exemplos de uso diversos:when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // Note the block
print("x is neither 1 nor 2")
}
}
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
fun hasPrefix(x: Any) = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
// a partir do Kotlin 1.3 é possível capturar um variável, visível ao escopo do when apenas.
fun Request.getBody() =
when (val response = executeRequest()) {
is Success -> response.body
is HttpError -> throw HttpException(response.status)
}
//when como expressão
val objectType = when (directoryType) {
UnixFileType.D -> "d"
UnixFileType.HYPHEN_MINUS -> "-"
UnixFileType.L -> "l"
}
val result = when (fileType) {
UnixFileType.L -> "linking to another file"
else -> "not a link"
}
val result: Boolean = when (fileType) {
UnixFileType.HYPHEN_MINUS -> true
else -> throw IllegalArgumentException("Wrong type of file")
}
//when sem argumento
val objectType = when {
fileType === UnixFileType.L -> "l"
fileType === UnixFileType.HYPHEN_MINUS -> "-"
fileType === UnixFileType.D -> "d"
else -> "unknown file type"
}
//when em coleção
val isRegularFileInDirectory = when (regularFile) {
in directory.children -> true
else -> false
}
//when com range
val isCorrectType = when (fileType) {
in UnixFileType.D..UnixFileType.L -> true
else -> false
}
//when com operador _is_
val result = when (unixFile) {
is UnixFile.RegularFile -> unixFile.content
is UnixFile.Directory -> unixFile.children.map { it.getFileType() }.joinToString(", ")
is UnixFile.SymbolicLink -> unixFile.originalFile.getFileType()
}
//when com auto-casting do Kotlin
when (view) {
is TextView -> toast(view.text)
is RecyclerView -> toast("Item count = ${view.adapter.itemCount}")
is SearchView -> toast("Current query: ${view.query}")
else -> toast("View type not supported")
}
A gramática do when pode ser vista no link da linguagem
Conclusão
Conhecemos mais um recurso bacana e muito rico da linguagem Kotlin. A medida que for descobrindo mais, vou registrando aqui como nota mental e para compartilhar com outras pessoas que possam estar estudando mesmo tópico.
Outras Fontes:
- http://www.thedevpiece.com/why-you-should-avoid-if-else-statements/
- https://www.w3schools.com/java/java_switch.asp
- https://blog.codefx.org/java/switch-expressions/
- https://medium.com/better-programming/a-look-at-the-new-switch-expressions-in-java-14-ed209c802ba0
- https://www.baeldung.com/java-switch
- https://www.baeldung.com/kotlin-when
- https://kotlinlang.org/docs/reference/control-flow.html
- https://superkotlin.com/kotlin-when-statement/