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