Alternativas do Rails Observer para 4.0

154

Com os Observadores removidos oficialmente do Rails 4.0 , estou curioso para saber o que outros desenvolvedores estão usando em seu lugar. (Além de usar a gema extraída.) Embora os Observadores certamente tenham sido abusados ​​e possam se tornar facilmente difíceis às vezes, havia muitos casos de uso fora da limpeza de cache, onde eles eram benéficos.

Pegue, por exemplo, um aplicativo que precise rastrear alterações em um modelo. Um observador pode facilmente observar as alterações no modelo A e registrar essas alterações no modelo B no banco de dados. Se você quisesse observar mudanças em vários modelos, um único observador poderia lidar com isso.

No Rails 4, estou curioso sobre quais estratégias outros desenvolvedores estão usando no lugar do Observers para recriar essa funcionalidade.

Pessoalmente, estou inclinado a uma espécie de implementação de "controlador de gordura", onde essas alterações são rastreadas no método de criação / atualização / exclusão de cada controlador de modelo. Embora inche levemente o comportamento de cada controlador, ele ajuda na legibilidade e compreensão, pois todo o código está em um só lugar. A desvantagem é que agora existe um código muito semelhante, espalhado por vários controladores. A extração desse código em métodos auxiliares é uma opção, mas você ainda recebe chamadas para esses métodos espalhados por toda parte. Não é o fim do mundo, mas também não está no espírito dos "controladores magros".

Os retornos de chamada ActiveRecord são outra opção possível, embora eu pessoalmente não goste, pois tende a acoplar dois modelos diferentes muito próximos na minha opinião.

Portanto, no mundo do Rails 4, sem observadores, se você tivesse que criar um novo registro depois que outro registro fosse criado / atualizado / destruído, que padrão de design você usaria? Controladores de gordura, retornos de chamada ActiveRecord ou algo totalmente diferente?

Obrigado.

kennyc
fonte
4
Estou realmente surpreso por não haver mais respostas postadas para esta pergunta. Meio desconcertante.
Tribunais

Respostas:

82

Dê uma olhada no Preocupações

Crie uma pasta no diretório de modelos chamada preocupações. Adicione um módulo lá:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

A seguir, inclua nos modelos em que você deseja executar o after_save em:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

Dependendo do que você está fazendo, isso pode te aproximar sem observadores.

TioAdão
fonte
20
Existem problemas com essa abordagem. Notavelmente, ele não limpa seus modelos; inclua copia os métodos do módulo de volta para sua classe. Extrair métodos de classe para um módulo pode agrupá-los por preocupação, mas a classe ainda é inchada.
18716 Steven Soroka
15
O título é 'Alternativas do Rails Observer para 4.0' e não 'Como minimizar o inchaço'. Como é que as preocupações não fazem o trabalho, Steven? E não, sugerir que 'inchaço' é uma razão pela qual isso não funcionará como um substituto para os observadores não é bom o suficiente. Você terá que sugerir melhor para ajudar a comunidade ou explicar por que as preocupações não funcionarão como um substituto para os observadores. Espero que você indique os dois = D
UncleAdam 13/03/2014
10
Bloat é sempre uma preocupação. Uma alternativa melhor é o mais inteligente , que, se implementado corretamente, permite limpar as preocupações, extraindo-as para classes separadas que não estão totalmente acopladas aos modelos. Isso também facilita muito o teste isolado
Steven Soroka 28/03
4
Inchaço do modelo ou Inchaço do aplicativo inteiro, puxando uma gema para fazer isso - podemos deixar a preferência individual. Obrigado pela sugestão adicional.
UncleAdam
Seria apenas o menu de preenchimento automático do método do IDE, o que deve ser bom para muitas pessoas.
Lulalala
33

Eles estão em um plugin agora.

Também posso recomendar uma alternativa que fornecerá controladores como:

class PostsController < ApplicationController
  def create
    @post = Post.new(params[:post])

    @post.subscribe(PusherListener.new)
    @post.subscribe(ActivityListener.new)
    @post.subscribe(StatisticsListener.new)

    @post.on(:create_post_successful) { |post| redirect_to post }
    @post.on(:create_post_failed)     { |post| render :action => :new }

    @post.create
  end
end
Kris
fonte
E quanto ao ActiveSupport :: Notifications?
svoop 03/03
O @svoop ActiveSupport::Notificationsé voltado para a instrumentação, não para o sub / pub genérico.
Kris
@Kris - você está certo. Ele é usado principalmente para instrumentação, mas eu me pergunto o que impede que ele seja usado como um método genérico para pub / sub? fornece os blocos de construção básicos, certo? Em outras palavras, quais são as vantagens / desvantagens do wisper em comparação ActiveSupport::Notifications?
gingerlime
Eu não usei Notificationsmuito, mas eu diria que Wispertem uma API melhor e recursos como 'assinantes globais', 'no prefixo' e 'mapeamento de eventos' que Notificationsnão. Uma versão futura do Wispertambém permitirá a publicação assíncrona via SideKiq / Resque / Celluloid. Além disso, potencialmente, em versões futuras do Rails, a API Notificationspode mudar para se concentrar mais na instrumentação.
Kris
21

Minha sugestão é ler o post de James Golick em http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (tente ignorar como indecente o título).

Voltar no dia era tudo "modelo gordo, controlador magro". Então os modelos gordos se tornaram uma dor de cabeça gigante, especialmente durante os testes. Mais recentemente, a tendência foi para modelos magros - a idéia é que cada classe deve lidar com uma responsabilidade e o trabalho de um modelo é manter seus dados em um banco de dados. Então, onde termina toda a minha lógica de negócios complexa? Nas classes de lógica de negócios - classes que representam transações.

Essa abordagem pode se transformar em um atoleiro (giggity) quando a lógica começa a ficar complicada. Porém, o conceito é sólido - em vez de desencadear coisas implicitamente com retornos de chamada ou observadores difíceis de testar e depurar, desencadeie coisas explicitamente em uma classe que coloca a lógica em cima do seu modelo.

MikeJ
fonte
4
Eu venho fazendo algo assim para um projeto nos últimos meses. Você acaba com muitos serviços pequenos, mas a facilidade de testar e mantê-lo definitivamente supera as desvantagens. Meus bastante extensa especificações sobre este sistema de tamanho médio ainda levar apenas 5 segundos para executar :)
Luca Spiller
Também conhecida como Poro (Plain Old Rubi Objects), ou serviços objetos
Cyril Duchon-Doris
13

O uso de retornos de chamada de registro ativos simplesmente inverte a dependência do seu acoplamento. Por exemplo, se você possui modelAum estilo de CacheObserverobservação de modelAtrilhos 3, pode removê CacheObserver-lo sem problemas. Agora, diga que Aprecisa chamar manualmente o CacheObserverafter save, que seria o trilhos 4. Você simplesmente mudou sua dependência para poder remover com segurança, Amas não o faz CacheObserver.

Agora, da minha torre de marfim, prefiro que o observador seja dependente do modelo que está observando. Eu me importo o suficiente para bagunçar meus controladores? Para mim, a resposta é não.

Presumivelmente, você pensou um pouco sobre por que deseja / precisa do observador e, portanto, criar um modelo dependente dele não é uma tragédia terrível.

Também tenho uma aversão (razoavelmente fundamentada, eu acho) por qualquer tipo de observador dependente de uma ação do controlador. De repente, você deve injetar seu observador em qualquer ação do controlador (ou outro modelo) que possa atualizar o modelo que você deseja observar. Se você pode garantir que seu aplicativo apenas modifique instâncias por meio de ações de criação / atualização de controlador, mais poder será para você, mas isso não é uma suposição que eu faria sobre um aplicativo rails (considere formulários aninhados, modele associações de atualização de lógica de negócios etc.)

agmin
fonte
1
Obrigado pelos comentários @agmin. Fico feliz em deixar de usar um Observer se houver um melhor padrão de design por aí. Estou mais interessado em saber como outras pessoas estão estruturando seu código e dependências para fornecer funcionalidade semelhante (excluindo o cache). No meu caso, eu gostaria de registrar as alterações em um modelo sempre que seus atributos forem atualizados. Eu costumava usar um observador para fazer isso. Agora estou tentando decidir entre um controlador de gordura, retorno de chamada de AR ou outra coisa em que eu não tinha pensado. Nenhum dos dois parece elegante no momento.
kennyc
13

O Wisper é uma ótima solução. Minha preferência pessoal por retornos de chamada é que eles são acionados pelos modelos, mas os eventos são ouvidos apenas quando uma solicitação é recebida, ou seja, eu não quero que os retornos sejam acionados enquanto estou configurando modelos em testes, etc. acionado sempre que houver controladores envolvidos. Isso é realmente fácil de configurar com o Wisper, porque você pode dizer para ouvir apenas eventos dentro de um bloco.

class ApplicationController < ActionController::Base
  around_filter :register_event_listeners

  def register_event_listeners(&around_listener_block)
    Wisper.with_listeners(UserListener.new) do
      around_listener_block.call
    end
  end        
end

class User
  include Wisper::Publisher
  after_create{ |user| publish(:user_registered, user) }
end

class UserListener
  def user_registered(user)
    Analytics.track("user:registered", user.analytics)
  end
end
opsb
fonte
9

Em alguns casos, eu simplesmente uso a Instrumentação de Suporte Ativo

ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
  # do your stuff here
end

ActiveSupport::Notifications.subscribe "my.custom.event" do |*args|
  data = args.extract_options! # {:this=>:data}
end
Pânico
fonte
4

Minha alternativa ao Rails 3 Observers é uma implementação manual que utiliza um retorno de chamada definido dentro do modelo, mas consegue (como agmin afirma em sua resposta acima) "inverter a dependência ... o acoplamento".

Meus objetos herdam de uma classe base que fornece o registro de observadores:

class Party411BaseModel

  self.abstract_class = true
  class_attribute :observers

  def self.add_observer(observer)
    observers << observer
    logger.debug("Observer #{observer.name} added to #{self.name}")
  end

  def notify_observers(obj, event_name, *args)
    observers && observers.each do |observer|
    if observer.respond_to?(event_name)
        begin
          observer.public_send(event_name, obj, *args)
        rescue Exception => e
          logger.error("Error notifying observer #{observer.name}")
          logger.error e.message
          logger.error e.backtrace.join("\n")
        end
    end
  end

end

(Concedido, no espírito de composição sobre herança, o código acima pode ser colocado em um módulo e misturado em cada modelo.)

Um inicializador registra observadores:

User.add_observer(NotificationSender)
User.add_observer(ProfilePictureCreator)

Cada modelo pode definir seus próprios eventos observáveis, além dos retornos de chamada básicos do ActiveRecord. Por exemplo, meu modelo de usuário expõe 2 eventos:

class User < Party411BaseModel

  self.observers ||= []

  after_commit :notify_observers, :on => :create

  def signed_up_via_lunchwalla
    self.account_source == ACCOUNT_SOURCES['LunchWalla']
  end

  def notify_observers
    notify_observers(self, :new_user_created)
    notify_observers(self, :new_lunchwalla_user_created) if self.signed_up_via_lunchwalla
  end
end

Qualquer observador que deseja receber notificações para esses eventos apenas precisa (1) se registrar no modelo que expõe o evento e (2) ter um método cujo nome corresponda ao evento. Como seria de esperar, vários observadores podem se registrar para o mesmo evento e (em referência ao segundo parágrafo da pergunta original) um observador pode observar eventos em vários modelos.

As classes de observador NotificationSender e ProfilePictureCreator abaixo definem métodos para os eventos expostos por vários modelos:

NotificationSender
  def new_user_created(user_id)
    ...
  end

  def new_invitation_created(invitation_id)
    ...
  end

  def new_event_created(event_id)
    ...
  end
end

class ProfilePictureCreator
  def new_lunchwalla_user_created(user_id)
    ...
  end

  def new_twitter_user_created(user_id)
    ...
  end
end

Uma ressalva é que os nomes de todos os eventos expostos em todos os modelos devem ser exclusivos.

Mark Schneider
fonte
3

Penso que o problema dos observadores serem preteridos não é que os observadores eram maus por si mesmos, mas que estavam sendo abusados.

Eu recomendaria não adicionar muita lógica em seus retornos de chamada ou simplesmente mover o código para simular o comportamento de um observador quando já houver uma solução sólida para esse problema, o padrão Observador.

Se faz sentido usar observadores, use todos os meios. Apenas entenda que você precisará garantir que a lógica do observador siga práticas de codificação de som, por exemplo, o SOLID.

A jóia do observador está disponível em rubygems se você quiser adicioná-la novamente ao seu projeto https://github.com/rails/rails-observers

veja esta breve discussão, embora não seja uma discussão abrangente e completa, acho que o argumento básico é válido. https://github.com/rails/rails-observers/issues/2

hraynaud
fonte
2

Que tal usar um PORO?

A lógica por trás disso é que suas 'ações extras ao salvar' provavelmente serão uma lógica de negócios. Eu gosto de me manter separado dos modelos de AR (que devem ser o mais simples possível) e dos controladores (que são incômodos para testar adequadamente)

class LoggedUpdater

  def self.save!(record)
    record.save!
    #log the change here
  end

end

E simplesmente chame assim:

LoggedUpdater.save!(user)

Você pode até expandi-lo injetando objetos de ação pós-salvamento extras

LoggedUpdater.save(user, [EmailLogger.new, MongoLogger.new])

E para dar um exemplo dos 'extras'. Você pode querer aumentá-los um pouco:

class EmailLogger
  def call(msg)
    #send email with msg
  end
end

Se você gosta dessa abordagem, recomendo a leitura da postagem no blog Bryan Helmkamps 7 Patterns .

Edição: devo mencionar também que a solução acima permite adicionar lógica de transação, bem quando necessário. Por exemplo, com ActiveRecord e um banco de dados suportado:

class LoggedUpdater

  def self.save!([records])
    ActiveRecord::Base.transaction do
      records.each(&:save!)
      #log the changes here
    end
  end

end
Houen
fonte
-2

Eu tenho o mesmo probjem! Encontro uma solução ActiveModel :: Dirty para que você possa acompanhar as alterações no seu modelo!

include ActiveModel::Dirty
before_save :notify_categories if :data_changed? 


def notify_categories
  self.categories.map!{|c| c.update_results(self.data)}
end

http://api.rubyonrails.org/classes/ActiveModel/Dirty.html

msroot
fonte