Rails Observer thay thế cho 4.0


154

Với các Quan sát viên chính thức bị xóa khỏi Rails 4.0 , tôi tò mò không biết các nhà phát triển khác đang sử dụng ở vị trí nào của họ. .

Lấy ví dụ, một ứng dụng cần theo dõi các thay đổi đối với một mô hình. Người quan sát có thể dễ dàng theo dõi các thay đổi trên Mô hình A và ghi lại những thay đổi đó với Mô hình B trong cơ sở dữ liệu. Nếu bạn muốn theo dõi các thay đổi trên một số mô hình, thì một người quan sát duy nhất có thể xử lý điều đó.

Trong Rails 4, tôi tò mò không biết chiến lược nào mà các nhà phát triển khác đang sử dụng thay cho Người quan sát để tạo lại chức năng đó.

Cá nhân, tôi đang nghiêng về một kiểu triển khai "bộ điều khiển chất béo", trong đó những thay đổi này được theo dõi trong phương pháp tạo / cập nhật / xóa của từng bộ điều khiển mô hình. Mặc dù nó làm tăng hành vi của từng bộ điều khiển một chút, nhưng nó giúp ích trong việc đọc và hiểu vì tất cả các mã nằm ở một nơi. Nhược điểm là bây giờ mã rất giống nhau nằm rải rác trong một số bộ điều khiển. Trích xuất mã đó vào các phương thức của trình trợ giúp là một tùy chọn, nhưng bạn vẫn còn các lệnh gọi đến các phương thức đó nằm rải rác khắp nơi. Không phải ngày tận thế, nhưng cũng không hoàn toàn theo tinh thần "người điều khiển gầy".

Các cuộc gọi lại ActiveRecord là một lựa chọn khả thi khác, mặc dù theo quan điểm của tôi thì tôi không thích hai mô hình khác nhau quá gần nhau.

Vậy trong thế giới Rails 4, không có người quan sát, nếu bạn phải tạo một bản ghi mới sau khi một bản ghi khác được tạo / cập nhật / hủy, bạn sẽ sử dụng mẫu thiết kế nào? Bộ điều khiển chất béo, cuộc gọi lại ActiveRecord, hoặc cái gì khác hoàn toàn?

Cảm ơn bạn.


4
Tôi thực sự ngạc nhiên khi không có nhiều câu trả lời được đăng cho câu hỏi này. Loại bối rối.
Courtimas

Câu trả lời:


82

Hãy nhìn vào mối quan tâm

Tạo một thư mục trong thư mục mô hình của bạn được gọi là mối quan tâm. Thêm một mô-đun ở đó:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

Tiếp theo, bao gồm điều đó trong các mô hình bạn muốn chạy after_save trong:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

Tùy thuộc vào những gì bạn đang làm, điều này có thể giúp bạn gần gũi mà không cần người quan sát.


20
Có vấn đề với phương pháp này. Đáng chú ý, nó không làm sạch mô hình của bạn; bao gồm các bản sao các phương thức từ mô đun trở lại lớp của bạn. Trích xuất các phương thức lớp ra một mô-đun có thể nhóm chúng theo mối quan tâm, nhưng lớp vẫn chỉ là cồng kềnh.
Steven Soroka

15
Tiêu đề là 'Rails Observer Alternators for 4.0' chứ không phải 'Làm cách nào để giảm thiểu sự phình to'. Làm thế nào mà những mối quan tâm không làm công việc Steven? Và không, cho rằng 'phình to' là một lý do tại sao điều này sẽ không hoạt động như một sự thay thế cho các nhà quan sát không đủ tốt. Bạn sẽ phải đưa ra một gợi ý tốt hơn để giúp cộng đồng hoặc giải thích lý do tại sao các mối quan tâm sẽ không hoạt động như một sự thay thế cho các nhà quan sát. Hy vọng bạn sẽ nêu cả hai = D
ChúAdam

10
Bloat luôn là một mối quan tâm. Một giải pháp thay thế tốt hơn là khôn ngoan hơn , nếu được triển khai đúng cách, cho phép bạn xóa sạch các mối quan tâm bằng cách trích xuất chúng ra các lớp riêng biệt không liên kết chặt chẽ với các mô hình. Điều này cũng giúp kiểm tra cách ly dễ dàng hơn nhiều
Steven Soroka

4
Mô hình phình to hoặc Toàn bộ ứng dụng phình to bằng cách kéo Gem để làm điều này - chúng ta có thể để nó tùy theo sở thích cá nhân. Cảm ơn đề nghị bổ sung.
ChúAdam

Nó sẽ chỉ làm mờ menu tự động hoàn thành của IDE, điều này sẽ tốt cho nhiều người.
lulalala

33

Bây giờ họ đang ở trong một plugin .

Tôi cũng có thể đề xuất một giải pháp thay thế sẽ cung cấp cho bạn các bộ điều khiển như:

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

Làm thế nào về ActiveSupport :: Thông báo?
svoop

@svoop ActiveSupport::Notificationshướng đến thiết bị, không phải là chung / quán rượu.
Kris

@Kris - bạn nói đúng. Nó được sử dụng cho thiết bị chủ yếu, nhưng tôi tự hỏi điều gì ngăn nó được sử dụng như một phương pháp chung cho pub / sub? Nó cung cấp các khối xây dựng cơ bản, phải không? Nói cách khác, những gì lên / xuống để khôn ngoan hơn so với ActiveSupport::Notifications?
gingerlime

Tôi chưa sử dụng Notificationsnhiều nhưng tôi nói Wispercó API đẹp hơn và các tính năng như 'người đăng ký toàn cầu', 'tiền tố' và 'ánh xạ sự kiện' Notificationskhông có. Bản phát hành trong tương lai Wispercũng sẽ cho phép xuất bản async thông qua SideKiq / Resque / Celluloid. Ngoài ra, có khả năng, trong các bản phát hành Rails trong tương lai, API cho Notificationscó thể thay đổi để tập trung nhiều công cụ hơn.
Kris

21

Đề nghị của tôi là đọc bài đăng trên blog của James Golick tại http://jamesgolick.com/2010/3/14/crazy-heratics-and-awclaw-the-way-i-write-rails-apps.html (cố gắng bỏ qua âm thanh tiêu đề).

Ngày trước, tất cả chỉ là "người mẫu béo, người điều khiển gầy". Sau đó, các mô hình chất béo đã trở thành một cơn đau đầu khổng lồ, đặc biệt là trong quá trình thử nghiệm. Gần đây, sự thúc đẩy đã dành cho các mô hình mỏng - ý tưởng là mỗi lớp nên xử lý một trách nhiệm và công việc của một mô hình là duy trì dữ liệu của bạn vào cơ sở dữ liệu. Vì vậy, tất cả logic kinh doanh phức tạp của tôi kết thúc ở đâu? Trong các lớp logic nghiệp vụ - các lớp đại diện cho các giao dịch.

Cách tiếp cận này có thể biến thành một vũng lầy (sự cười khúc khích) khi logic bắt đầu trở nên phức tạp. Tuy nhiên, khái niệm này là âm thanh - thay vì kích hoạt mọi thứ ngầm với các cuộc gọi lại hoặc các trình quan sát khó kiểm tra và gỡ lỗi, hãy kích hoạt mọi thứ một cách rõ ràng trong một lớp có lớp logic trên mô hình của bạn.


4
Tôi đã làm một cái gì đó như thế này cho một dự án trong vài tháng qua. Bạn kết thúc với rất nhiều dịch vụ nhỏ, nhưng việc dễ dàng kiểm tra và duy trì nó chắc chắn vượt xa những nhược điểm. Thông số kỹ thuật khá rộng của tôi trên hệ thống cỡ trung bình này vẫn chỉ mất 5 giây để chạy :)
Luca Spiller

Còn được gọi là PORO (Đối tượng Ruby cũ đơn giản) hoặc đối tượng dịch vụ
Cyril Duchon-Doris

13

Sử dụng các cuộc gọi lại bản ghi hoạt động chỉ đơn giản là bỏ qua sự phụ thuộc của khớp nối của bạn. Ví dụ, nếu bạn có modelAvà một kiểu đường ray CacheObserverquan sát modelA3, bạn có thể loại bỏ CacheObservermà không có vấn đề gì. Bây giờ, thay vì nói Aphải gọi thủ công CacheObserversau khi lưu, đó sẽ là đường ray 4. Bạn chỉ cần di chuyển sự phụ thuộc của mình để bạn có thể gỡ bỏ một cách an toàn Anhưng không CacheObserver.

Bây giờ, từ tháp ngà của tôi, tôi thích người quan sát phụ thuộc vào mô hình mà nó đang quan sát. Tôi có quan tâm đủ để làm lộn xộn bộ điều khiển của tôi? Đối với tôi, câu trả lời là không.

Có lẽ bạn đã suy nghĩ về lý do tại sao bạn muốn / cần người quan sát, và do đó tạo ra một mô hình phụ thuộc vào người quan sát của nó không phải là một thảm kịch khủng khiếp.

Tôi cũng có một sự chán ghét (có căn cứ, tôi nghĩ) đối với bất kỳ loại người quan sát nào đang phụ thuộc vào một hành động của bộ điều khiển. Đột nhiên bạn phải đưa người quan sát của mình vào bất kỳ hành động điều khiển nào (hoặc một mô hình khác) có thể cập nhật mô hình mà bạn muốn quan sát. Nếu bạn có thể đảm bảo ứng dụng của bạn sẽ chỉ sửa đổi các phiên bản thông qua các hành động tạo / cập nhật bộ điều khiển, cung cấp thêm sức mạnh cho bạn, nhưng đó không phải là giả định tôi sẽ thực hiện về một ứng dụng rails (xem xét các biểu mẫu lồng nhau, các hiệp hội cập nhật logic kinh doanh mô hình, v.v.)


1
Cảm ơn các ý kiến ​​@agmin. Tôi rất vui khi tránh sử dụng Observer nếu có mẫu thiết kế tốt hơn ngoài kia. Tôi quan tâm nhất đến cách người khác cấu trúc mã và các phụ thuộc của họ để cung cấp chức năng tương tự (không bao gồm bộ đệm). Trong trường hợp của tôi, tôi muốn ghi lại các thay đổi cho một mô hình bất cứ khi nào các thuộc tính của nó được cập nhật. Tôi đã từng sử dụng một Observer để làm điều đó. Bây giờ tôi đang cố gắng quyết định giữa một bộ điều khiển chất béo, gọi lại AR hoặc một cái gì đó khác mà tôi chưa từng nghĩ đến. Không có vẻ thanh lịch tại thời điểm này.
kennyc

13

Wisper là một giải pháp tuyệt vời. Sở thích cá nhân của tôi đối với các cuộc gọi lại là chúng được các mô hình kích hoạt nhưng các sự kiện chỉ được lắng nghe khi có yêu cầu, nghĩa là tôi không muốn gọi lại trong khi tôi đang thiết lập các mô hình trong các thử nghiệm, v.v. sa thải bất cứ khi nào kiểm soát viên có liên quan. Điều này thực sự dễ dàng để thiết lập với Wisper vì bạn có thể yêu cầu nó chỉ nghe các sự kiện bên trong một khối.

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

Trong một số trường hợp, tôi chỉ cần sử dụng Thiết bị hỗ trợ hoạt động

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

Sự thay thế của tôi cho Rails 3 Observers là một triển khai thủ công sử dụng một cuộc gọi lại được xác định trong mô hình mà vẫn quản lý (như trạng thái agmin trong câu trả lời của anh ta ở trên) "lật phụ thuộc ... khớp nối".

Các đối tượng của tôi kế thừa từ một lớp cơ sở cung cấp cho việc đăng ký quan sát viên:

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

(Được cấp, theo tinh thần sáng tác so với thừa kế, mã trên có thể được đặt trong một mô-đun và trộn lẫn trong mỗi mô hình.)

Một trình khởi tạo đăng ký quan sát viên:

User.add_observer(NotificationSender)
User.add_observer(ProfilePictureCreator)

Mỗi mô hình sau đó có thể xác định các sự kiện quan sát được của riêng mình, ngoài các cuộc gọi lại ActiveRecord cơ bản. Chẳng hạn, mô hình Người dùng của tôi hiển thị 2 sự kiện:

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

Bất kỳ người quan sát nào muốn nhận thông báo cho các sự kiện đó chỉ cần (1) đăng ký với mô hình phơi bày sự kiện và (2) có một phương thức có tên phù hợp với sự kiện. Như người ta có thể mong đợi, nhiều người quan sát có thể đăng ký cho cùng một sự kiện và (liên quan đến đoạn 2 của câu hỏi ban đầu), một người quan sát có thể theo dõi các sự kiện trên một số mô hình.

Các lớp quan sát NotificationSender và ProfilePictureCreator bên dưới định nghĩa các phương thức cho các sự kiện được hiển thị bởi các mô hình khác nhau:

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

Một cảnh báo là tên của tất cả các sự kiện được hiển thị trên tất cả các mô hình phải là duy nhất.


3

Tôi nghĩ rằng vấn đề với những người quan sát bị phản đối không phải là những người quan sát xấu về bản thân họ mà là họ đã bị lạm dụng.

Tôi sẽ thận trọng trước việc thêm quá nhiều logic trong các cuộc gọi lại của bạn hoặc chỉ đơn giản là di chuyển mã xung quanh để mô phỏng hành vi của người quan sát khi đã có một giải pháp âm thanh cho vấn đề này theo mẫu Observer.

Nếu nó có ý nghĩa để sử dụng người quan sát thì bằng mọi cách hãy sử dụng người quan sát. Chỉ cần hiểu rằng bạn sẽ cần đảm bảo rằng logic người quan sát của bạn tuân theo các thực tiễn mã hóa âm thanh, ví dụ RẮN.

Viên ngọc quan sát có sẵn trên rubygems nếu bạn muốn thêm nó trở lại dự án của mình https://github.com/rails/rails-observers

xem chủ đề ngắn gọn này, trong khi không thảo luận đầy đủ toàn diện Tôi nghĩ rằng các đối số cơ bản là hợp lệ. https://github.com/rails/rails-observers/issues/2



2

Làm thế nào về việc sử dụng PORO thay thế?

Logic đằng sau điều này là 'các hành động bổ sung về tiết kiệm' của bạn có thể sẽ là logic kinh doanh. Điều này tôi muốn tách biệt khỏi cả hai mô hình AR (cần đơn giản nhất có thể) và các bộ điều khiển (gây khó chịu để kiểm tra đúng cách)

class LoggedUpdater

  def self.save!(record)
    record.save!
    #log the change here
  end

end

Và chỉ cần gọi nó là như vậy:

LoggedUpdater.save!(user)

Bạn thậm chí có thể mở rộng trên nó, bằng cách tiêm thêm các đối tượng hành động sau lưu

LoggedUpdater.save(user, [EmailLogger.new, MongoLogger.new])

Và để đưa ra một ví dụ về 'tính năng bổ sung'. Bạn có thể muốn làm hỏng chúng lên một chút mặc dù:

class EmailLogger
  def call(msg)
    #send email with msg
  end
end

Nếu bạn thích cách tiếp cận này, tôi khuyên bạn nên đọc bài đăng trên blog của Bryan Helmkamp 7 Forms .

EDIT: Tôi cũng nên đề cập rằng giải pháp trên cho phép thêm logic giao dịch khi cần thiết. Ví dụ với ActiveRecord và cơ sở dữ liệu được hỗ trợ:

class LoggedUpdater

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

end


-2

Tôi có cùng một probjem! Tôi tìm thấy một giải pháp ActiveModel :: Dirty để bạn có thể theo dõi các thay đổi mô hình của mình!

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/groupes/ActiveModel/Dundred.html

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.