Boas práticas com Higher-Order Functions no Kotlin

Alex Felipe
CollabCode
Published in
5 min readFeb 14, 2018
Robôs

No artigo onde escrevi sobre o deprecated do ProgressDialog, fiz uso de algumas técnicas para adicionar uma pré e pós execução a partir de HOFs, entretanto, a implementação apresentou os seguintes aspectos:

  • Não é fácil para qualquer um entender o que acontece;
  • Obriga implementar várias HOFs, mesmo em casos que não é necessário;
  • O código duplica.

Note que são pontos que impactam na qualidade do código de modo geral, seja por questões de leitura/compreensão e manutenção.

Quer aprender mais sobre Kotlin tanto no mundo mobile como no back-end? Então confira este agregador de conteúdo onde listo todos os conteúdos que escrevi de Kotlin e os que serão publicados mais pra frente 😉

Considerando todos esses pontos, neste artigo veremos como podemos evitar essas situações com algumas técnicas e boas práticas. Sendo assim, vamos dar início fazendo uma análise na seguinte amostra de código:

implementando várias HOFs na função de adição de nota do dialog

Olhando a função add() de cara, você sabe o que os parâmetros que estão recebendo expressões lambdas fazem? Não se sinta mal se realmente você não tem ideia, pois você não é o culpado!

Trata-se de um código que omite comportamentos para não ser verboso, mas, ao mesmo tempo, prejudica muito a leitura/compreensão do código…

Named parameter funciona para HOF também!

Para lidarmos com esse tipo de situação podemos aplicar uma das técnicas mais legais do Kotlin que considero:

Utilizando labels para identificar as expressões lambdas

Isso mesmo, somos capazes de aplicar os Named Parameters da mesma maneira que utilizamos em construtores de classes, logo, podemos até mesmo deixar numa ordem que faça sentido pra nós:

Enviando os parâmetros em ordem diferente da padrão

Ao realizarmos a leitura desse código, é nítido que primeiro acontece uma pré-execução (preExecute), criação (created) e depois uma finalização (finished).

Mesmo que tenha deixado o nosso código mais simples, não significa que resolvemos todos os problemas que listei no começo do artigo, pois ainda somos obrigados a implementar todas as HOFs para utilizar a função add() do nosso Dialog…

Valores padrões nos parâmetros das funções

Para deixar a implementação mais flexível, possibilitando a implementação das HOFs, porém, sem ser obrigatório, podemos utilizar a abordagem de valores padrões para os parâmetros que recebem HOFs também!

fun add(preExecute: () -> Unit = {},
finished: () -> Unit = {},
created: (createdNote: Note) -> Unit = {}) {
// restante do código
}

Perceba que nesta atribuição padrão, implementando uma expressão lambda que não faz nada! Desta maneira, qualquer um que chamar a função add() não é obrigado a implementar nenhuma HOF:

fab_add_note.setOnClickListener {
NoteDialog(window.decorView as ViewGroup, this)
.add()
}

Claro, é importante sempre pensar se faz sentido, ou não, obrigar a implementação de uma HOF, pois a created, que devolve a nota de acordo com o callback, é uma ação bem importante para a razão dela existir, pois ela permite tomar uma ação assim que a requisição for realizada com sucesso.

Em outras palavras, se uma HOF tiver uma grande importância para ser implementada, provavelmente, a abordagem de deixá-la sem um valor padrão pode ser mais benéfico.

Replicando comportamentos

Mesmo tendo uma flexibilidade maior, perceba que atualmente as pré e pós execuções são feitas apenas na função add(), ou seja, faz todo o sentido possibitarmos também essas ações para outras funções do dialog, como é o caso do alter():

fun alter(note: Note,
preExecute: () -> Unit = {},
finished: () -> Unit = {},
altered: (alteredNote: Note) -> Unit) {
// restante do código
}

Então basta apenas executarmos as HOFs no momento esperado:

  • preExecute: antes de começar com a requisição;
  • finished: quando a requisição finalizar, da mesma maneira como fizemos com o add() do Web Client.

Por mais que não tenha tanto segredo, veja que cada vez mais o nosso código abre margem ao erro, pois todas as vezes as classes que chamam o Web Client ficam responsáveis em lembrar da regra de execução das HOFs para obter o comportamento esperado…

Em outras palavras, faz todo o sentido resolvermos o problema pela raiz! Ou seja, podemos fazer com que a função de callback genérica receba todas as HOFs:

Adicionando as funções de pré e pós execução na função de callback genérica

Note que também já modifiquei todas as HOFs para que sejam opcionais, ou seja, todas elas possuem uma implementação padrão.

Delegando as HOFs para a função de callback

Consequentemente, todas as funções do Web Client podem delegar a responsabilidade diretamente para a função callback():

Delegando as HOFs de pré e pós execução para a função de callback genérica

Perceba que agora, cada membro da Web Client está apenas implementando as HOFs de sucesso ou falha.

Inclusive, podemos até mesmo deixar ambos comportamentos mais sucintos já que a HOF de resposta e falha fazem praticamente a mesma coisa em cada uma das funções do Web Client.

Extraíndo Extension Functions genéricas

Para isso, podemos criar Extension Functions genéricas que farão esse comportamento padrão:

Funções genéricas para callback de resposta e falha

Agora cada uma das enqueue() ficam da seguinte maneira com a chamada das funções genéricas:

call.enqueue(
callback(
response = { it.defaultResponse(success) },
failure = { it.defaultFailure(failure) },
preExecute = preExecute,
finished = finished))

Cada parâmetro em apenas uma linha! Bem mais limpo, né? No dialog, basta apenas realizarmos a modificação para que ele delegue a pré e pós execução também:

Delegando a HOF para o Web Client

Agora, deixamos toda responsabilidade em chamar as funções de pré e pós execução para nossa função genérica de callback. Em modo gráfico temos o seguinte resultado:

Fluxo de implementação e execução das HOFs para realizar a requisição

Com essas modificações, a única coisa que precisamos nos preocupar é justamentre com o Named Paramater das funções, ou seja, na função list() do dialog, precisamos apenas realizar o seguinte ajuste:

NoteWebClient().list(
success = {
notes.addAll(it)
configureList()
},
failure = {
Toast.makeText(this, "Falha ao buscar as notas", Toast.LENGTH_LONG).show()
})

Repare que agora somos capazes de identificar o que cada expressão lambda significa e, no momento que for desejado, podemos implementar comportamentos de pré e pós execução, incrível, né? 😉

Código fonte

Caso surgiu alguma dúvida ou simplesmente queira consultar o código fonte, fique à vontade me dar uma olhada no repositório GitHub.

Conclusão

Neste artigo reforçamos a ideia do uso de Named Parameter para identificar com mais precisão as expressões lambda das HOFs, também vimos que somos capazes de atribuir valores padrões com expressões lambdas vazias para tornar as HOFs opcionais.

Por fim, vimos algumas técnicas para delegar a execução das HOFs de pré e pós execução diretamente na função de callback genérica, como também, criamos extension functions padrões que fez com que o código duplicado na Web Client fosse drasticamente reduzido.

O que achou dessas técnicas? Compartilhe comigo nos comentários 😃

--

--