Thông báo Rails 4.2 có tin thú vị về Rails 5 sắp tới: Nó có thể yêu cầu Ruby 2.2. Điều đó sẽ làm nó thành Rails version đầu tiên tận dụng toàn bộ lợi ích Ruby 2.
Bài viết nhắc garbage collected symbols và keyword arguments. Nhưng với tôi, tính năng Ruby 2 thú vị nhất là Module#prepend.
Tốt (và xấu) của alias_method_chain
Khi mới học Rails, alias_method_chain làm tôi choáng. Nó thực sự show sự linh hoạt của Ruby.
Chỉ một dòng code, bạn hoàn toàn thay đổi cách method hoạt động. Không còn hack thư viện để thêm code muốn, bạn thêm on the fly. alias_method_chain dẫn đến patch đầu tiên của tôi cho gem, dẫn đến pull request đầu, dẫn đến contribution open source đầu.
Nhưng giống monkey patching, alias_method_chain bị lạm dụng, vấn đề nổi lên:
- Tên method nó sinh ra gây nhầm lẫn, làm error khó tìm và debug. Ví dụ:
class Person
def greeting
"Hello"
end
end
module GreetingWithExcitement
def self.included(base)
base.class_eval do
alias_method_chain :greeting, :excitement
end
end
def greeting_with_excitement
"#{greeting_without_excitement}!!!!!"
end
end
Person.send(:include, GreetingWithExcitement)
Nếu lỗi ở Person#greeting, backtrace báo lỗi thực ra ở Person#greeting_without_excitement. Nhưng method đó define đâu? Không thấy đâu. Làm sao biết greeting method nào là cái có bug? Tên method càng rối càng chain nhiều.
-
Nếu gọi
alias_method_chainhai lần cùng tham số trên cùng class, có thể stack overflow. (Tại sao? Bạn thấy không?) Thường không xảy ra, miễnrequirenhất quán path. Nhưng phiền nếu thường paste code vào Rails console. -
Và các vấn đề Yehuda Katz mô tả trong bài blog. Bài đó thuyết phục nhiều Rails dev bỏ
alias_method_chaincho module inheritance.
Vậy tại sao vẫn dùng?
Bạn thay thế hầu hết alias_method_chain bằng override method trong module, include module vào child class. Nhưng chỉ works nếu override superclass, không phải class chính. Tức:
class ParentClass
def log
puts "In parent"
end
end
class ChildClass < ParentClass
def log
puts "In child"
super
end
def log_with_extra_message
puts "In child, with extra message"
log_without_extra_message
end
alias_method_chain :log, :extra_message
end
Chạy ChildClass.new.log, thấy:
In child, with extra message
In child
In parent
Nếu dùng module thay alias_method_chain, output là:
In child
In child, with extra message
In parent
Nhưng bạn không thể match output gốc nếu không đổi method log trong ChildClass. Ruby inheritance không chạy vậy. Tiền, nó không.
Điều gì đổi trong Ruby 2.0?
Trước Ruby 2.0, không cách nào thêm code dưới class, chỉ trên. Nhưng với prepend, bạn override method trong class bằng method từ module, vẫn truy cập implementation class bằng super. Vậy, dùng ví dụ cuối, ta có output gốc với:
class ParentClass
def log
puts "In parent"
end
end
module ExtraMessageLogging
def log
puts "In child, with extra message"
super
end
end
class ChildClass < ParentClass
prepend ExtraMessageLogging
def log
puts "In child"
super
end
end
In child, with extra message
In child
In parent
Hoàn hảo.
Nếu prepend vẫn khó hiểu, nghĩ nó làm kiểu:
class NewChildClass < ChildClass
include ExtraMessageLogging
end
ChildClass = NewChildClass
Trừ nó không mess với tên class, và affect object đã tồn tại.
(Có, bạn có thể gán lại tên class trong Ruby. Không, có lẽ không phải ý kiến tốt.)
Ý nghĩa với Rails?
Vậy, lý do cuối cùng dùng alias_method_chain mất ở Ruby 2.0. Chúng ta lấy một trong vài alias_method_chain còn lại trong Rails:
require 'active_support/core_ext/module/aliasing'
class Range
#:nodoc:
def each_with_time_with_zone(&block)
ensure_iteration_allowed
each_without_time_with_zone(&block)
end
alias_method_chain :each, :time_with_zone
def step_with_time_with_zone(n = 1, &block)
ensure_iteration_allowed
step_without_time_with_zone(n, &block)
end
alias_method_chain :step, :time_with_zone
private
def ensure_iteration_allowed
if first.is_a?(Time)
raise TypeError, "can't iterate from #{first.class}"
end
end
end
và thay bằng module:
require 'active_support/core_ext/module/aliasing'
module RangeWithTimeWithZoneSupport
#:nodoc:
def each(&block)
ensure_iteration_allowed
super(&block)
end
def step(n = 1, &block)
ensure_iteration_allowed
super(n, &block)
end
private
def ensure_iteration_allowed
if first.is_a?(Time)
raise TypeError, "can't iterate from #{first.class}"
end
end
end
Range.send(:prepend, RangeSupportingTimeWithZone)
Sạch hơn, Range#each không bị đổi tên, và ensure_iteration_allowed không bị monkey-patch.
Dùng inheritance, không patch
Ruby cho flexibility khổng lồ, đó là lý do tôi thích. Nhưng cũng có object model mạnh. Vì vậy khi muốn inject code, hãy thử dựa vào module và inheritance trước khi hack. Code sẽ dễ hiểu, debug hơn, và tránh side effect khó phát hiện của alias_method_chain.
alias_method_chain là method cool nhất tôi biết trong Rails. Nhưng ngày nó đã số. Chúng ta đã outgrow nó. Và tôi không nhớ nó khi nó biến mất.