Pour éviter de rendre le fichier de modèle trop long, déplacez les callbacks dans un abonné.
Scénario et problème
- À mesure que les affaires deviennent plus complexes, il y a de plus en plus de cas où les modèles s’appellent les uns les autres.
- Cependant, dans la plupart des cas, encapsuler une couche de service est trop complexe.
- Par conséquent, un grand nombre d’opérations s’accumulent dans les callbacks, rendant le fichier de modèle de plus en plus long.
Solution
- Utilisez le modèle observateur pour écouter les déclencheurs de callbacks de modèle.
- Utilisez des conventions pour délimiter clairement les emplacements du code.
Exemple simple
Lors de la mise à jour du prix d’un produit, il est nécessaire de mettre à jour le prix des commandes associées.
class Product < ApplicationRecord
has_many :orders
after_update do
if price_changed?
orders.each do |order|
order.update! price: price
end
end
end
end
Après avoir utilisé un abonné
# ./app/subscribers/order_subscriber.rb
subscribe_model :product, :after_update do
if price_changed?
orders.each do |order|
order.update! price: price
end
end
end
Utilisation des conventions
- Seules les interactions d’affaires avec d’autres modèles doivent être écrites dans les abonnés.
- Les abonnés ne doivent pas modifier le modèle déclencheur.
- Le code d’affaires doit être placé dans l’abonné qui consomme le déclencheur.
- Si un seul abonné est trop long, il peut également être divisé en plusieurs abonnés en fonction des affaires, comme order_product_subscriber.rb.
Avantages
- Le fichier de modèle ne contient plus un grand nombre de callbacks.
- Il est clair quels autres modèles chaque modèle doit consommer.
- Les callbacks comme after_save sont toujours encapsulés dans une transaction.
- Prend en charge les callbacks des modèles secondaires (déclenche d’abord les callbacks du modèle secondaire, puis ceux de la classe de base).
- Permet d’écrire des tests individuels (basés sur ActiveSupport::Notifications).
Code principal
# ./config/initializers/subscribe_model.rb
def subscribe_model(model_name, event_name, &block)
ActiveSupport::Notifications.subscribe("active_record.#{model_name}.#{event_name}") do |_name, _started, _finished, _unique_id, data|
data[:model].instance_eval(&block)
end
end
class ActiveRecord::Base
class_attribute :skip_model_subscribers
self.skip_model_subscribers = false
end
%i(after_create after_update after_destroy after_save after_commit after_create_commit after_update_commit).each do |name|
ActiveRecord::Base.public_send(name) do
unless skip_model_subscribers
readonly! unless readonly?
ActiveSupport::Notifications.instrument "active_record.#{self.class.model_name.singular}.#{name}", model: self
ActiveSupport::Notifications.instrument "active_record.#{self.class.base_class.model_name.singular}.#{name}", model: self if self.class.base_class != self.class
public_send(:instance_variable_set, :@readonly, false)
end
end
end
Rails.application.config.after_initialize do
Dir[Rails.root.join('app/subscribers/*.rb')].each { |f| require f }
end
FAQ
Q: Pourquoi ne pas utiliser une préoccupation ?
R: Dans l’équipe, il est convenu que seuls les codes communs à deux modèles ou plus doivent être déplacés vers une préoccupation.
Q: Pourquoi ne pas créer une couche de service ?
R: La couche de service n’est pas assez intuitive, et pour les petites équipes, le coût de maintenance est plus élevé que celui d’un abonné.