sampope 10: Como usar callbacks para armazenar dados calculados numa aplicação Rails?

Usando callbacks para calcular e armazenar dados numa aplicação Rails.

Considerando que grande parte da motivação para ter rotinas de trabalho num sistema é diminuição da margem de erros, é comum que a gente queira calcular certas informações dentro do sistema, ao invés de permitir que o usuário às insira.

No nosso caso onde a aplicação Rails será responsável por rotinas de uma operadora de turismo, um exemplo deste tipo de campo seria o total de um pacote ou roteiro após o desconto ser aplicado. Ao invés do usuário inserir o subtotal, o desconto, e depois o total (tendo calculado este manualmente), vamos fazer com que ele possa simplesmente colocar o subtotal e o desconto, deixando que o sistema calcule o total final.

Vamos lá?

1 - Crie a classe Itinerary

rails generate scaffold itinerary subtotal:integer discount:integer total:integer

Vamos usar scaffold nesta criação, pois neste momento não estamos preocupados com os detalhes do CRUD. Queremos somente focar neste dado calculado.

Lembre-se de rodar a migração na sequência:
rails db:migrate

(Se quiser mais detalhes sobre esta parte, veja este post, onde focamos nas mágicas do scaffold do Rails.)

2 - Crie um teste para a classe

# spec/models/itinerary_spec.rb

require 'rails_helper'

describe Itinerary do
  it 'saves total' do
    itinerary = Itinerary.create(subtotal: 5, discount: 3, total: 2)

    expect(itinerary.total).to eq 2
  end
end

Podemos falar mais sobre testes de classes em outro post. O mais importante aqui é que ele esteja no diretório certo e especifique valores para subtotal, desconto, e total.

O teste acima foi criado para que ele passe agora, do jeito que a classe está. Como já criamos a classe e uma série de coisas no scaffold do primeiro passo, é importante fazer isso antes e nos certificarmos de que está tudo como esperamos. Veja na minha tela abaixo que está tudo ok, o teste desta forma passou:


Agora que temos certeza de que está tudo em ordem com a classe e o teste neste momento, vamos alterar o teste para o cenário que buscamos. Queremos criar uma situação onde o total não seja inserido, mas sim calculado pela própria classe antes de salvar, certo? Então, o ajuste no nosso teste é retirar o total: 2 dali dos nossos parâmetros e continuar com a expectativa de que o total seja 2, deixando o teste assim:

# spec/models/itinerary_spec.rb

require 'rails_helper'

describe Itinerary do
  it 'saves total' do
    itinerary = Itinerary.create(subtotal: 5, discount: 3)

    expect(itinerary.total).to eq 2
  end
end

E desta vez, quando rodamos o teste, temos problemas:


Tudo certo. Este era o problema que queríamos. O teste está nos dizendo que ao procurar pelo valor 2, ele acabou não encontrando nada. Faz sentido, pois não estamos colocando nada lá.

Agora começa a parte boa!

3 - Crie o método do cálculo

Dentro da nossa classe que no momento está assim:

# app/models/itinerary.rb

class Itinerary < ApplicationRecord
end

Precisamos inserir este método:

def assign_total
  self.total = subtotal - discount
end

Ele pede para que o Rails subtraia o valor do desconto do valor subtotal e armazene esta diferença no total do objeto em questão.

4 - Escolha o callback

O Rails coloca vários callbacks à nossa disposição. Este link nos mostra todos na ordem em que acontecem. Para escolher o que vamos usar, precisamos pensar em duas coisas:
  1. Porque nosso callback vai existir?
  2. Quando ele deve agir?
A resposta da primeira pergunta é que nosso callback vai existir porque queremos tirar do usuário a responsabilidade de calcular o total. Sobre a segunda, ele deve agir quando o objeto for salvo, independente dele estar sendo criado ou atualizado.

Com estas questões esclarecidas, fica claro que o nosso callback vai ser o before_save, afinal ele roda logo antes do objeto ser salvo, não importante se ele já existia ou não.

5 - Configure o callback para chamar o método 

Agora que já temos o método e sabemos qual callback vamos usar, podemos configurá-lo com a seguinte linha:
before_save :assign_total

Nossa classe completa fica assim:

# app/models/itinerary.rb

class Itinerary < ApplicationRecord

  before_save :assign_total

  def assign_total
    self.total = subtotal - discount
  end

end

E quando rodamos novamente o teste, temos:


Passou! Tudo certo!

Pronto!

Agora nossa aplicação Rails não salva mais o valor total vindo do usuário, mas sim o resultado do cálculo feito pela classe antes de salvar os dados no banco!


Um passo além

Agora que a nossa classe não usa mais o valor total passado pelo usuário, não seria melhor impedir que ele insira um no formulário?


Fazemos isso retirando o campo no arquivo app/views/itineraries/_form.html.erb.

Depois de retirar o campo do formulário, é indicado também retirar ele da lista de dados aceitos no arquivo app/controllers/itineraries_controller.rb.

Nenhum comentário:

Postar um comentário

E depois que seu código funciona, o que mais ele pode fazer?

Conhecer um idioma e se comunicar bem nele são a mesma coisa? Nem sempre, né? Você pode saber vocabulário, gramática, e até gírias, mas se n...