Rails Observer Альтернативы для 4.0

154

С Observers официально удален из Rails 4.0 мне интересно, что другие разработчики используют вместо них. (Кроме использования извлеченного драгоценного камня.) Хотя Обозреватели, несомненно, подвергались жестокому обращению и иногда могли легко стать громоздкими, было много вариантов использования, помимо простой очистки кеша, где они были полезны.

Возьмем, к примеру, приложение, которое должно отслеживать изменения в модели. Наблюдатель может легко отслеживать изменения в модели A и записывать эти изменения с помощью модели B в базу данных. Если вы хотите следить за изменениями в нескольких моделях, то один наблюдатель может справиться с этим.

В Rails 4 мне любопытно, какие стратегии используют другие разработчики вместо Observers для воссоздания этой функциональности.

Лично я склоняюсь к некой реализации «жирного контроллера», где эти изменения отслеживаются в методе создания / обновления / удаления контроллера каждой модели. Хотя он слегка раздувает поведение каждого контроллера, он помогает в удобочитаемости и понимании, поскольку весь код находится в одном месте. Недостатком является то, что теперь существует очень похожий код, разбросанный по нескольким контроллерам. Извлечение этого кода в вспомогательные методы является опцией, но вы все равно будете вызывать эти методы повсюду. Не конец света, но не совсем в духе "тощих контролеров".

Обратные вызовы ActiveRecord - это еще один возможный вариант, хотя лично мне он не нравится, так как, на мой взгляд, он слишком тесно связывает две разные модели.

Так что в Rails 4, мире без наблюдателей, если бы вам пришлось создавать новую запись после создания / обновления / уничтожения другой записи, какой шаблон проектирования вы бы использовали? Толстые контроллеры, обратные вызовы ActiveRecord или что-то еще целиком?

Спасибо.

kennyc
источник
4
Я действительно удивлен, что на этот вопрос больше нет ответов. Вид сбивает с толку.
courtimas

Ответы:

82

Посмотрите на проблемы

Создайте папку в каталоге моделей под названием проблемы. Добавьте туда модуль:

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

В зависимости от того, что вы делаете, это может сблизить вас без наблюдателей.

UncleAdam
источник
20
Есть проблемы с этим подходом. Примечательно, что он не очищает ваши модели; include копирует методы из модуля обратно в ваш класс. Извлечение методов класса в модуль может сгруппировать их по интересам, но класс все еще такой же раздутый.
Стивен Сорока
15
Название «Альтернативы Rails Observer для 4.0», а не «Как минимизировать раздувание». Почему проблемы не выполняют работу, Стивен? И нет, предположить, что «раздувание» является причиной, почему это не сработает, поскольку замена наблюдателей недостаточно хороша. Вам нужно будет предложить лучшее предложение, чтобы помочь сообществу или объяснить, почему проблемы не будут заменять наблюдателей. Надеюсь, вы
заявите
10
Раздувание всегда вызывает беспокойство. Лучшей альтернативой является wisper , который, при правильной реализации, позволяет устранить проблемы , выделив их в отдельные классы, которые не тесно связаны с моделями. Это также значительно облегчает тестирование в одиночку
Стивен Сорока,
4
Раздувание модели или раздувание всего приложения, потянув Gem, чтобы сделать это - мы можем оставить это до индивидуальных предпочтений. Спасибо за дополнительное предложение.
UncleAdam
Это только раздувает меню автозаполнения метода IDE, что должно быть хорошо для многих людей.
Лулалала
33

Они в плагине сейчас.

Могу ли я также порекомендовать альтернативу, которая даст вам такие контроллеры, как:

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
источник
Как насчет ActiveSupport :: Уведомления?
своп
@svoop ActiveSupport::Notificationsориентированы на инструментарий, а не на общий sub / pub.
Крис
@ Крис - ты прав. Он используется в основном для инструментовки, но мне интересно, что мешает использовать его как универсальный метод для pub / sub? это обеспечивает основные строительные блоки, верно? Другими словами, каковы преимущества / недостатки Wisper по сравнению с ActiveSupport::Notifications?
имбирный лайм
Я не использовал Notificationsмного, но я бы сказал, что Wisperимеет более приятный API и такие функции, как «глобальные подписчики», «по префиксу» и «сопоставление событий», которых Notificationsнет. В будущем выпуске Wisperтакже будет возможна асинхронная публикация через SideKiq / Resque / Celluloid. Также, потенциально, в будущих версиях Rails, API для Notificationsможет измениться, чтобы быть более сфокусированным на инструментах.
Крис
21

Я предлагаю прочитать сообщение в блоге Джеймса Голика по адресу http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (попытаться игнорировать нескромно звучит название).

Раньше это была вся "толстая модель, тощий контроллер". Тогда толстые модели стали гигантской головной болью, особенно во время тестирования. В последнее время толчок был для тощих моделей - идея заключалась в том, что каждый класс должен выполнять одну обязанность, а задача модели - сохранять ваши данные в базе данных. Так где же заканчивается вся моя сложная бизнес-логика? В классах бизнес-логики - классы, представляющие транзакции.

Этот подход может превратиться в болото (гиганство), когда логика начинает усложняться. Тем не менее, концепция разумна - вместо того, чтобы неявно вызывать вещи с помощью обратных вызовов или наблюдателей, которые трудно протестировать и отлаживать, явно запускайте вещи в классе, который накладывает логику поверх вашей модели.

MikeJ
источник
4
За последние несколько месяцев я делал что-то подобное для проекта. В конечном итоге вы получаете множество небольших сервисов, но простота тестирования и обслуживания определенно перевешивает недостатки. Мои довольно обширные характеристики этой системы среднего размера все еще занимают всего 5 секунд, чтобы запустить :)
Luca Spiller
Также известный как PORO (простые старые рубиновые объекты) или объекты обслуживания
Кирилл Дюшон-Дорис,
13

Использование обратных вызовов активных записей просто переворачивает зависимость вашей связи. Например, если у вас есть modelAи CacheObserverстеллажи для наблюдения modelA3 стиля, вы можете удалить CacheObserverбез проблем. Вместо этого, скажем, Aдолжен вручную вызывать CacheObserverпосле сохранения, что будет rails 4. Вы просто переместили свою зависимость, чтобы вы могли безопасно удалить, Aно неCacheObserver .

Теперь из своей башни из слоновой кости я предпочитаю, чтобы наблюдатель зависел от модели, которую он наблюдает. Я достаточно забочусь, чтобы загромождать мои контроллеры? Для меня ответ - нет.

Предположительно, вы задумывались над тем, почему вы хотите / нуждаетесь в наблюдателе, и, таким образом, создание модели, зависящей от ее наблюдателя, - не страшная трагедия.

У меня также есть (вполне обоснованное, я думаю) отвращение к любому виду наблюдателя, зависящему от действия диспетчера. Внезапно вы должны внедрить своего наблюдателя в любое действие контроллера (или другую модель), которое может обновить модель, которую вы хотите наблюдать. Если вы можете гарантировать, что ваше приложение будет когда-либо модифицировать экземпляры только с помощью действий контроллера создания / обновления, это даст вам больше возможностей, но я не буду исходить из предположения о приложении rails (рассмотрим вложенные формы, ассоциации обновления бизнес-логики модели и т. Д.).

agmin
источник
1
Спасибо за комментарии @agmin. Я с радостью откажусь от использования Observer, если есть лучший шаблон проектирования. Меня больше всего интересует, как другие люди структурируют свой код и зависимости для обеспечения аналогичной функциональности (исключая кэширование). В моем случае я хотел бы записывать изменения в модели каждый раз, когда ее атрибуты обновляются. Я использовал Обозреватель, чтобы сделать это. Теперь я пытаюсь выбрать между жирным контроллером, обратным вызовом AR или чем-то еще, о чем я не думал. Ни один не кажется элегантным в данный момент.
Kennyc
13

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
opsb
источник
9

В некоторых случаях я просто использую 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
Паника
источник
4

Моя альтернатива Rails 3 Observers - это ручная реализация, которая использует обратный вызов, определенный в модели, но при этом умудряется (как заявляет agmin в своем ответе выше) «перевернуть зависимость ... связь».

Мои объекты наследуются от базового класса, который предусматривает регистрацию наблюдателей:

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) иметь метод, имя которого соответствует событию. Как и следовало ожидать, несколько наблюдателей могут зарегистрироваться для одного и того же события, и (со ссылкой на 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

Одно предостережение заключается в том, что имена всех событий, представленных во всех моделях, должны быть уникальными.

Марк Шнайдер
источник
3

Я думаю, что проблема с устаревшими наблюдателями заключается не в том, что наблюдатели были плохими сами по себе, а в том, что они подвергались насилию.

Я бы не стал добавлять слишком много логики в ваши обратные вызовы или просто перемещать код, чтобы имитировать поведение наблюдателя, когда уже есть надежное решение этой проблемы - шаблон наблюдателя.

Если имеет смысл использовать наблюдателей, то обязательно используйте наблюдателей. Просто поймите, что вам нужно убедиться, что ваша логика наблюдателя соответствует практике кодирования звука, например, SOLID.

Камень наблюдателя доступен на rubygems, если вы хотите добавить его обратно в свой проект https://github.com/rails/rails-observers

см. эту короткую ветку, хотя и не полное всестороннее обсуждение, я думаю, что основной аргумент является действительным https://github.com/rails/rails-observers/issues/2

hraynaud
источник
2

Вы можете попробовать https://github.com/TiagoCardoso1983/association_observers . Он еще не тестировался для rails 4 (который еще не был запущен), и нуждается в некотором дополнительном сотрудничестве, но вы можете проверить, подходит ли он вам.

ChuckE
источник
2

Как насчет использования 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 .

РЕДАКТИРОВАТЬ: я должен также упомянуть, что вышеупомянутое решение позволяет добавлять логику транзакции, когда это необходимо. Например, с ActiveRecord и поддерживаемой базой данных:

class LoggedUpdater

  def self.save!([records])
    ActiveRecord::Base.transaction do
      records.each(&:save!)
      #log the changes here
    end
  end

end
Houen
источник
-2

У меня такой же пробьем! Я нахожу решение 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

msroot
источник