Um pouco sobre Observers em Rails

Existem algumas formas de deixarmos nosso código mais limpo, organizado e mais fácil de testar. Observers é só o início desse caminho, que no final, acaba entrando nos corações dos desenvolvedores Ruby.

O que é isso?

"Observers" em Rails são uma maneira de separar os "callbacks" de um determinado Model em uma Classe específica, deixando o código dos seus Modelos mais limpos, organizados e sem métodos desnecessários e sem sentidos de serem mantidos dentro destes arquivos.

Por exemplo, você quer enviar um email para todos os contribuintes de um projeto específico na sua plataforma de "Crowdfunding". Vocês pode fazer o seguinte:

class PaymentObserver < ActiveRecord::Observer
  observe :contribution #NOTA: você pode dizer o que ele irá observar

  def after_create(resource)
    NotificationMailer.confirm(user.email).deliver
  end

end

Ao invés de colocar todos os "callbacks" dentro do model, poluindo e criando confusão, você abstrai eles para uma Classe na qual herda de ActiveRecord::Observer. Apenas um detalhe aqui, os métodos originais como por exemplo: before_save, before_update... são suportados por padrão, mas é possível criar "callbacks" customizados, explicarei mais para frente.

Another important detail is that the Observers created within of this class are executed after the original callbacks defined within the model, this includes the already told before_[action] and the relation methods like dependent: :destroy, so be careful to write your methods to avoid problems with existent methods.

Outro detalhe importante aqui é que os "Observers" criados dentro desta classe são executados DEPOIS dos "callbacks" originais definidos dentro do arquivo do Modelo, isso inclui os já citados before_[alguma coisa] e os métodos de relação, como por exemplo, dependent: :destroy, então seja cuidadoso ao sobrescrever métodos para não gerar problemas de conflito.

Setup

Adicione ao seu Gemfile:

gem 'rails-observers'

Dentro de application.rb adicione esta linha: (Aqui estou criando um array de símbolos)

config.active_record.observers = %i(
    authorization_observer
    contact_observer
    contribution_observer
    match_observer
    notification_observer
    payment_observer
    project_observer
    update_observer
    user_observer
  )

Rode bundle

Originalmente a documentação recomenda que os "Observers" sejam declarados dentro da pasta app/models, mas eu prefiro coloca-los dentro de uma pasta chamada app/observers para uma melhor organização do código.

Testando seus Observers

Ok, agora que você já implementou seus novos "Observers" é hora de testa-los. Mas como?
Testes são relativamente simples, há algumas formas de fazer isso, alguns preferem testar os métodos e seus resultados diretamente no Spec do Modelo, outros preferem criar cada Spec para cada Classe de "Observers" (O que na minha opinião é o caminho mais correto e sábio):

describe PaymentObserver do
  before :each do
    @payment  = stub_model(Payment)
    @observer = PaymentObserver.instance
  end

  it "should invoke after_create on the observed object" do
    @payment.should_receive(:set_status).with("aha!")
    @observer.after_create(@payment)
  end
end

Callbacks/Triggers Customizáveis

Digamos que você está uma Gem que necessite de "Callbacks" customizáveis, por exemplo a Gem state_machine. Eu quero adicionar um "Trigger" (não confunda com "Callback", este aqui serve para CHAMAR o "Callback") para quando algum objeto passar de um estado "pendente" para "confirmado". Como eu faço isso?

Um dos modos é criar um ActiveRecord::Concern onde serão incluídas algumas coisas no modelo em questão. Podemos fazer isso criando um modulo e importando-o no modelo.

module StateMachineHandler
  extend ActiveSupport::Concern

  included do
    state_machine :state, initial: :pendent do
      state :pendent
      state :confirmed

      event :push_to_pendent do
        transition all => :pendent
      end

      event :push_to_online do
        transition pendent: :confirmed
      end

      after_transition do |payment, transition|
        payment.notify_observers :"from_#{transition.from}_to_#{transition.to}"
      end
    end
  end

end

Ok, neste caso o que eu fiz foi criar um módulo chamado StateMachineHandler e dentro dele colocar um bloco chamado included do, que irá jogar todos os métodos declarados para dentro do Modelo quando este for carregado. Entretanto, oberver a linha contendo after_transition, dentro dela há um método do objeto Project chamado notify_observers. Este comando irá notificar o observador deste objeto quando ele alterar o status (de pendente para confirmado).

O próximo passo é criar um "Callback" para esta notificação, chamado neste caso de from_pendent_to_confirmed dentro de PaymentObserver que irá enviar um Mailer para o pagador, da seguinte maneira:

class PaymentObserver < ActiveRecord::Observer
  observe :contribution # you can tell what to observe

  def after_create(resource)
  NotificationMailer.confirm(user.email).deliver
  end

  def from_pendent_to_confirmed(resource)
    NotificatioMailer.notify_confirmed(user.email)
  end

end

Por último, mas não menos importante, dentro do modelo é necessário incluir o Concern que criamos (StateMachineHandler) para que os métodos e Triggers que criamos sejam incluídos ao Modelo:

class Payment < ActiveRecord::Base
  include StateMachineHandler #Include this line

  [..code]
end

Callbacks Padrão do Rails

Aqui estão todos os "Callbacks" disponíveis por padrão inclusos no ActiveRecord. Você pode verificar como usa-los e algumas outras informações neste link.

after_initialize
after_find
after_touch
before_validation
after_validation
before_save
around_save
after_save
before_create
around_create
after_create
before_update
around_update
after_update
before_destroy
around_destroy
after_destroy
after_commit
after_rollback

OBS.: Pra quem não entendeu a imagem de cover, é de um seriado Sci-Fichamado Fringe que inclusive eu recomendo muito! :D

Autor:
Patrick muller
Patrick Müller
Líder Tecnológico na COSMIT. Já trabalhou com planejamento e desenvolvimento de diversas Startups ao longo dos últimos anos. Adora animes, desenhos antigos e aproveita boa parte do tempo livre em busca de conhecimento que possa ajudar seus clientes a atingirem seus objetivos.
Compartilhe: