Hook methods trong Ruby

Triết lý của Ruby là lập trình hạnh phúc (programmer happiness). Ruby tin tưởng mạnh mẽ vào điều đó (programmer happiness) và nó đã cung cấp nhiều cách khác nhau để đạt được. Metaprogramming cung cấp cho các lập trình viên cách để viết dynamic code. Đa luồng cung cấp cho các lập trình một cách thoải mái để viết code. Đó là chính là hook method, nó giúp cho các coder mở rộng hành vi của chương trình trong quá trình runtime.

Các tính năng nói trên, cùng với một số khía cạnh khác, làm cho Ruby là một trong những sự lựa chọn ưa thích để lập trình. Bài viết này sẽ tìm hiểu một số hook method quan trọng trong Ruby. Chúng ta sẽ thảo luận về các khía cạnh khác nhau về hook method, chẳng hạn như đó là gì, sử dụng mục đích gì và làm thế nào để sử dụng vào những trường hợp khác nhau. Chúng ta cũng sẽ xem xét cách thức phổ biến của các Ruby framework / gems / libraries sử dụng hook method để cung cấp những tính năng tuyệt vời.

Chúng ta cùng bắt đầu

Hook Method là gì?

Hook Method cung cấp cách để mở rộng hành vi của chương trình tại thời điểm runtime. Bạn hãy tưởng tượng khả năng nhận được thông báo bất cứ khi nào một lớp con kế thừa từ một lớp cha hoặc xử lý lỗi những phương thức của object nhưng không có phép complier bắn ra ngoại lệ. Đây là một trong số các trường hợp sử dụng hook method. Các framework / library đã sử dụng hook method khác nhau để đạt được chức năng của họ mong muốn.

Chúng tôi sẽ thảo luận về các hook method sau đây móc trong bài viết này:

  • included
  • extended
  • prepended
  • inherited
  • method_missing

Included

Ruby cho chúng ta một cách để viết modular code sử dụng modules (gọi là mixins trong những ngôn ngữ khác) mà sau này có thể được sử dụng trong các modules/classes khác. Ý tưởng đằng sau module là khá đơn giản; đó là một phần code mà có thể được sử dụng ở những nơi khác.

Ví dụ, nếu chúng ta muốn viết một đoạn code trả về một String theo ý muốn bất cứ khi nào một hàm của object được gọi:

module Person
  def name
    puts "My name is Person"
  end
end

Việc này khá đơn giản, và áp dụng chúng ta có như sau:

class User
  include Person
end

Ruby cung cấp nhiều cách khác nhau để sử dụng modules. Một trong những cách đó là include. Những gì include thực hiện là làm cho những phần code trong module có sẵn trên class. Trong trường hợp của chúng ta, những hàm được định nghĩa trong module Person trở thành một trong những hàm của đối tượng của class User. Do đó chúng ta có thể dễ dàng gọi hàm name với object của class User. Ví dụ:

User.new.name
=> My name is Person

Cùng xem xét hook method dựa trên include. included là một hook method được cung cấp bởi Ruby và được gọi bất cứ lúc nào bạn include module vào trong 1 class. Cập nhật Person module chúng ta có như sau:

module Person
  def self.included(base)
    puts "#{base} included #{self}"
  end

  def name
    "My name is Person"
  end
end

Chúng ta thấy một method includedđược định nghĩa trong Person module giống như class method. Thử gọi kiểm tra lại như lúc nãy chúng ta thấy kết quả như sau:

User included Person
My name is Person

Như chúng ta thấy, base trả về tên của classmodule được include vào. Như vậy chúng ta có một tham chiếu đến class bao gồm cả Person

Chắc hẳn mọi người cũng khá quen thuộc với gem Devise. Vậy chúng ta cùng xem cách mà Devise sử dụng included hook. Khai báo devise trong class hay được sử dụng như sau:

devise :database_authenticatable, :registerable, :validatable

Để làm được điều đó devise đã xử lý ở đây. Tôi sẽ dán đoạn code đó vào ngay bên dưới để tiện theo dõi:

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

Các module được truyền vào phương thức devise trong model của chúng ta chính là tham số *modules như là một mảng (array). Thông qua việc xử lý sẽ chuyển các tham số đầu vào thành các hằng số tương ứng. Ví dụ:

:validateable => Validatable
:registration => Registration

và cuối cùng là sử dụng include để ở dòng số 27 để xử lý. Ví dụ Validatable, module này được định nghĩa ở đây và biểu diễn như sau:

def self.included(base)
  base.extend ClassMethods
  assert_validations_api!(base)

  base.class_eval do
    validates_presence_of   :email, if: :email_required?
    validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
    validates_format_of     :email, with: email_regexp, allow_blank: true, if: :email_changed?

    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

Model trong trường hợp này là base. Ở dòng số 5, có một block class_eval. Viết code thông qua class_eval thì giống như viết code trong các class tương ứng.

Extended

Ruby cũng cho phép những lập trình viên extend một module, có chút khác biệt với include. Thay vì xem như đó là hàm của object thì extend lại xem đó là những hàm của class. Cùng xem ví dụ sau:

module Person
  def name
    "My name is Person"
  end
end

class User
  extend Person
end

puts User.name # => My name is Person

Thử so sánh includeextend qua ví dụ sau:

#We are using same Person module and User class from previous example.

u1 = User.new
u2 = User.new

u1.extend Person

puts u1.name # => My name is Person
puts u2.name # => undefined method `name' for #<User:0x007fb8aaa2ab38> (NoMethodError)

Trong bài viết này tôi đã làm rõ includedextended. Những phần còn lại sẽ làm rõ trong những bài viết tiếp theo.

Nguồn + tham khảo: