Hãy nắm chặt hành lý xách tay, vì hôm nay chúng ta sẽ đi hết chuỗi tổ tiên (ancestor chain). Chúng ta sẽ theo dõi một lời gọi phương thức và xem nó di chuyển lên chuỗi như thế nào, cũng như tìm ra điều gì xảy ra nếu phương thức bị thiếu. Và vì chúng ta thích "chơi với lửa", chúng ta sẽ không dừng lại ở đó mà tiếp tục ghi đè BasicObject#method_missing. Nếu bạn chú ý, chúng ta có thể cũng sử dụng nó trong một ví dụ thực tế. Không đảm bảo đâu. Cùng bắt đầu!
Chuỗi Tổ tiên (Ancestor Chain)
Hãy bắt đầu với các quy tắc cơ bản của chuỗi tổ tiên trong Ruby:
- Ruby chỉ hỗ trợ đơn kế thừa (single inheritance)
- Nó cũng cho phép một object include một tập hợp các module
Trong Ruby, chuỗi tổ tiên được cấu thành từ việc duyệt qua tất cả các lớp và module được kế thừa cho một lớp cho trước.
Hãy xem một ví dụ để thấy chuỗi tổ tiên được xử lý như thế nào trong Ruby.
module Auth end module Session end module Iterable end class Collection prepend Iterable end class Users < Collection prepend Session include Auth end p Users.ancestorscho kết quả:
[ Session, Users, Auth, # Users Iterable, Collection, # Collection Object, Kernel, BasicObject # Ruby Object Model ]Đầu tiên, chúng ta gọi phương thức lớp ancestors để truy cập chuỗi tổ tiên của một lớp cho trước.
Chúng ta có thể thấy lời gọi Users.ancestors trả về một mảng các lớp và module theo thứ tự:
- Các module được prepend của
Users - Lớp
Users - Các module được include của lớp
Users - Các module được prepend của lớp
Collection— làm cha trực tiếp của Users - Lớp
Collection - Các module được include của lớp
Collection— không có - Lớp
Object— kế thừa mặc định của bất kỳ lớp nào - Module
Kernel— được include trongObjectvà chứa các phương thức cốt lõi - Lớp
BasicObject— lớp gốc trong Ruby
Vì vậy, thứ tự xuất hiện cho một lớp hoặc module được duyệt qua luôn như sau:
- Các module được prepend
- Lớp hoặc module
- Các module được include của nó
Chuỗi tổ tiên chủ yếu được duyệt qua bởi Ruby khi một phương thức được gọi trên một object hoặc một lớp.
Đường dẫn tra cứu phương thức (Method Lookup Path)
Khi một message được gửi, Ruby duyệt chuỗi tổ tiên của đối tượng nhận message và kiểm tra xem có bất kỳ cái nào phản hồi message đã cho không.
Nếu một lớp hoặc module cho trước của chuỗi tổ tiên phản hồi message, thì phương thức liên kết với message này được thực thi và việc duyệt chuỗi tổ tiên dừng lại.
class Collection < Array end Collection.ancestors # => [Collection, Array, Enumerable, Object, Kernel, BasicObject] collection = Collection.new([:a, :b, :c]) collection.each_with_index # => :aỞ đây, message collection.each_with_index được nhận bởi module Enumerable. Sau đó phương thức Enumerable#each_with_index được gọi cho message này.
Ở đây, khi collection.each_with_index được gọi, Ruby kiểm tra xem:
- Collection có phản hồi message
each_with_indexkhông => KHÔNG - Array có phản hồi message
each_with_indexkhông => KHÔNG - Enumerable có phản hồi message
each_with_indexkhông => CÓ
Vì vậy, từ đây, Ruby dừng việc duyệt chuỗi tổ tiên và gọi phương thức liên kết với message này. Trong trường hợp của chúng ta, là phương thức Enumerable#each_with_index.
Trong Ruby, cơ chế này được gọi là Method Lookup Path (Đường dẫn tra cứu phương thức).
Bây giờ, nếu không có lớp và module nào cấu thành chuỗi tổ tiên của một receiver cho trước phản hồi message thì sao?
BasicObject#method_missing
Đủ rồi, chơi theo kiểu tốt! Hãy phá vỡ mọi thứ, kiểu lập trình viên: bằng cách ném exception. Chúng ta sẽ triển khai một lớp Collection và gọi một phương thức không xác định trên một instance của nó.
class Collection end c = Collection.new c.search('item1') # => NoMethodError: undefined method `search` for #<Collection:0x123456890>Ở đây, lớp Collection không triển khai phương thức search. Vì vậy một NoMethodError được ném ra. Nhưng việc ném lỗi này đến từ đâu?
Lỗi được ném ra trong phương thức BasicObject#method_missing. Phương thức này được gọi khi Method Lookup Path kết thúc mà không tìm thấy bất kỳ phương thức nào tương ứng với một message cho trước.
Ồ... nhưng phương thức này chỉ ném NoMethodError. Vậy sẽ tuyệt vời nếu chúng ta có thể ghi đè phương thức này trong ngữ cảnh của lớp Collection của mình.
Ghi đè phương thức BasicObject#method_missing
Đoán xem? Việc ghi đè method_missing hoàn toàn ổn vì phương thức này cũng tuân theo cơ chế của Method Lookup Path. Sự khác biệt duy nhất với một phương thức bình thường là chúng ta chắc chắn rằng phương thức này sẽ được tìm thấy ít nhất một lần bởi Method Lookup Path.
Thực tế, lớp BasicObject — là lớp gốc của bất kỳ lớp nào trong Ruby — định nghĩa một phiên bản tối thiểu của phương thức này. Classic Ruby Magic, n'est pas?
Vậy hãy ghi đè phương thức này trong lớp Collection của chúng ta:
class Collection def initialize @collection = {} end def method_missing(method_id, *args) if method_id[-1] == '=' key = method_id[0..-2] @collection[key.to_sym] = args.first else @collection[method_id] end end end collection = Collection.new collection.obj1 = 'value1' collection.obj2 = 'value2' collection.obj1 # => 'value1' collection.obj2 # => 'value2'Ở đây, Collection#method_missing đóng vai trò như một delegator cho biến instance @collection. Thực tế, đây là cách Ruby xấp xỉ xử lý delegation của object — cf: thư viện delegate.
Nếu phương thức bị thiếu là một phương thức setter (collection.obj1 = 'value1'), thì tên phương thức (:obj1) được dùng làm key và đối số ('value1') làm giá trị của entry hash @collection (@collection[:obj1] = 'value1').
Một Trình tạo Thẻ HTML
Bây giờ chúng ta đã biết cách phương thức method_missing hoạt động bên hậu trường, hãy triển khai một use case có thể tái hiện.
Ở đây, mục tiêu là định nghĩa DSL sau:
HTML.p 'hello world' # => <p>hello world</p> HTML.div 'hello world' # => <div>hello world</div> HTML.h1 'hello world' # => <h1>hello world</h1> HTML.h2 'hello world' # => <h2>hello world</h2> HTML.span 'hello world' # => <span>hello world</span> HTML.p "hello #{HTML.b 'world'}" # => <p>hello <b>world</b></p>Để làm được vậy, chúng ta sẽ triển khai phương thức HTML.method_missing để tránh phải định nghĩa một phương thức cho mỗi thẻ HTML.
Đầu tiên, chúng ta định nghĩa một module HTML. Sau đó định nghĩa một phương thức lớp method_missing trong module này:
module HTML def HTML.method_missing(method_id, *args, &block) "<#{method_id}>#{args.first}</#{method_id}>" end endPhương thức của chúng ta đơn giản chỉ xây dựng một thẻ HTML sử dụng method_id bị thiếu — :div cho lời gọi HTML.div, ví dụ.
Lưu ý rằng các phương thức lớp cũng tuân theo Method Lookup Path.
Chúng ta có thể cải tiến trình tạo thẻ HTML của mình bằng cách:
- sử dụng đối số block để xử lý các thẻ lồng nhau
- xử lý các thẻ đơn — ví dụ
<br/>
Nhưng lưu ý rằng chỉ với vài dòng code, chúng ta có thể tạo ra một lượng lớn thẻ HTML.
Vậy, để tóm lại:
method_missing là một điểm nhập tốt để tạo các DSL nơi hầu hết các lệnh sẽ chia sẻ một tập hợp các pattern đã xác định.
Kết luận
Chúng ta đã đi hết chuỗi tổ tiên trong Ruby và đi sâu vào BasicObject#method_missing. BasicObject#method_missing là một phần của Ruby Hook Methods (Các phương thức hook của Ruby). Nó được dùng để tương tác với các object tại những thời điểm chính xác trong vòng đời của chúng. Giống như bất kỳ Ruby Hook Methods nào khác, phương thức hook này phải được dùng cẩn thận. Và bằng cẩn thận, chúng ta muốn nói là nó không bao giờ nên sửa đổi hành vi của Ruby Object Model — trừ khi bạn đang chơi với nó hoặc viết một bài blog về nó ;-)
Voilà!