+4

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 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 là gì?

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

Included

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 đó.

extended

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.

prepended

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 includeextend. Các phương thức được sử dụng bởi includeextend 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

inherited

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

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 nameaddress. 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/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí