Một số hook methods quan trọng trong Ruby
Tư tưởng chính của Ruby là khiến lập trình viên cảm thấy hạnh phúc . Và để hiện thực hoá tư tưởng đó, Ruby đã cung cấp rất nhiều thứ cho chúng ta. Metaprogramming cho phép lập trình viên sinh code động trong lúc thực thi chương trình, threading cho phép lập trình viên viết ra những chương trình ...
Tư tưởng chính của Ruby là khiến lập trình viên cảm thấy hạnh phúc. Và để hiện thực hoá tư tưởng đó, Ruby đã cung cấp rất nhiều thứ cho chúng ta. Metaprogramming cho phép lập trình viên sinh code động trong lúc thực thi chương trình, threading cho phép lập trình viên viết ra những chương trình chạy đa luồng, và hook methods cho phép lập trình viên mở rộng hành vi của chương trình trong lúc thực thi.
Các tính năng nói trên, cùng với một số khía cạnh khác của Ruby, làm cho nó trở thành một trong những lựa chọn được ưu tiên để code. Trong bài viết này, chúng ta sẽ cùng khám phá một số hook methods quan trọng trong Ruby. Chúng ta sẽ thảo luận các khía cạnh khác nhau về hook methods, chẳng hạn như chúng là gì, chúng được sử dụng như thế nào và cách chúng ta có thể sử dụng chúng để giải quyết các vấn đề khác nhau. Chúng ta cũng sẽ xem cách mà các framework / thư viện Ruby phổ biến sử dụng chúng để cung cấp các tính năng khá là thú vị.
Bắt đầu nào!
Hook methods cung cấp một cách để mở rộng hành vi của các chương trình trong thời gian chạy. Hãy tưởng tượng bạn có thể nhận được thông báo bất cứ khi nào một lớp con kế thừa một số lớp cụ thể nào đó, hoặc xử lý các phương thức không gọi được trên các đối tượng một cách tự nhiên mà không khiến trình biên dịch đưa ra các ngoại lệ. Đây là một số trường hợp sử dụng cho các hook methods, nhưng việc sử dụng chúng không chỉ giới hạn ở đó. Các framework / thư viện khác nhau đã sử dụng các hook methods khác nhau để đạt được chức năng mong muốn của chúng.
Trong bài này, chúng ta sẽ cùng thảo luận về các hook methods sau nhé:
- included
- extended
- prepended
- inherited
- method_missing
Ruby cung cấp cho chúng ta một cách để viết code theo mô-đun sử dụng các module (hay còn gọi là mixins ở các ngôn ngữ khác) mà có thể được sử dụng sau trong các module hay class khác. Ý tưởng nằm phía sau module khá là đơn giản, nó là 1 đoạn code mà có thể sử dụng lại được ở những chỗ khác.
Ví dụ, nếu chúng ta muốn viết code để trả về cùng một chuỗi bất kì khi nào một phương thức, tạm gọi là phương thức name, được gọi đến, và bạn cũng muốn sử dụng lại đoạn code y hệt thế này ở những chỗ khác nữa. Và đây là cơ hội cho module toả sáng...
Trước tiên, mình sẽ tạo ra module Person:
module Person def name "Person name" end end
Đây là một module khá đơn giản với duy nhất 1 phương thức name có nhiệm vụ luôn trả về 1 chuỗi. Và ta sẽ thử sử dụng đến module đó:
class User include Person end
Ruby cung cấp vài cách khác nhau để sử dụng đến module, một trong số đó là include. Khi bạn sử dụng include trong một class, những phương thức được định nghĩa trong module được gọi sẽ trở thành instance method của class đó. Như trong ví dụ trên thì phương thức name được định nghĩa trong module Person sẽ trở thành instance method của class User. Ta có thể sử dụng phương thức đó như là một phương thức bình thường được định nghĩa trong class User:
puts User.new.name # => Person name
Bây giờ, hãy xem hook method khi sử dụng include, đó chính là included. Nó là một hook method được cung cấp bởi Ruby, sẽ tự động được gọi ra bất cứ khi nào bạn include một module trong 1 module hoặc class khác. Sửa lại module Person bên trên một chút như sau:
module Person def self.included base puts "#{base} included #{self}" end ... end
Bạn có thể thấy, phương thức included được định nghĩa như là 1 class method của module Person. Phương thức này sẽ được gọi ra bất cứ khi nào bạn sử dụng include Person trong một module hay class khác. Nó nhận một tham số chính là module hoặc class đang include module Person.
Và khi chạy lại đoạn code ví dụ bên trên, kết quả sẽ như sau:
User included Person Person name
Bạn có thể thấy, base trả về chính lớp đang include module của chúng ta. Như vậy, chúng ta có thể sử dụng metaprogramming để đạt được chức năng mong muốn của mình. Hãy xem Devise tận dụng included hook như thế nào nhé
included trong Devise
Devise là một trong những thư viện xác thực được sử dụng rộng rãi nhất. Nó cung cấp cho chúng ta các tính năng từ đăng ký đến đăng nhập, từ quên mật khẩu đến khôi phục mật khẩu,... Devise cho phép chúng ta cài đặt nhiều modules mà nó cung cấp bằng cách sử dụng cú pháp vô cùng đơn giản trong model:
devise :database_authenticatable, :registerable, :validatable
Phương thức devise mà chúng ta sử dụng trong models được định nghĩa như sau:
def devise(*modules) options = modules.extract_options!.dup selected_modules = modules.map(&:to_sym).uniq.sort_by do |s| Devise::ALL.index(s) || -1 # follow Devise::ALL order end devise_modules_hook! do include Devise::Models::Authenticatable selected_modules.each do |m| mod = Devise::Models.const_get(m.to_s.classify) if mod.const_defined?("ClassMethods") class_mod = mod.const_get("ClassMethods") extend class_mod if class_mod.respond_to?(:available_configs) available_configs = class_mod.available_configs available_configs.each do |config| next unless options.key?(config) send(:"#{config}=", options.delete(config)) end end end include mod end self.devise_modules |= selected_modules options.each { |key, value| send(:"#{key}=", value) } end end
Tạm hiểu thì phương thức này sẽ include từng module tương ứng với danh sách mà ta truyền vào từ model. Bạn có thể thấy dòng code include mod trong vòng lặp ở phía trên. Ví dụ với module Validatable, có phương thức included được định nghĩa như sau:
def self.included(base) base.extend ClassMethods assert_validations_api!(base) base.class_eval do validates_presence_of :email, if: :email_required? if Devise.activerecord51? validates_uniqueness_of :email, allow_blank: true, if: :will_save_change_to_email? validates_format_of :email, with: email_regexp, allow_blank: true, if: :will_save_change_to_email? else validates_uniqueness_of :email, allow_blank: true, if: :email_changed? validates_format_of :email, with: email_regexp, allow_blank: true, if: :email_changed? end validates_presence_of :password, if: :password_required? validates_confirmation_of :password, if: :password_required? validates_length_of :password, within: password_length, allow_blank: true end end
Ta có thể thấy, Devise sử dụng class_eval cho base (trong trường hợp này chính là model của chúng ta) để bổ sung thêm tính năng cho nó. Bạn có thể hiểu rằng, khi viết code trong class_eval gọi từ 1 class cũng giống như bạn mở class đó ra và viết thêm code vào vậy. Devise sử dụng class_eval để bổ sung thêm validations cho module. Bạn sẽ thấy những validations này khi đăng ký, đăng nhập sử dụng Devise, mà rõ ràng bạn không cần phải tự viết ra những validations đó.
Ruby cũng cho phép lập trình viên extend một module, hơi khác một chút so với include, đó là thay vì áp dụng các phương thức được định nghĩa trong module cho các instances của 1 class, khi sử dụng extend trong class sẽ áp dụng những phương thức đó cho chính class luôn (tạo ra các class methods). Ví dụ:
module Person def name "Person name" end end class User extend Person end puts User.name # => Person name
Tuy nhiên, extend cũng có thể được dùng để gán các phương thức được định nghĩa trong module như là các singleton methods cho các objects nữa. Ví dụ với cùng class User và module Person ở trên:
u1 = User.new u2 = User.new u1.extend Person puts u1.name # => Person name puts u2.name #=> báo lỗi "undefined method `name'"
như vậy, phương thức name là singleton method của object u1, còn các instances khác của User, ví dụ như object u2, thì lại không không có phương thức đó. Cũng giống như included, có một phương thức là extended là hook method khi sử dụng extend. Phương thức này sẽ tự động được gọi ra khi mà module được extend bởi 1 class hoặc 1 module khác. Ví dụ:
module Person def self.extended(base) puts "#{base} extended #{self}" end def name "My name is Person" end end class User extend Person end
Khi chạy đoạn code trên, kết quả là dòng chữ User extended Person sẽ được in ra. Vậy extended được áp dụng như thế nào? Hãy khám phá với ActiveRecord nhé.
extended trong ActiveRecord
Chúng ta có thể thấy ActiveRecord extend module ActiveRecord::Models như sau:
extend ActiveModel::Callbacks
Và đoạn callback của chúng ta ở đây, trong module ActiveModel::Callbacks
def self.extended(base) base.class_eval do include ActiveSupport::Callbacks end end
Như vậy, khi extend module này, nó lại mở class ra và include thêm module ActiveSupport::Callbacks vào cho class.
Có một cách khác để sử dụng các phương thức được định nghĩa trong các module, cách đó là sử dụng prepend. prepend được giới thiệu từ Ruby 2.0 và nó hoàn toàn khác với include và extend. Các phương thức được sử dụng bởi include và extend có thể được ghi đè bằng các phương thức được định nghĩa trong module/class đích. Ví dụ, nếu chúng ta đã định nghĩa một phương thức trong một số module và cũng định nghĩa một phương thức cùng tên như thế trong module/class đích, thì phương thức được định nghĩa trong module / lớp của chúng ta sẽ ghi đè cái từ module được include hay extend. prepend thì khá là khác vì nó ghi đè các phương thức được định nghĩa trong module/class đích với các phương thức được định nghĩa trong module được prepend. Hãy xem ví dụ sau nhé:
module Person def name "My name belongs to Person" end end class User def name "My name belongs to User" end end puts User.new.name # => My name belongs to User
Bây giờ thử sử dụng prepend nhé:
module Person def name "My name belongs to Person" end end class User prepend Person def name "My name belongs to User" end end puts User.new.name # => My name belongs to Person
Khi thêm prepend Person, phương thức được định nghĩa trong Person sẽ ghi đè các phương thức cùng tên được định nghĩa trong class User. Và bạn hoàn toàn có thể gọi hàm được định nghĩa trong class User từ module Person bằng cách sử dụng super.
Khi sử dụng prepend, sẽ có 1 hook method đi kèm với nó đó là prepended, được gọi ra mỗi khi module được prepend vào 1 module/class khác. Thử sửa lại module Person ở ví dụ trên như sau nhé:
module Person def self.prepended(base) puts "#{self} prepended to #{base}" end ... end
Và khi bạn chạy thử lại đoạn code trên, kết quả sẽ thay đổi 1 chút:
Person prepended to User My name belongs to Person
Thừa kế là một trong những khái niệm quan trọng nhất của lập trình hướng đối tượng. Ruby là một ngôn ngữ hướng đối tượng và cung cấp khả năng kế thừa một lớp con từ một số lớp cơ sở. Hãy cùng xem ví dụ sau:
class Person def name "My name is Person" end end class User < Person end puts User.new.name # => My name is Person
Chúng ta đã tạo ra một lớp Person và một lớp con User. Các phương thức được định nghĩa trong Person trở thành một phần của User. Đó chính là tính kế thừa. Nhưng bạn có biết có cách nào để thông báo khi một lớp được kế thừa từ một lớp khác hay không? Đây chính là lúc dùng đến hook method inherited:
class Person def self.inherited(child_class) puts "#{child_class} inherits #{self}" end def name "My name is Person" end end class User < Person end puts User.new.name
phương thức inherited sẽ được gọi mỗi khi có 1 lớp kế thừa từ lớp Person. Khi chạy đoạn code trên sẽ cho ra kết quả:
User inherits Person My name is Person
method_missing có lẽ là hook Ruby được sử dụng rộng rãi nhất. Nó có thể được tìm thấy trong nhiều framework / thư viện Ruby phổ biến. Nó được gọi khi code của chúng ta cố gắng gọi một phương thức không tồn tại trên một đối tượng. Hãy xem ví dụ sau nhé:
class Person def name "My name is Person" end end p = Person.new puts p.name # => My name is Person puts p.address # => undefined method `address' for #<Person:0x007fb730a2b450> (NoMethodError)
Chúng ta đã khai báo một lớp Person đơn giản chỉ với một phương thức name. Sau đó, tạo một cá thể của Person và gọi hai phương thức name và address. Vì name đã được định nghĩa trên Person nên sẽ chạy trơn tru. Tuy nhiên address không được định nghĩa nên sẽ đưa ra một ngoại lệ. Hook method method_missing có thể giúp chúng ta tránh hiển thị các loại ngoại lệ này, nó sẽ xử lý các phương thức chưa được định nghĩa một cách gọn gàng. Hãy viết lại lớp Person một chút như sau:
class Person def method_missing(sym, *args) "#{sym} not defined on #{self}" end def name "My name is Person" end end p = Person.new puts p.name # => My name is Person puts p.address # => address not defined on #<Person:0x007fb2bb022fe0>
method_missing nhận hai tham số: tên của phương thức được gọi và các đối số được truyền vào cho phương thức đó. Đầu tiên, Ruby sẽ tìm phương thức mà chúng ta đang cố gắng gọi, nếu phương thức không được tìm thấy, nó sẽ tìm đến phương thức method_missing. Do chúng ta đã ghi đè phương thức method_missing trên class Person, vì vậy Ruby sẽ gọi nó và sẽ không đưa ra bất kỳ ngoại lệ nào như mặc định.
Tham khảo: https://www.sitepoint.com/rubys-important-hook-methods/