Alternatives à Rails Observer pour 4.0

154

Avec Observers officiellement retiré de Rails 4.0 , je suis curieux de savoir ce que les autres développeurs utilisent à leur place. (Autre que l'utilisation de la gemme extraite.) Alors que les observateurs étaient certainement maltraités et pouvaient facilement devenir parfois difficiles à manier, il y avait de nombreux cas d'utilisation en dehors du simple effacement de cache où ils étaient bénéfiques.

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

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

Personnellement, je penche vers une sorte d'implémentation de "gros contrôleur", où ces changements sont suivis dans la méthode de création / mise à jour / suppression de chaque contrôleur de modèles. Bien que cela gonfle légèrement le comportement de chaque contrôleur, cela aide à la lisibilité et à la 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é dans plusieurs contrôleurs. Extraire ce code dans des méthodes d'assistance est une option, mais il vous reste toujours des appels à ces méthodes éparpillés partout. Pas la fin du monde, mais pas tout à fait dans l'esprit des "maigres contrôleurs" non plus.

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

Donc, dans le monde Rails 4, sans observateurs, si vous deviez créer un nouvel enregistrement après qu'un autre enregistrement a été créé / mis à jour / détruit, quel modèle de conception utiliseriez-vous? De gros contrôleurs, des rappels ActiveRecord ou autre chose?

Je vous remercie.

Kennyc
la source
4
Je suis vraiment surpris qu'il n'y ait pas plus de réponses publiées pour cette question. Un peu déconcertant.
courtsimas

Réponses:

82

Jetez un œil aux préoccupations

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

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.

OncleAdam
la source
20
Il y a des problèmes avec cette approche. Notamment, il ne nettoie pas vos modèles; include copie les méthodes du module dans votre classe. Extraire des méthodes de classe dans un module peut les regrouper par souci, mais la classe est toujours aussi gonflée.
Steven Soroka
15
Le titre est «Rails Observer Alternatives for 4.0» et non «Comment minimiser les ballonnements». Comment se fait-il que les préoccupations ne fassent pas le travail de Steven? Et non, suggérer que le `` ballonnement '' est une raison pour laquelle cela ne fonctionnera pas en remplacement des observateurs n'est pas suffisant. Vous devrez trouver une meilleure suggestion pour aider la communauté ou expliquer pourquoi les préoccupations ne fonctionneront pas en remplacement des observateurs. J'espère que vous déclarerez les deux = D
UncleAdam
10
Le ballonnement est toujours une préoccupation. Une meilleure alternative est plus wisper , qui, si elle est mise en œuvre correctement, vous permet de nettoyer les problèmes en les extrayant dans des classes séparées qui ne sont pas étroitement couplées aux modèles. Cela rend également beaucoup plus facile le test isolé
Steven Soroka
4
Modélisez le gonflement ou le gonflement de l'application entière en tirant sur une gemme pour ce faire - nous pouvons laisser cela aux préférences individuelles. Merci pour la suggestion supplémentaire.
UncleAdam
Cela ne ferait que gonfler le menu de saisie semi-automatique de la méthode IDE, ce qui devrait convenir à de nombreuses personnes.
lulalala
33

Ils sont maintenant dans un plugin .

Puis-je également recommander une 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
Kris
la source
Qu'en est-il d'ActiveSupport :: Notifications?
svoop
@svoop ActiveSupport::Notificationssont orientés vers l'instrumentation, pas les sous / pub génériques.
Kris
@Kris - vous avez raison. Il est principalement utilisé pour l'instrumentation, mais je me demande ce qui l'empêche d'être utilisé comme méthode générique pour pub / sub? il fournit les éléments de base, non? En d'autres termes, quels sont les avantages / inconvénients à plus sifflant par rapport à ActiveSupport::Notifications?
gingerlime
Je n'ai pas Notificationsbeaucoup utilisé, mais je dirais que j'ai Wisperune API et des fonctionnalités plus agréables telles que «abonnés globaux», «sur le préfixe» et «mappage d'événements», ce qui Notificationsn'est pas le cas. Une future version de Wisperpermettra également la publication asynchrone via SideKiq / Resque / Celluloid. De plus, potentiellement, dans les futures versions de Rails, l'API de Notificationspourrait changer pour être plus axée sur l'instrumentation.
Kris
21

Ma suggestion est de lire l'article de blog de James Golick à http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (essayez d'ignorer comment impudique le titre sonne).

À l'époque, tout était "gros modèle, contrôleur maigre". Ensuite, les gros modèles sont devenus un casse-tête géant, en particulier lors des tests. Plus récemment, la poussée a été pour les modèles maigres - l'idée étant que chaque classe devrait assumer une responsabilité et que le travail d'un modèle est de 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 bourbier (giggity) lorsque la logique commence à se compliquer. Le concept est cependant valable - au lieu de déclencher des choses implicitement avec des rappels ou des observateurs difficiles à tester et à déboguer, déclenchez les choses explicitement dans une classe qui superpose la logique à votre modèle.

MikeJ
la source
4
J'ai fait quelque chose comme ça pour un projet ces derniers mois. Vous vous retrouvez avec beaucoup de petits services, mais la facilité de les tester et de les maintenir l'emporte définitivement sur les inconvénients. Mes spécifications assez étendues sur ce système de taille moyenne ne prennent toujours que 5 secondes pour fonctionner :)
Luca Spiller
Aussi connu sous PORO (Plain Old Ruby Objects), ou objets de service
Cyril Duchon-Doris
13

L'utilisation de rappels d'enregistrement actifs inverse simplement la dépendance de votre couplage. Par exemple, si vous avez modelAun style et des rails d' CacheObserverobservation modelA3, vous pouvez le supprimer CacheObserversans problème. Maintenant, dites à la place Ad'invoquer manuellement la CacheObserversauvegarde après, qui serait des rails 4. Vous avez simplement déplacé votre dépendance pour pouvoir la supprimer en toute sécurité, Amais pas CacheObserver.

Maintenant, depuis ma tour d'ivoire, je préfère que l'observateur dépende du modèle qu'il observe. Est-ce que je m'en soucie suffisamment pour encombrer mes contrôleurs? Pour moi, la réponse est non.

Vraisemblablement, vous avez réfléchi aux raisons pour lesquelles vous voulez / avez besoin de l'observateur, et donc créer un modèle dépendant de son observateur n'est pas une terrible tragédie.

J'ai aussi un dégoût (raisonnablement fondé, je pense) pour toute sorte d'observateur dépendant de l'action d'un contrôleur. Tout à coup, vous devez injecter à votre observateur toute action de contrôleur (ou un autre modèle) qui peut 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, plus de puissance pour vous, mais ce n'est pas une hypothèse que je ferais à propos d'une application rails (envisagez des formulaires imbriqués, des associations de mise à jour de la logique métier de modèle, etc.)

agmin
la source
1
Merci pour les commentaires @agmin. Je suis heureux de ne plus utiliser un observateur s'il existe un meilleur modèle de conception. Je suis plus intéressé par la manière dont les autres structurent leur code et leurs dépendances pour fournir des fonctionnalités similaires (à l'exclusion de la mise en cache). Dans mon cas, j'aimerais enregistrer les modifications apportées à un modèle chaque fois que ses attributs sont mis à jour. J'avais l'habitude d'utiliser un observateur pour faire cela. J'essaye maintenant de choisir entre un gros contrôleur, un rappel AR ou quelque chose d'autre auquel je n'avais pas pensé. Ni l'un ni l'autre ne semble élégant pour le moment.
kennyc
13

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 arrive, c'est-à-dire que je ne veux pas que les rappels soient déclenchés pendant que je configure des modèles dans les tests, etc. mais je les veux tiré chaque fois que les contrôleurs sont impliqués. C'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
opsb
la source
9

Dans certains cas, j'utilise simplement l' instrumentation de support actif

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
Panique
la source
4

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

Mes objets héritent d'une classe de base qui permet d'enregistrer les 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 la composition plutôt que de l'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 ActiveRecord de base. 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'inscrire avec le 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 sur plusieurs modèles.

Les classes d'observateurs NotificationSender et ProfilePictureCreator ci-dessous définissent des méthodes pour les événements exposés par divers 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

Une mise en garde est que les noms de tous les événements exposés dans tous les modèles doivent être uniques.

Mark Schneider
la source
3

Je pense que le problème avec les observateurs étant obsolètes 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 simplement déplacer du code pour simuler le comportement d'un observateur alors qu'il existe déjà une solution solide à ce problème, le modèle Observer.

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

Le gem observer 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 valable. https://github.com/rails/rails-observers/issues/2

Hraynaud
la source
2

Pourquoi ne pas utiliser un PORO à la place?

La logique derrière cela est que vos «actions supplémentaires lors de l'enregistrement» seront probablement une logique métier. Ceci que j'aime garder séparé à la fois des modèles 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 supplémentaires après l'enregistrement

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

Et pour donner un exemple des «extras». Vous voudrez peut-être les épeler un peu:

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

Si vous aimez cette approche, je vous recommande de lire l'article de blog de Bryan Helmkamps 7 Patterns .

EDIT: Je dois également mentionner que la solution ci-dessus permet également d'ajouter une logique de transaction en cas de besoin. 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
Houen
la source
-2

J'ai le même problème! Je trouve une solution ActiveModel :: Dirty pour que vous puissiez suivre les changements de votre modèle!

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
la source