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.
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.
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
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.
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.)
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
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
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.
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
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.
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
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
.