Macros trong Ruby

Trong Rails, bạn có thể để ý là mình đã dùng qua các class method như has_many, belongs_to, và một vài hàm khác tương tự nữa.

class Movie < ActiveRecord::Base
  has_many :reviews
end

Những người mới tiếp xúc với Rails (và Ruby) thường cho rằng những khai báo như thế này là một phần "magic" của Rails.

Thực ra, không có gì magic ở đây cả - đây chỉ là Ruby code mà thôi. Những hàm này - trong Ruby - thực chất có hẳn một tên gọi riêng cho chúng: Macros.

Thậm chí, trong Ruby thì việc khai báo theo stype như thế này dễ hơn bạn nghĩ đấy ! Và một khi bạn hiểu được cách nó làm việc, bạn sẽ trở nên tự tin hơn với Rails và thể tự tin sử dụng công cụ rất mạnh mẽ này trong code của mình.

Giờ, hãy thử tự viết một phiên bản đơn giản của dòng code trên - từ đầu - dựa trên những nguyên lý cơ bản của nó !

Singleton Method của một Object

Đây là một object String đơn thuần, và khi ta gọi method upcase được định nghĩa trong class String:

dog1 = 'Rosco'
p dog.updase    # => ROSCO

Ta có thể gọi bất kì một method nào trong String cũng theo cách trên; nhưng ngoài ra, thậm chí Ruby còn cho phép ta định nghĩa method trên một object cụ thể nữa. Lấy ví dụ, ở đây ta có thể định nghĩa method hunt trên đối tượng dog1:

def dog1.hunt
  p 'WOOF!"
end

dog1.hunt # => WOOF!

Giờ, ta gọi hunt trên dog1, nó in ra "WOOF!".

Hiển nhiên, việc gọi hunt trên một đối tượng khác - dog2 chẳng hạn - là không thể:

dog2 = 'Snoopy'
dog2.hunt   # => undefined method `hunt`

Method hunt chỉ được định nghĩa cho đối tượng dog1. Bạn sẽ thường xuyên nghe đến khái niệm này như là singleton method - method chỉ được định nghĩa cho 1 object cụ thể mà thôi.

Nghe hay đấy, nhưng tại sao ta lại cần đến cái này ? Hóa ra, singleton method được sử dụng rất nhiều trong Ruby! Và ta cũng sẽ cần đến nó để tự tạo cho mình một hàm has_many đấy.

Class cũng là Object

Khái niệm này thì chắc các bạn coder Ruby cũng đã nghe đến nhiều hơn rồi đúng không ?

Đây là một class Movie:

class Movie
end

Trong Ruby, class cũng là object !

p Movie.class # => Class

Class của class MovieClass 🤔 Tất nhiên, ta cũng có thể xem object id của 1 class là cái gì:

p Movie.class.object_id # => 70233956488680

Movie là một constant reference tới object Class

Vậy tóm lại: class trong Ruby là object, và mỗi một Ruby class là một object của class Class.

Singleton Method của Class

Ok, và nếu mỗi class Ruby là một object, vậy thì ta có thể làm bất cứ thứ gì với chúng như với một object thông thường vầy. Ví dụ, ta có thể định nghĩa một singleton method cho object Class mà được reference qua Movie ở trên:

movie_class = Movie

def movie_class.my_class_method
  p "Running class method ..."
end

my_class_method ở đây sẽ là một singleton method định nghĩa cho object Movie class. Để gọi được nó, receiver của lời gọi sẽ là object Movie class.

movie_class.my_class_method  # => "Running class method ..."

Tức là về cơ bản - việc ta vừa làm là tương tự như đối với dog mở trên. Trong trường hợp này, ta định nghĩa một singleton method cho object Class.

Để cho gọn hơn, ta có thể không cần dùng đến biến tạm thời movie_class khi mà thực chất nó chỉ là reference tới object Movie.

def Movie.my_class_method
  p "Running class method ..."
end

Movie.my_class_method  # => "Running class method ..."

Trong thực tế, ta thường để hàm trên vào thẳng trong body của class:

class Movie
  def Movie.my_class_method
    puts "Running class method..."
  end
end

Nhìn quen thuộc chứ ?

Giờ thì bạn đã biết thêm một nguyên lý nữa về Ruby - khái niệm về class method:

Trong ruby , class method thực chất chỉ là một singleton method được định nghĩa cho object class.

Ok, tuy nhiên đoạn code trên nhìn vẫn chưa có gì giống với hàm has_many của chúng ta cả ?

Class definition cũng là các đoạn code executabe

Nguyên lý thứ hai: class definition là các đoạn code có-thể-thực-thi (dịch ra nghe không dễ hiểu cho lắm ? 🤔 ). Nhìn đoạn code dưới đây có lẽ sẽ dễ hiểu hơn.

puts "Before class definition"

class Movie
  puts "Inside class definition"

  def Movie.my_class_method
    puts "Running class method..."
  end
end

puts "After class definition"

Movie.my_class_method

Chạy thử đoạn trên, ta sẽ có được:

Before class definition
Inside class definition
After class definition
Running class method...

Nhìn 4 dòng output được in ra, bạn có nhận thấy dòng nào đặc biệt nhất không ?

Inside class definition => Như vậy là code được thực thi ngay trong quá trình định nghĩa một class. Thực ra, ta có thể chạy method my_class_method ngay bên trong class definition của Movie:

puts "Before class definition"

class Movie
  puts "Inside class definition"

  def Movie.my_class_method
    puts "Running class method..."
  end

  Movie.my_class_method  # Chạy method này ngay cả khi class vẫn đang được khai báo.
end

puts "After class definition"

Sẽ cho ta kết quả dưới đây:

Before class definition
Inside class definition
Running class method...
After class definition

Như vậy là method my_class_method có thể được gọi ngay cả khi class vẫn đang trong quá trình khai báo !

Nhìn lại một chút, có vẻ việc gọi thông qua constant Movie nhìn hơi thừa thãi. Hóa ra, bên trong khai báo của class, Ruby sẽ gán biến self tới class object đang được định nghĩa đó. Hãy thử thay đổi một chút với lời gọi in ra thứ 2 trong ví dụ trên này như sau:

  p "Inside class definition of #{self}"

Ta nhận được

Before class definition
Inside class definition of Movie
Running class method...
After class definition

Tức là self ở đây được gán cho class object hiện tại đang được định nghĩa, trong trường hợp này là class object Movie.

Tức là ta có thể viết lại như sau:

def self.my_class_method
  puts "Running class method..."
end

self.my_class_method

Đến đoạn này thì self.my_class_method bắt đầu nhìn giống giống với hàm has_many mà chúng ta muốn rồi đấy, ngoài việc nó thừa ra từ self!

Trong bối cảnh này, self ở đây sẽ đóng vai trò receiver của lời gọi my_class_method. Tuy nhiên, nếu như không có một receiver cụ thể nào được nhắc tới, Ruby sẽ có thể tự ngầm hiểu self là receiver. Vậy là thành ra ta cũng có thể bỏ luôn nốt self đi:

my_class_method

Đổi tên

Như vậy là nó đã giống với thứ mà chúng ta muốn rồi đấy. Giờ thì đổi tên lại cho đúng xem thế nào:

class Movie
  def self.has_many(name)
    puts "#{self} has many #{name}"
  end
  
  has_many :reviews
end

Chú ý rằng has_many nhận name là một argument của nó, vì vậy việc gọi has_many :reviews sẽ truyền :reviews như là argument cho has_many

Nếu chạy nó, ta sẽ nhận được:

Movie has many reviews

Chú ý lại một lần nữa: giá trị của self ở đây chính là class object Movie.

Định nghĩa method

Ok, vậy là hàm has_many đã được gọi trong class definition, vậy thì giờ ta nên làm gì bên trong method này đây ? Trong Rails, hàm has_many có tác dụng khởi tạo một assocation kèm theo một đống method phụ trợ đi kèm với nó.

Ví dụ, ta có thể khởi tạo method reviews mà sẽ trả về các review được kết nối tới movie:

movie = Movie.new
movie.reviews # => undefined method

Nếu là trong Rails, hàm này sẽ trả về mảng các review có kết nối tới movie. Nhưng ở đây thì ta chưa có cái đó 😃

Ta có thể hard-code như thế này:

def self.has_many(name)
  puts "#{self} has many #{name}"

  def reviews
    puts "SELECT * FROM reviews WHERE..."
    puts "Returning reviews..."
    []
  end
end

Nhưng, hard-code kiểu trên thì sẽ lại không hoạt động nếu ta có thêm association khác, ví dụ như genres chẳng hạn:

has_many :reviews
has_many :genres

Để làm cho nó chạy, ta cần phải định nghĩa dynamically một method cho mỗi association: trong trường hợp này là một method mang tên reviews và một mang tên genres. Ta sẽ không biết được tên chính xác của chúng cho tới runtime - tới khi class được định nghĩa.

Để thực hiện được điều này, ta có thể dùng define_method:

def self.has_many(name)
  puts "#{self} has many #{name}"

  define_method(name) do
    puts "SELECT * FROM #{name} WHERE..."
    puts "Returning #{name}..."
    []
  end
end

define_method nhận argument là tên của method sẽ được khởi tạo, cùng với một block mà sẽ là body của method đó. Chú ý, define_method luôn luôn định nghĩa instance method trong receiver, trong trường hợp này là object Movie. Vì vậy đến cuối cùng ta sẽ nhận được instance method reviews cho class Movie.

Giờ nếu ta chạy thử, ta có thể thấy method reviews được định nghĩa:

Movie has many reviews
SELECT * FROM reviews WHERE...
Returning reviews...

Và bởi vì ta định nghĩa method has_many dynamically, ta có thể gọi nó bao nhiêu lần tùy thích:

class Movie < ActiveRecord::Base
  has_many :reviews
  has_many :genres
end

movie.reviews
movie.genres

Hai method được định nghĩa, và ta có thể gọi cả hai:

Movie has many reviews
Movie has many genres
SELECT * FROM reviews WHERE...
Returning reviews...
SELECT * FROM genres WHERE...
Returning genres...

Khá là hay rồi ! Tuy nhiên, hiện giờ thì has_many chỉ dành cho Movie mà thôi. Thứ ta muốn ở đây là has_many có thể dùng được cho nhiều class. Và ta có thể thực hiện điều đó bằng kế thừa.

Kế thừa class method

Để share has_many sử dụng kế thừa, đầu tiên ta sẽ định nghĩa nó trong một class - tạm gọi là Base đi - bên trong một module tên là ActiveRecord chẳng hạn (bạn có thấy quen quen không ^^)

module ActiveRecord
  class Base
    def self.has_many(name)
      puts "#{self} has many #{name}"
      define_method(name) do
        puts "SELECT * FROM #{name}..."
        puts "Returning #{name}..."
      end
    end
  end
end

Giờ, nếu Movie mà kế thừa từ ActiveRecord::Base (bỏ cái đoạn định nghĩa has_many ở trên khỏi Movie đi):

class Movie < ActiveRecord::Base
  has_many :reviews
  has_many :genres
end

Lại chú ý một lần nữa: self ở đây sẽ là Movie chứ không phải Base !

Movie has many reviews
Movie has many genres
SELECT * FROM reviews WHERE...
Returning reviews...
SELECT * FROM genres WHERE...
Returning genres...

Tương tự vậy, ta có thể định nghĩa một class Project khác.

class Project < ActiveRecord::Base
  has_many :tasks
end

project = Project.new
project.tasks

Và trong trường hợp này self sẽ mang giá trị là Project

Project has many tasks
SELECT * FROM tasks WHERE...
Returning tasks...

Kết luận

Vậy là ta đã kết thúc bài viết ở đây với việc implement thành công method has_many mà ta muốn:

class Movie < ActiveRecord::Base
  has_many :reviews
  has_many :genres
end

class Project < ActiveRecord::Base
  has_many :tasks
end

Rails cung cấp rất, rất nhiều đồ chơi cho bạn - sẽ có nhiều thứ mà được coi như là magic của Rails vậy; nhưng hi vọng rằng, thông qua bài viết này, bạn có thể hiểu được thêm về bản chất của một vài trong số chúng.

Nguồn dịch

https://pragmaticstudio.com/tutorials/ruby-macros