web-dev-qa-db-fra.com

Rails Alternatives à Observer pour 4.0

Avec Observers officiellement retiré de Rails 4. , je suis curieux de savoir ce que les autres développeurs utilisent à leur place. (Autre que l’utilisation de la gem extraite.) maltraités et susceptibles de devenir parfois trop lourds, il existait de nombreux cas d'utilisation autres que le nettoyage de cache où ils étaient bénéfiques.

Prenons, par exemple, une application qui doit suivre les modifications apportées à un modèle. Un observateur pourrait facilement surveiller les modifications sur le modèle A et enregistrer ces modifications avec le modèle B dans la base de données. Si vous souhaitez surveiller les modifications sur plusieurs modèles, un seul observateur peut le gérer.

Dans Rails 4, je suis curieux de savoir quelles stratégies les développeurs utilisent à la place des observateurs pour recréer cette fonctionnalité.

Personnellement, je penche pour une sorte d'implémentation de "contrôleur de graisse", dans laquelle ces modifications sont suivies dans la méthode de création/mise à jour/suppression de chaque contrôleur de modèle. Bien que cela gêne légèrement le comportement de chaque contrôleur, cela aide en termes de lisibilité et de compréhension car tout le code est au même endroit. L'inconvénient est qu'il existe maintenant un code très similaire dispersé sur plusieurs contrôleurs. Extraire ce code dans des méthodes d'assistance est une option, mais il reste toujours des appels à ces méthodes jonchées partout. Pas la fin du monde, mais pas tout à fait dans l'esprit de "contrôleurs maigres" non plus.

Les rappels ActiveRecord sont une autre option possible, bien que je n'aime pas personnellement cela, car il a tendance à coupler deux modèles différents de manière trop étroite, à mon avis.

Ainsi, dans le monde Rails 4, no-Observers, si vous deviez créer un nouvel enregistrement après la création/la mise à jour/la destruction d'un autre enregistrement, quel modèle de conception utiliseriez-vous? Contrôleurs de graisse, rappels ActiveRecord , ou quelque chose d'autre entièrement?

Merci.

149
kennyc

Jetez un oeil à Préoccupations

Créez un dossier dans votre répertoire de modèles appelé préoccupations. Ajoutez un module ici:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

Ensuite, incluez cela dans les modèles dans lesquels vous souhaitez exécuter after_save:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

Selon ce que vous faites, cela pourrait vous rapprocher sans observateurs.

77
UncleAdam

Ils sont dans un plugin maintenant.

Puis-je également recommander ne alternative qui vous donnera des contrôleurs comme:

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
33
Kris

Ma suggestion est de lire le billet de blog de James Golick à l'adresse http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-Rails-apps.html (essayez d'ignorer à quel point le titre semble immodeste).

À l'époque, tout était "gros modèle, contrôleur maigre". Ensuite, les gros modèles sont devenus un mal de tête géant, en particulier lors des tests. Plus récemment, le Push a été pour les modèles maigres - l'idée étant que chaque classe devrait gérer une responsabilité et que le travail d'un modèle consiste à conserver vos données dans une base de données. Alors, où finit toute ma logique métier complexe? Dans les classes de logique métier - classes qui représentent des transactions.

Cette approche peut se transformer en un bourbier lorsque la logique commence à se compliquer. Le concept est cependant valable: au lieu de déclencher implicitement des choses avec des callbacks ou des observateurs difficiles à tester et à déboguer, déclenchez les choses explicitement dans une classe superposant la logique au-dessus de votre modèle.

21
MikeJ

L'utilisation de rappels d'enregistrements actifs inverse simplement la dépendance de votre couplage. Par exemple, si vous avez modelA et un style CacheObserver observant modelA Rails 3, vous pouvez supprimer CacheObserver avec Maintenant, à la place, dites que A doit appeler manuellement le CacheObserver après la sauvegarde, ce qui serait Rails 4. Vous avez simplement déplacé votre dépendance pour vous pouvez supprimer en toute sécurité A mais pas CacheObserver.

Maintenant, de ma tour d’ivoire, je préfère que l’observateur soit dépendant du modèle qu’il observe. Est-ce que je me soucie assez d'encombrer mes contrôleurs? Pour moi, la réponse est non.

Vous avez probablement réfléchi à la raison pour laquelle vous voulez/avez besoin de l'observateur, et créer ainsi un modèle dépendant de son observateur n'est pas une terrible tragédie.

Je suis également réticent à l'idée que tout observateur soit tributaire d'une action du contrôleur. Du coup, vous devez injecter votre observateur dans toute action de contrôleur (ou un autre modèle) susceptible de mettre à jour le modèle que vous souhaitez observer. Si vous pouvez garantir que votre application ne modifiera jamais les instances que via des actions de création/mise à jour de contrôleur, vous bénéficierez de plus de pouvoir, mais ce n'est pas une hypothèse que je formulerais à propos d'une application Rails (considérez les formulaires imbriqués, associations de mise à jour de la logique métier, etc.)

13
agmin

Wisper est une excellente solution. Ma préférence personnelle pour les rappels est qu'ils sont déclenchés par les modèles, mais les événements ne sont écoutés que lorsqu'une demande est reçue, c.-à-d. Que je ne veux pas que les rappels soient déclenchés pendant la configuration des modèles dans les tests, etc. tiré lorsque des contrôleurs sont impliqués. Ceci est vraiment facile à configurer avec Wisper car vous pouvez lui dire de n'écouter que les événements à l'intérieur d'un bloc.

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
13
opsb

Dans certains cas, j'utilise simplement Active Support Instrumentation

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
8
Panic

Mon alternative à Rails 3 Observers est une implémentation manuelle qui utilise un rappel défini dans le modèle, mais parvient à (comme l'indique l'agmin dans sa réponse ci-dessus) "d'inverser la dépendance ... le couplage".

Mes objets héritent d'une classe de base permettant l'enregistrement d'observateurs:

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

(Certes, dans l’esprit de composition sur héritage, le code ci-dessus pourrait être placé dans un module et mélangé dans chaque modèle.)

Un initialiseur enregistre les observateurs:

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

Chaque modèle peut ensuite définir ses propres événements observables, au-delà des rappels de base d'ActiveRecord. Par exemple, mon modèle utilisateur expose 2 événements:

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

Tout observateur qui souhaite recevoir des notifications pour ces événements doit simplement (1) s'enregistrer auprès du modèle qui expose l'événement et (2) avoir une méthode dont le nom correspond à l'événement. Comme on pouvait s'y attendre, plusieurs observateurs peuvent s'inscrire pour le même événement et (en référence au deuxième paragraphe de la question initiale), un observateur peut surveiller les événements de plusieurs modèles.

Les classes d'observateur NotificationSender et ProfilePictureCreator ci-dessous définissent des méthodes pour les événements exposés par différents modèles:

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

Un inconvénient est que les noms de tous les événements exposés dans tous les modèles doivent être uniques.

4
Mark Schneider

Je pense que le problème avec les observateurs étant déconseillés n'est pas que les observateurs étaient mauvais en eux-mêmes, mais qu'ils étaient maltraités.

Je vous déconseille d'ajouter trop de logique dans vos rappels ou de déplacer simplement du code pour simuler le comportement d'un observateur lorsqu'il existe déjà une solution satisfaisante à ce problème, le modèle Observer.

S'il est judicieux d'utiliser des observateurs, utilisez-les. Comprenez simplement que vous devrez vous assurer que votre logique d’observateur suit des pratiques de codage saines, par exemple SOLID.

Le joyau de l’observateur est disponible sur rubygems si vous souhaitez le rajouter à votre projet https://github.com/Rails/rails-observers

voir ce bref fil, bien que pas une discussion complète complète, je pense que l'argument de base est valide. https://github.com/Rails/rails-observers/issues/2

3
hraynaud

Vous pouvez essayer https://github.com/TiagoCardoso1983/obsociation_observers . Il n'a pas encore été testé pour Rails 4 (qui n'a pas encore été lancé), et a besoin de plus de collaboration, mais vous pouvez vérifier si cela vous convient.

2
ChuckE

Que diriez-vous d'utiliser un PORO à la place?

La logique derrière cela est que vos "actions supplémentaires sur la sauvegarde" vont probablement être de la logique métier. C’est ce que j’aime séparer des modèles d’AR (qui devraient être aussi simples que possible) et des contrôleurs (qui sont gênants à tester correctement)

class LoggedUpdater

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

end

Et appelez-le simplement comme tel:

LoggedUpdater.save!(user)

Vous pouvez même le développer en injectant des objets d’action post-save supplémentaires

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

Et pour donner un exemple des "extras". Vous voudrez peut-être les épater un peu:

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

Si vous aimez cette approche, je vous recommande une lecture de Bryan Helmkamps 7 Patterns blog.

EDIT: Je devrais également mentionner que la solution ci-dessus permet d’ajouter une logique de transaction si nécessaire. Par exemple. avec ActiveRecord et une base de données prise en charge:

class LoggedUpdater

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

end
1
Houen

Il est à noter que Observable module de Ruby ne peut pas être utilisée dans des objets de type enregistrement actif, car les méthodes d'instance changed? et changed entreront en conflit avec ceux de ActiveModel::Dirty .

Rapport de bug pour Rails 2.3.2

0
Artur Beljajev