Como você implementaria um simples like na sua aplicação?

Eis uma forma:

app.post('/like', async (req, res) => {
const user = req.user.id
const article = req.body.article
if (await db.get({article, user})) {
await db.delete({article, user})
} else {
await db.put({article, user})
}
res.send()
})

(Não se assuste com as palavras async e await aqui, são apenas promises .)

Qual o resultado desta operação? Quer dar um palpite?

Se você achou isto parecido com o comportamento de um checkbox (ou toggle), você provavelmente acertou a intenção 100% das vezes de quem escreve este tipo de código. Mas dificilmente a intenção do usuário é inverter o estado atual. Ele quer marcar. Ou desmarcar. Se o estado futuro é igual ao estado atual, ele não quer inverter.

Alguns problemas desta abordagem:

  • A API não carrega a intenção do usuário;
  • Não apresenta idempotência;
  • Uso dos recursos de banco de dados é ineficiente;
  • Ignora efeitos do ambiente distribuído.

O objetivo deste artigo é demonstrar porque este conceito (toggle) deve ser desconsiderado quando se trata de uma operação assíncrona. Apesar de simples, é baseado em pressupostos demais. Apresento uma solução mais robusta e devidamente justificada.

Sem mais delongas, vamos resolver isto da seguinte forma:

app.post('/like', async (req, res) => {
const user = req.user.id
const article = req.body.article
await db.put({article, user})
res.send()
})
app.post('/unlike', async (req, res) => {
const user = req.user.id
const article = req.body.article
await db.delete({article, user})
res.send()
})

Nós fizemos o seguinte:

  • Separamos a operação em dois endpoints, /like e /unlike, garantindo assim a intenção do usuário;
  • Em cada endpoint, apenas uma operação de banco de dados, atômica e idempotente, é executada.

Note que não há mais condicionais nesta implementação. E com estas 2 simples medidas, atendemos aos 4 pontos que motivaram uma implementação melhor.

Mas isto apenas transfere o controle para o front end, você argumenta. Eu concordo. E vejo como uma forma adequada para preservar a intenção do usuário. O servidor não mantém estado e não sabe qual a saída esperada. E o banco de dados só armazena o último dado que você enviou. Já o front end é o que se apresenta ao usuário, e o usuário toma ações baseado no que vê nesta apresentação.

Sobre APIs

Logo, esta API deve refletir as operações disponíveis no front end. O front end não ficará mais complicado tendo que escolher entre duas operações distintas, afinal ele tem o conhecimento do estado e sabe qual chamar. E ainda temos o benefício de poder medir qual intenção do usuário está sendo mais frequente (like ou unlike), se for desejado.

Por último, torna possível executar a operação com maior eficiência no banco de dados.

Sobre Banco de Dados

Bancos de dados relacionais (SGDBs) dão suporte a transações. Elas são atômicas, consistentes, isoladas e duráveis. Por isoladas (as demais características não importam no momento), quer dizer que não é observável o efeito da execução concorrente das transações. É como se cada transação executasse complementamente antes da próxima começar. Para isso o SGDB coloca locks em registros escritos durante a transação. Mas lembra-se que no primeiro trecho de código começamos com uma operação de leitura? Ainda assim nestes SGBDs é possível solicitar que a leitura coloque um lock no registro. Outro processo efetuando uma leitura na tabela/registro precisará aguardar a conclusão da transação que mantém o lock. Isto pode levar vários segundos e até minutos! Imagine o impacto no desempenho global do sistema, considerando todos os usuários concorrentes. E transações não estão disponíveis em um banco de dados NoSQL.

A execução do banco de dados é sequencial? Vejamos. Uma única instância de banco de dados pode ter a capacidade de executar mais de uma operação simultaneamente. O servidor pode ter mais de um core, os dados já estão em cache, as requisições são processadas em paralelo em diferentes estágios da pipeline (analisar a consulta, E/S, processar). E principalmente, mesmo com transação, o banco de dados não está impedido de executar uma consulta de outro processo, mesmo antes de retornar um resultado em andamento. Salvo a existência de locks, o banco de dados irá paralelizar a execução dos comandos.

Ainda há o cenário de sharding ou consistência eventual, mecanismos lançados para proporcionar escalabilidade e disponibilidade, mas onde a leitura pode ser efetuada em uma versão obsoleta dos dados. Para evitar isto é necessário ativar o modo de consistência robusta, menos performática, pois executa a operação em ao menos dois nodes no servidor.

Estou usando este exemplo simples para demonstrar conceitos. Claro que neste caso específico, tão simples, a implementação original pode passar sem problemas. O conceito que eu quero transmitir é que, podendo fazer melhor, o que nos impede? Por quê ficar preso a uma estrutura menos eficiente?

Reduzindo o número de operações de banco de dados pela metade, temos um ganho de desempenho de 50%, por assim dizer. E ainda evitamos cenários de execução entrelaçada e desvantagens do uso de transações.

Sobre User Experience

Então você, no front end, decide implementar o padrão idiomático optimistic updates:

like() {
setState({like: true})
try {
await post('/like')
} catch (e) {
setState({like: false})
}
}
unlike() {
setState({like: false})
try {
await post('/unlike')
} catch (e) {
setState({like: true})
}
}
onLikeClick() {
getState().like ? unlike() : like()
}

Este padrão consiste em refletir em tela como seria o resultado da operação, mesmo antes dela ser iniciada. Bacana hein?! Isto inverte a lógica pessismista (ou realista) que precisamos esperar pelo término da operação. Você sabe que isto remove o efeito da latência. Não somente da latência, mas o custo da operação completa. E se dá por satisfeito. Não esqueça de tratar apropriadamente em caso de insucesso, ainda que improvável.

Eu uso e recomendo este padrão, mas agora precisamos falar sobre idempotência e as mentiras que te contaram sobre programação de sistemas distribuídos.

Sobre o Ambiente

São elas:

  1. A rede é confiável;
  2. A latência é zero;
  3. A banda é infinita;
  4. A rede é segura;
  5. A topologia não muda;
  6. Existe um administrador;
  7. O custo de transporte é zero;
  8. A rede é homogênea.

Aqui, nos interessam apenas os 2 primeiros itens.

A latência é zero.

A latência pode ser inócua do ponto de vista do usuário, com a atualização precoce do estado da tela, mas ela ainda existe. O tempo de execução de um comando no banco de dados pode resultar em diferença no resultado quando há entrelaçamento de operações, como vimos antes.

É possível alcançar este resultado implementando um toggle?

Se estiver marcado, então desmarque; caso contrário, então marque.

Na execução serial das requisições, ou seja, processar completamente uma requisição após a outra, a segunda reverte o efeito da primeira.

Na situação da execução em paralelo, onde uma leitura ocorre antes da escrita anterior em outro processo concluir, 50% das vezes será como se apenas uma única requisição foi posta, e pode até resultar em likes duplicados. Eu não gosto que meu software seja imprevisível desta forma.

Você não quer depender de fatores que você não pode controlar. Alguns fatores que você não tem controle:

  • A ordem de execução de comandos de processos distintos no banco de dados;
  • Que ambas operações irão resultar ou em sucesso, ou em erro;
  • A notificação da execução com sucesso da requisição: um timeout pode ocorrer mesmo com a operação completada.

A rede é confiável.

Agora você já está convencido de separar suas APIs conforme a intenção. Vamos com calma. Este caminho também tem suas nuances. Você precisa de um mecanismo que determine a ordem das requisições. Não é porque uma requisição saiu primeiro que ela chegará primeiro. Aí você já pensa em voltar pro toggle. Mas lembre que com toggle, você precisa garantir isolamento na operação, algo que teu banco NoSQL não fornece, e o banco SGDB reduz teu desempenho. Abordarei soluções para sequenciamento em um artigo futuro.

Referências:

Espero que tenha valido a pena ler a até aqui! Eu gostei de poder escrever sobre isso, eu curto muito quando diferentes habilidades são necessárias para resolver um problema e aqui encontrei um probleminha bem propício. E mantenha seus toggles apenas nos checkboxes.

Feedbacks serão muito bem-vindos! Desenvolvi os conceitos demais? De menos? O conteúdo é relevante? Aguardo teu comentário.

Ah e se gostou, dá um like!

Agradecimentos: aos membros do grupo nodebr no Slack, em particular ao joaoarau que trouxe este debate.

Artigo publicado originalmente em http://blog.werlangtecnologia.com.br/design/likes-depois-do-click/

Written by

Software Development Expert

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store