Rails Observer Alternatives для 4.0
Когда наблюдатели официально удалены из Rails 4.0, мне любопытно, что другие разработчики используют вместо них. (За исключением использования извлеченного драгоценного камня.) Хотя наблюдатели, безусловно, злоупотребляли и могли легко становиться неудобно время от времени, было много случаев использования за пределами очистки только кеша, где они были полезны.
Возьмем, например, приложение, которое должно отслеживать изменения модели. Наблюдатель мог легко следить за изменениями в модели A и записывать эти изменения с помощью модели B в базе данных. Если вы хотите следить за изменениями в нескольких моделях, то один наблюдатель может это обработать.
В Rails 4 мне любопытно, какие стратегии используют другие разработчики вместо Observers для воссоздания этой функциональности.
Лично я склоняюсь к реализации "жирного контроллера", где эти изменения отслеживаются в каждом контроллере моделей, создавая/обновляя/удаляя метод. Хотя он слегка раздувает поведение каждого контроллера, он помогает в удобочитаемости и понимании, поскольку весь код находится в одном месте. Недостатком является то, что теперь код, который очень похож, разбросан по нескольким контроллерам. Выделение этого кода в вспомогательные методы является опцией, но вы по-прежнему остаетесь с вызовами тех методов, которые усеяны повсюду. Не конец света, но не совсем в духе "тощих контроллеров".
Обратные вызовы ActiveRecord - еще один возможный вариант, хотя мне лично не нравится, поскольку, как мне кажется, имеет тенденцию связывать две разные модели слишком близко друг к другу.
Итак, в Rails 4, мире без наблюдателей, если вам нужно было создать новую запись после создания/обновления/уничтожения другой записи, какой шаблон дизайна вы бы использовали? Жирные контроллеры, обратные вызовы ActiveRecord или что-то еще?
Спасибо.
Ответы
Ответ 1
Взгляните на Concerns
Создайте папку в каталоге моделей, вызывающую проблемы. Добавьте туда модуль:
module MyConcernModule
extend ActiveSupport::Concern
included do
after_save :do_something
end
def do_something
...
end
end
Затем включите, что в моделях вы хотите запустить after_save в:
class MyModel < ActiveRecord::Base
include MyConcernModule
end
В зависимости от того, что вы делаете, это может закрыть вас без наблюдателей.
Ответ 2
Теперь они находятся в плагине.
Могу ли я также рекомендовать альтернативу, которая даст вам такие контроллеры, как:
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
Ответ 3
Мое предложение - прочитать пост в блоге Джеймса Голика в http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (попробуйте проигнорировать, как нескромно звучит заголовок).
В тот же день это была "толстая модель, тощий контроллер". Тогда жирные модели стали гигантской головной болью, особенно во время тестирования. Совсем недавно толчок был для тощих моделей - идея состоит в том, что каждый класс должен обрабатывать одну ответственность, а модельная работа - сохранять ваши данные в базе данных. Так где же заканчивается вся моя сложная бизнес-логика? В классах бизнес-логики - классы, представляющие транзакции.
Этот подход может превратиться в трясину (гибкость), когда логика начинает усложняться. Концепция звучит, хотя - вместо того, чтобы запускать вещи неявно с помощью обратных вызовов или наблюдателей, которые трудно тестировать и отлаживать, явным образом запускает вещи в классе, в котором логика слоев поверх вашей модели.
Ответ 4
Использование активных обратных вызовов для записи просто переворачивает зависимость вашей связи. Например, если у вас есть стиль modelA
и CacheObserver
наблюдения modelA
rails 3, вы можете удалить CacheObserver
без проблем. Теперь вместо этого A
должен вручную вызвать CacheObserver
после сохранения, что будет рельсами 4. Вы просто переместили свою зависимость, чтобы вы могли безопасно удалить A
, но не CacheObserver
.
Теперь, с моей башни из слоновой кости, я предпочитаю, чтобы наблюдатель зависел от модели, которую она наблюдала. Достаточно ли я заботиться о том, чтобы загромождать мои контроллеры? Для меня ответ - нет.
Предположительно, вы задумались о том, почему вы хотите/нуждаетесь в наблюдателе, и, таким образом, создание модели, зависящей от ее наблюдателя, не является ужасной трагедией.
У меня также есть (разумно обоснованный, я думаю) отвращение к тому, что любой наблюдатель зависит от действия контроллера. Внезапно вам нужно ввести своего наблюдателя в любое действие контроллера (или другую модель), которое может обновить модель, которую вы хотите наблюдать. Если вы можете гарантировать, что ваше приложение будет только модифицировать экземпляры с помощью действий контроллера create/update, больше возможностей для вас, но это не предположение, которое я сделал бы о приложении rails (рассмотрите вложенные формы, ассоциации модификации бизнес-логики и т.д.)
Ответ 5
Wisper - отличное решение. Мое личное предпочтение обратных вызовов заключается в том, что они запускаются с помощью моделей, но события прослушиваются только при поступлении запроса, т.е. Я не хочу, чтобы обратные вызовы запускались, когда я настраивал модели в тестах и т.д., Но я действительно хочу их когда задействованы контроллеры. Это очень легко настроить с помощью Wisper, потому что вы можете сказать, что он только прослушивает события внутри блока.
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
Ответ 6
В некоторых случаях я просто использую 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
Ответ 7
Моя альтернатива Rails 3 Observers - это ручная реализация, которая использует обратный вызов, определенный в модели, которой все же удается (поскольку агмин утверждает в своем ответе выше) "перевернуть зависимость... связь".
Мои объекты наследуются от базового класса, который обеспечивает регистрацию наблюдателей:
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
(Конечно, в духе композиции над наследованием вышеупомянутый код может быть помещен в модуль и смешан в каждой модели.)
Инициализатор регистрирует наблюдателей:
User.add_observer(NotificationSender)
User.add_observer(ProfilePictureCreator)
Каждая модель может затем определить свои собственные наблюдаемые события, помимо базовых обратных вызовов ActiveRecord. Например, моя модель User предоставляет 2 события:
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
Любой наблюдатель, который хочет получать уведомления для этих событий, просто должен (1) зарегистрироваться в модели, которая предоставляет событие, и (2) иметь метод, имя которого соответствует событию. Как и следовало ожидать, несколько наблюдателей могут регистрироваться для одного и того же события и (со ссылкой на второй абзац исходного вопроса) наблюдатель может наблюдать за событиями в нескольких моделях.
В классах наблюдателей NotificationSender и ProfilePictureCreator ниже описаны методы для событий, выставленных различными моделями:
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
Одно из предостережений заключается в том, что имена всех событий, открытых во всех моделях, должны быть уникальными.
Ответ 8
Я думаю, что проблема с наблюдателями, которые устарели, заключается не в том, что наблюдатели плохо себя чувствуют, а в том, что их злоупотребляют.
Я бы посоветовал не добавлять слишком много логики в ваши обратные вызовы или просто перемещать код вокруг, чтобы имитировать поведение наблюдателя, когда уже есть звуковое решение этой проблемы в шаблоне Observer.
Если имеет смысл использовать наблюдателей, то, во всяком случае, используйте наблюдателей. Просто поймите, что вам нужно будет убедиться, что ваша логика наблюдателя следует за методами кодирования звука, например SOLID.
Драгоценный камень наблюдателя доступен на rubygems, если вы хотите добавить его обратно в свой проект
https://github.com/rails/rails-observers
см. эту краткую ветку, а не полное всестороннее обсуждение. Я думаю, что основной аргумент верен.
https://github.com/rails/rails-observers/issues/2
Ответ 9
Вы можете попробовать https://github.com/TiagoCardoso1983/association_observers. Он еще не тестировался на рельсы 4 (который еще не был запущен), и ему требуется еще больше совместной работы, но вы можете проверить, действительно ли это трюк для вас.
Ответ 10
Как насчет использования PORO?
Логика этого заключается в том, что ваши "дополнительные действия по спасению", скорее всего, будут бизнес-логикой. Мне нравится сохранять отдельно от обеих моделей AR (которые должны быть как можно более простыми) и контроллеров (которые надоедливо тестировать)
class LoggedUpdater
def self.save!(record)
record.save!
#log the change here
end
end
И просто назовите его как таковой:
LoggedUpdater.save!(user)
Вы можете даже расширить его, добавив дополнительные объекты после сохранения
LoggedUpdater.save(user, [EmailLogger.new, MongoLogger.new])
И привести пример "дополнительных". Возможно, вы захотите немного подтянуть их:
class EmailLogger
def call(msg)
#send email with msg
end
end
Если вам нравится этот подход, я рекомендую прочитать сообщение Bryan Helmkamps 7 Patterns.
EDIT: Я должен также упомянуть, что вышеупомянутое решение позволяет добавлять транзакционную логику, когда это необходимо. Например. с ActiveRecord и поддерживаемой базой данных:
class LoggedUpdater
def self.save!([records])
ActiveRecord::Base.transaction do
records.each(&:save!)
#log the changes here
end
end
end
Ответ 11
Следует отметить, что Observable
модуль из стандартной библиотеки Ruby не может использоваться в объектах с активной записью, поскольку методы экземпляров changed?
и changed
будут сталкиваться с теми из ActiveModel::Dirty
.
Отчет об ошибках для Rails 2.3.2
Ответ 12
У меня такой же пробджем! Я нахожу решение ActiveModel:: Dirty, поэтому вы можете отслеживать изменения модели!
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