Rails Observer 4.0の代替


154

Observersが正式にRails 4.0から削除されたので、他の開発者が自分の代わりに何を使っているのか知りたいです。(抽出されたgemを使用する場合を除きます。)オブザーバーは確かに乱用され、時として扱いにくくなる可能性がありますが、キャッシュをクリアする以外にも多くのユースケースがあり、有益でした。

たとえば、モデルへの変更を追跡する必要があるアプリケーションを考えてみましょう。オブザーバーはモデルAでの変更を簡単に監視し、モデルBでそれらの変更をデータベースに記録できます。複数のモデルにわたる変更を監視したい場合は、1人のオブザーバーがそれを処理できます。

Rails 4では、他の開発者がその機能を再現するためにObserversの代わりに使用している戦略に興味があります。

個人的には、これらの変更が各モデルコントローラーのcreate / update / deleteメソッドで追跡される、一種の「ファットコントローラー」の実装に傾いています。各コントローラーの動作が少し膨れますが、すべてのコードが1か所にあるため、読みやすさと理解に役立ちます。欠点は、非常によく似たコードがいくつかのコントローラーに散在していることです。そのコードをヘルパーメソッドに抽出することはオプションですが、これらのメソッドへの呼び出しはどこにでも散らかっています。世界の終わりではありませんが、「細いコントローラー」の精神でもありません。

ActiveRecordコールバックも考えられる別のオプションですが、私の意見では2つの異なるモデルを非常に密接に結合する傾向があるため、個人的には好きではありません。

したがって、Rails 4のオブザーバーのない世界では、別のレコードが作成/更新/破棄された後に新しいレコードを作成する必要がある場合、どの設計パターンを使用しますか?ファットコントローラー、ActiveRecordコールバック、または何か他のもの?

ありがとうございました。


4
この質問に対して回答が投稿されていないことに本当に驚いています。一種の当惑させる。
2013年

回答:


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

何をしているのかによっては、オブザーバーがいなくても、これで近づくかもしれません。


20
このアプローチには問題があります。特に、これはモデルをクリーンアップしません。includeは、メソッドをモジュールからクラスコピーします。クラスメソッドをモジュールに抽出すると、問題ごとにグループ化される可能性がありますが、クラスは依然として肥大化しています。
Steven Soroka 2014年

15
タイトルは「Rails Observer Alternatives for 4.0」ではなく、「膨張を最小限に抑える方法」です。懸念がスティーブンの仕事をしないのはなぜですか?いいえ、「膨張」がオブザーバーの代わりとして機能しない理由であることを示唆するだけでは十分ではありません。コミュニティを支援したり、懸念がオブザーバーの代わりとして機能しない理由を説明したりするためのより良い提案を考え出す必要があります。うまくいけば、両方の= D
UncleAdam

10
膨満感は常に問題です。より適切な代替手段はwisperです。wisperを適切に実装すると、モデルに密接に結合されていない個別のクラスに問題を抽出して、問題をクリーンアップできます。これにより、単独でのテストも非常に簡単になります
Steven Soroka '28

4
これを行うためにGemを引き込むことにより、モデルの膨らみまたはWhole Appの膨らみを作ります-個人の好みに任せることができます。追加の提案をありがとう。
UncleAdam 14

これはIDEのメソッドのオートコンプリートメニューを膨らませるだけで、多くの人にとっては問題ないはずです。
lulalala 2015

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

ActiveSupport :: Notificationsはどうですか?
2014

@svoop ActiveSupport::Notificationsは、汎用のサブ/パブではなく、インストルメンテーション向けです。
クリス

@クリス-あなたは正しい。主にインストルメンテーションに使用されますが、pub / subの汎用メソッドとして使用できないのはなぜですか?基本的な構成要素を提供しますよね?言い換えれば、と比較してささやくの利点/欠点は何ActiveSupport::Notificationsですか?
gingerlime 2014

私はあまり使用しNotificationsていませんWisperが、より優れたAPIと、「グローバルサブスクライバー」、「プレフィックス」、「イベントマッピング」などのNotifications機能を備えていません。の将来のリリースでWisperは、SideKiq / Resque / Celluloidを介した非同期パブリッシングも可能になります。また、潜在的に、将来のRailsリリースでは、のAPIが、Notificationsより計測に焦点を合わせるように変更される可能性があります。
クリス2014

21

私の提案は、http: //jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.htmlにあるJames Golickのブログ投稿を読むことです(方法を無視してください)タイトルが不快に聞こえる)。

昔は全て「太ったモデル、やせこけたコントローラー」だった。その後、特にテスト中に、脂肪モデルは大きな頭痛の種になりました。ごく最近では、細いモデルが求められています。つまり、各クラスが1つの責任を処理する必要があり、モデルの仕事はデータをデータベースに永続化することです。では、私の複雑なビジネスロジックはどこにあるのでしょうか。ビジネスロジッククラス-トランザクションを表すクラス。

ロジックが複雑になり始めると、このアプローチは泥沼(ギギィ)に変わります。コンセプトはしっかりしていますが、テストやデバッグが難しいコールバックやオブザーバーで暗黙的にトリガーするのではなく、モデルの上にロジックを階層化するクラスで明示的にトリガーします。


4
この数か月間、私はプロジェクトでこのようなことをしてきました。結局、多くの小さなサービスが発生しますが、テストと保守の容易さは間違いなく欠点を上回ります。この中規模システムでのかなり広範囲にわたるスペックは、実行に5秒しかかかりません:)
Luca Spiller 14

PORO(Plain Old Ruby Objects)またはサービスオブジェクトとしても知られています
Cyril Duchon-Doris

13

アクティブレコードコールバックを使用すると、カップリングの依存関係が単純に反転します。あなたが持っている場合たとえば、modelAおよびCacheObserver観測はmodelA3スタイルをレール、あなたは削除することができCacheObserverていない問題に。代わりAに、手動でCacheObserver後保存を呼び出す必要があると言います。これはレール4になります。依存関係を移動しただけなので、安全に削除できますが、削除はできAませんCacheObserver

今、私の象牙の塔から、私は観察者が観察しているモデルに依存することを好む。私のコントローラーを散らかすほど気にしますか?私にとって、答えはノーです。

おそらくあなたはなぜオブザーバーが欲しい/必要であるかを考えたので、そのオブザーバーに依存するモデルを作成することは恐ろしい悲劇ではありません。

私はまた、コントローラーのアクションに依存しているあらゆる種類のオブザーバーに対して(合理的に根拠があると思います)嫌悪感を抱いています。突然、監視したいモデルを更新する可能性のあるコントローラーアクション(または別のモデル)にオブザーバーを挿入する必要があります。アプリが作成/更新コントローラーアクションを介してインスタンスのみを変更することを保証できる場合、より強力になりますが、それは私がRailsアプリケーションについて想定するものではありません(ネストされたフォーム、ビジネスロジックの更新の関連付けなどを検討してください)。


1
コメント@agminをありがとう。より良いデザインパターンがある場合は、オブザーバーの使用をやめさせていただきます。私は、他の人々が同様の機能(キャッシングを除く)を提供するためにコードと依存関係をどのように構築しているかに最も興味があります。私の場合、モデルの属性が更新されるたびにモデルへの変更を記録したいと思います。私はそのためにオブザーバーを使用していました。今、私は太ったコントローラー、ARコールバック、または私が考えていなかった他のものの間で決定しようとしています。どちらも現時点ではエレガントに見えません。
kennyc 2013年

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

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

1つの注意点は、すべてのモデルで公開されるすべてのイベントの名前は一意でなければならないということです。


3

オブザーバーの廃止が問題となっているのは、オブザーバー自体が悪かったのではなく、虐待されていたということです。

オブザーバーパターンのこの問題に対する適切な解決策がある場合、コールバックにロジックを追加しすぎたり、コードを移動してオブザーバーの動作をシミュレートしたりしないように注意します。

オブザーバーを使用することが理にかなっている場合は、必ずオブザーバーを使用してください。オブザーバーロジックがSOLIDなどの適切なコーディング方法に従っていることを確認する必要があることを理解してください。

プロジェクトにhttps://github.com/rails/rails-observersを追加する場合は、rubygemsでオブザーバーgemを使用できます

この短いスレッドを参照してください。完全な包括的な議論ではありませんが、基本的な議論は妥当だと思います。 https://github.com/rails/rails-observers/issues/2



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


-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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.