[RubyonRails] Ruby method lookup path
Một câu hỏi đơn giản nhưng khó trả lời. Ruby có nhiều cách khác nhau trong việc định nghĩa một method và thêm nó vào một class: Thêm nó vào singleton class Thêm nó vào class Include một module Prepend một module Extend một module Kế thừa từ superclass Nguyên ...
Một câu hỏi đơn giản nhưng khó trả lời. Ruby có nhiều cách khác nhau trong việc định nghĩa một method và thêm nó vào một class:
-
Thêm nó vào singleton class
-
Thêm nó vào class
-
Include một module
-
Prepend một module
-
Extend một module
-
Kế thừa từ superclass
Nguyên tắc: cố gắng tránh các tình huống khi bạn có nhiều class và module định nghĩa các method giống nhau.
Làm thế nào để chúng ta thực hiện tìm kiếm lookup path? Ví dụ dưới đây sẽ giải thích điều này:
module Include def call(level) puts "#{level} include" super(level + 1) rescue nil end end module Prepend def call(level) puts "#{level} prepend" super(level + 1) rescue nil end end module Extend def call(level) puts "#{level} extend" super(level + 1) rescue nil end end class Super def call(level) puts "#{level} super" super(level + 1) rescue nil end end class Klass < Super include Include prepend Prepend def call(level) puts "#{level} klass" super(level + 1) rescue nil end end thing = Klass.new def thing.call(level) puts "#{level} singleton" super(level + 1) rescue nil end thing.extend(Extend) thing.call(1)
Đoạn code trên đã làm gì? Nó định nghĩa một phương thức call cho 6 trường hợp mô tả ở trên. Tất cả in ra một số thông tin và sau đó chuyển tiếp đến super và dừng khi gặp nil. Chạy đoạn code trên bằng irb ta được kết quả:
1 singleton 2 extend 3 prepend 4 klass 5 include 6 super => nil
Điều gì sẽ xảy ra nếu bạn extend, include hoặc prepend nhiều lần? Định nghĩa cuối cùng sẽ được gọi trước. Ví dụ như trong trường hợp:
class Foo include Bar include Baz end
Như ở ví dụ trên thì định nghĩa từ Baz sẽ được gọi trước. Và dĩ nhiên nếu không gọi super thì tất cả sẽ dừng lại ở phương thức hiện tại mà không gọi thêm khai triển nào khác.
Trước khi đi vào method lookup ta cần hiểu về hai khái niệm receiver và ancestors chain.
- receiver: Như đã đề cập bên trên, ta có thể hiểu đơn giản receiver chỉ là một object và gọi được phương thức từ nó.
- ancestors chain: trước khi đi vào định nghĩa chúng ta xét ví dụ sau:
class People def description puts "people is singular or plural" end end class Student < People end 2.1.5 :018 > st = Student.new => #<Student:0x000000032aec18> 2.1.5 :019 > st.description => people is singular or plural
Khi gọi st.description, ruby sẽ tìm trong class Student instance method description nhưng không thấy, tiếp tục tìm đến superclass tức là class mà nó kế thừa (ở đây là People), sau khi tìm thấy nó sẽ dừng lại và thực thi method. Nhưng giả sử nó không tìm thấy ở class nó kế thừa People nó sẽ tiếp tục tìm lên superclass của class People là Object. Path từ object -> class -> superclass -> superclass... như vậy được gọi là ancestors chain.
Vậy Method lookup là việc tìm kiếm phương thức, Ruby sẽ tìm trong class của receiver và từ đó nó sẽ tìm method theo ancesstors chain cho đến khi nó tìm được phương thức cần gọi.
Một cách khác để xác định lookup path là sử dụng ancestors. Nó sẽ trả về một danh sách các module được include (kể cả chính module gốc).
irb(main):053:0> p thing.class.ancestors [Prepend, Klass, Include, Super, Object, Kernel, BasicObject] => [Prepend, Klass, Include, Super, Object, Kernel, BasicObject]
Thứ tự này được xác định nhưng chưa đầy đủ, nó vẫn thiếu các method được thêm vào singleton class. Chúng có thể được nhìn thấy nếu chúng ta kiểm tra singleton_class thay vào đó (lưu ý: điều này sẽ tạo ra singleton class nếu nó chưa tồn tại):
irb(main):054:0> p thing.singleton_class.ancestors [#<Class:#<Klass:0x00005559c9a7f238>>, Extend, Prepend, Klass, Include, Super, Object, Kernel, BasicObject] => [#<Class:#<Klass:0x00005559c9a7f238>>, Extend, Prepend, Klass, Include, Super, Object, Kernel, BasicObject]