Vấn đề của Rails Callback
Bài đăng này đã không được cập nhật trong 7 năm
Nếu bạn tìm kiếm trên Stackoverflow với từ khóa Rails callbacks
, phần lớn các kết quả tìm kiếm đều khuyên tránh sử dụng callback
trong một số trường hợp. Nó dường như đã bị xa lánh ngay khi các lập trình viên phát hiện ra .
Tại sao lại vậy ? Callback là gì ?
Callback là gì?
Callback
là hooks
trong vòng đời của một đối tượng ActiveRecord
,
Các action: before
, after
, hoặc around
Các event: save
, validate
, create
, update
,...
Ta có thể có 2 actions before_update
trong 1 object ActiveRecord
, và các actions này sẽ được thực hiện theo tứ thự đã được khai báo.
Bạn có thể tìm hiểu thêm về callback
qua đây.
Vấn đề đầu tiên
Các lập trình viên thường sẽ thấy vấn đề ngay khi bắt đầu với việc viết test rspec
. Bởi vì bạn thử tưởng tượng xem, khi viết test bạn sẽ cần khởi tạo object, nhưng mỗi lần khởi tạo bạn lại phải quan tâm tới hàng tá callback khác, và phải thêm cơ số data để phục vụ cho việc khởi tạo này. Hay thậm chí khi bạn đã có thể viết được suôn sẻ lần đầu thì khi có một logic mới được thêm vào => chúng ta thêm callback
=> phải sửa lại tất cả các rspec liên quan, bởi rất có thể logic thêm vào sẽ khiến việc khởi tạo object khi test gặp vấn đề. Đương nhiên chúng ta có thể sử dụng stub
cho các callback
giống như ví dụ sau:
#Model
class Post < ActiveRecord::Base
has_many :followers
after_save :notify_followers
def publish!
self.published_at = Time.now
save
end
private
def notify_followers
Notifier.post_mailer.deliver
end
end
#Rspec
describe "publishing the article" do
it "saves the object with a defined published_at value" do
Post.any_instance.stub(:notify_followers) # Codey McSmellsalot
post = Post.new(:title => "The Problem with Callbacks in Rails")
post.publish!
expect(post.published_at).to be_an_kind_of(Time)
expect(post).to_not be_a_new_record
end
end
Bạn có thấy để rspec pass thì chúng ta phải sử dụng Post.any_instance.stub(:notify_followers)
không, nếu không có stub hẳn sẽ rất vất vả để viết được rspec này đây.
Có vẻ nếu dùng stub
thì vấn đề rspec sẽ được giải quyết. Không hẳn vậy, hãy thử tưởng tượng tất cả các rspec có save Post đều phải thêm stub xem, thật phiền phức, hơn nữa nếu ta chỉ cần thêm 1 callback thôi thì tất cả các rspec khác sẽ đều phải thêm stub cho callback mới nữa. Bạn sẽ vấn muốn dùng callback chứ ?
Observers liệu có thay thế được Callback ?
Đây có lẽ là hướng đi đúng, bởi vì bằng cách tạo ra một observer, tức là bạn đã di chuyển những trách nhiệm xử lý không thuộc object chính cho observer
xử lý.
Thế nhưng vấn đề ở chỗ observer
là một loại callback ẩn
, tức là nó hoạt động giống callback
nhưng các callback này được chuyển sang một class khác mà thôi. Các class observer
này sẽ được gán cho class tương ứng khi ứng dụng của bạn khởi động vì thế khi bạn test rspec, các callback sẽ vẫn hoạt động, và vấn đề sẽ vẫn tồn tại. Tuy nhiên nếu bạn dùng observer
thì có một cách để disable observer khi test với gem no-peeping-toms
#Thêm dòng sau vào file spec_helper.rb
ActiveRecord::Observer.disable_observers
Tại sao Callback lại có vấn đề ?
before_
callback thường được sử dụng để chuẩn bị cho một object, như việc cập nhật timestamps
hoặc tăng biến counters
của object đó trước khi object được lưu lại.
after_
callback chủ yếu được sử dụng liên quan tới việc lưu object.
Khi object được lưu, mục đích và trách nhiệm của object đã được hoàn thành, nhưng với callback thì chúng ta thường thấy nó phải xử lý cả những vấn đề vượt ra ngoài phạm vi trách nhiệm của mình và đó là lúc chúng ta gặp phải vấn đề.
Hãy xem ví dụ sau:
class Order < ActiveRecord::Base
belongs_to :user
has_many :line_items
has_many :products, :through => :line_items
after_create :purchase_completion_notification
private
def purchase_completion_notification
Notifier.purchase_notifier(self).deliver
end
end
class Notifier < ActionMailer...
def purchase_notifier(order)
@order = order
@user = order.user
@products = order.products
rest of the action mailer logic
end
end
Khi Order
được lưu, nó sẽ gửi một email tới cho khách hàng. Mailer
sẽ sử dụng object order
để lấy ra user
và products
, sau đó dùng chúng để gửi email.
Chúng ta có thể thấy trách nhiệm gửi mail là của class Notifier
, tuy nhiên khi sử dụng after_create
, chúng ta lại liên kết chặt object order
với trách nhiệm của Notifier
, việc liên kết này nhiều khi sẽ gây ra những vấn đề về sau mà chúng ta không thể kiểm soát được.
Làm sao để giải quyết ?
Có một nguyên tắc đơn giản như sau: Chỉ sử dụng callback khi logic liên quan với bản thân đối tượng
.
Nguyên tắc này sẽ tránh việc object phải xử lý những vấn đề nằm ngoài phạm vi kiểm soát của chính nó, tuy nhiên nếu muốn xử lý cả những vấn đề nằm ngoài phạm vi thì phải làm thế nào ?
Tất nhiên là không sử dụng callback
rồi, chúng ta sẽ xử lý như sau:
#Model Order
class Order < ActiveRecord::Base
belongs_to :user
has_many :line_items
has_many :products, :through => :line_items
end
#New class
class OrderCompletion
attr_accessor :order
def initialize(order)
@order = order
end
def create
if self.order.save
self.purchase_completion_notification
end
end
def purchase_completion_notification
Notifier.purchase_notifier.deliver(self.order)
end
end
#Controller
def create
@order = Order.new(params[:order])
@order_completion = OrderCompletion.new(@order)
if @order_completion.create
redirect_to root_path, notice: 'Your order is being processed.'
else
@order = @order_completion.order
render action: "new"
end
end
Với cách này, chúng ta chuyển việc lưu order
và gửi notification sang một class mới. Lúc này, việc viết test sẽ dễ dàng hơn nhiều, chúng ta không cần phải dùng stub
nữa, thật tuyệt phải không
Tham khảo
http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/
All rights reserved