Tìm hiểu về Macros trong Ruby

1. Giới thiệu

Trong Rails bạn có thể thường xuyên làm việc với một số các class method như has_many, belongs_to,... những class method đó còn được gọi chung là Macros. Ví dụ:

class Movie < ActiveRecord::Base
  has_many :reviews
end

class Project < ActiveRecord::Base
  has_many :tasks
end

Rất quen đúng không 😄

Đối với những người bắt đầu sử dụng Rails (và Ruby) có thể khai báo các đoạn mã trên là do sự hỗ trợ của Rails. Nhưng sự thực ở đây là không có sự hỗ trợ nào cả, tất cả chỉ là mã code của Ruby, Ruby đã làm cho việc lập trình theo kiểu khai báo này dễ dàng hơn cho chúng ta.

Bài viết này sẽ giúp các bạn hiểu được vì sao lại có thể khai báo như vậy, qua đó giúp các bạn hiểu hơn về ngôn ngữ lập trình Ruby.

2. Làm rõ vấn đề.

Để hiểu rõ đoạn code trên phần giới thiệu các bạn cần nắm rõ một số các khái niệm cơ bản trong Ruby.

2.1 Singleton Method của một Object

Dưới đây là một đối tượng String và chúng ta gọi phương thức upcase được định nghĩa trong lớp String:

dog1 = "Rosco"

puts dog1.upcase  # => ROSCO

Tương tự, ta có thể gọi đến bất cứ method nào thuộc lớp string theo cách trên, tuy nhiên trong ruby ta còn cho phép ta có thể định nghĩa một method trên một object cụ thể.

Ví dụ:

def dog1.hunt
  puts "WOOF!"
end

dog1.hunt  # => WOOF!

Đối tượng dog1 gọi tới phương thức hunt, và kết quả là WOOF!

Ta thử gọi phương thức hunt tứ một đối tượng khác:

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

Việc gọi hunt trên đối tượng dog2 là không thế.

Suy ra, singleton method là method chỉ được định nghĩa cho 1 object cụ thể.

Như vậy là các bạn đã hiểu về singleton method, và đây cũng là một hàm cơ bản để xây dựng lên các class method như has-many đó 😄.

2.2 Class cũng là Object

Đây là một class Movie

class Movie
end

Trong Ruby, class cũng là object.

p Movie.class  # => Class

Tất nhiên, ta cũng có thể xem object_id của 1 class"

p Movie.class.object_id # => 70233956488680

Thực tế thì tên của class Movie là một constant và nó tham chiếu đến object class.

2.3 Singleton Method của Class

Như đã nói mỗi class trong 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, ví dụ:

movie_class = Movie

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

my_class_method ở đây sẽ là một singleton method định nghĩa cho object Movie.

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

Vì vậy, về cơ bản, điều đó giống như những gì chúng ta đã làm với đối tượng dog1 trước đó. Trong trường hợp này, ta định nghĩa một singleton method cho object Class.

Để cho đoạn code trên trở nên ngắn gọn hơn ta có thể viết lại như sau:

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

Nhìn quen thuộc hơn rồi đúng không?

Từ nhưng điều trên ta thấy : class method thực chất chỉ là một singleton method được định nghĩa cho object class.

2.4 Các class cũng là các mã thực thi.

Thêm một điểu chúng ta cần phải hiểu là class definitions are executable code , theo dõi đoạn code dưới đây:

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

Và kết quả là:

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

Theo dõi kết quả thì chúng ta thấy code có thể thực thi ngay trong quá trình định nghĩa class. Hơn thế nữa ta cũng có thể thực thi nó trong cả quá trình định nghĩa method luôn 😄

Ví dụ:

puts "Before class definition"

class Movie
  puts "Inside class definition"

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

  Movie.my_class_method
end

puts "After class definition"

Kết quả:

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

Tuy nhiên Ruby sẽ gán biến self tới class object đang được định nghĩa đó nên ta có thể sử dụng cách sau:

puts "Inside class definition of #{self}"

Và kết quả là:

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

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.

Ta có thể sửa lại đoạn code trên như sau:

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

self.my_class_method

Đến đây thì self.my_class_method bắt đầu nhìn giống với hàm has_many , ngoài việc nó thừa ra từ self!

Nhưng ta cũng có thể bỏ luôn self vì trong trường hợp này Ruby sẽ có thể tự ngầm hiểu self là receiver.

my_class_method

Oke, bây giờ chúng ta thực hiện thay đổi tên phương thức thành has_many xem nó có hoạt động tốt không nhé 😄

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 đối số của nó, vì vậy việc gọi has_many :reviews sẽ truyền :reviews như là đối số cho has_many Và kết quả nó vẫn hoạt động.

Movie has many reviews

2.5 Define Method

Oke, vậy là phương thức has_many đã được xây dựng, tuy nhiên như chúng ta biết trong Rails hàm has_many có tác dụng khởi tạo một assocation kèm theo các 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. Ở đây ta sẽ làm 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

Tuy nhiên nếu làm như thế thế thì ta không thể thêm các association khác, ví dụ như genres chẳng hạn.

Vì thế 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 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 đối số 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 đó. define_method 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. Chạy tử chương trình ta có method reviews đã được định nghĩa:

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

Oke bây giờ ta có thể gọi phương thức has_many bao nhiêu lần tùy thích.

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

movie.reviews
movie.genres

Hiện tại thì has_many chỉ dành cho Movie mà thôi. Nhưng trong rails 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.

2.6 Class Method Inheritance

Để share has_many sử dụng kế thừa, trước tiên chúng ta sẽ định nghĩa nó trong class Base, bên trong là một module có tên là ActiveRecord(để nó giống với Rails).

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

Sau đó, class Movie có thể kế thừa từ lớp ActiveRecord :: Base

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

Và mọi thứ hoạt động:

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

Lưu ý: giá trị của self ở đây là Movie class nhé. Bây giờ khi chúng ta kế thừa từ ActiveRecord::Base thì bất cứ class nào cũng có thể sử dụng has_many method. Ví dụ: ta có một class Project

class Project < ActiveRecord::Base
  has_many :tasks
end

project = Project.new
project.tasks

Kết qủa là:

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

3. Tổng kết

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

Thông qua bài viết này hi vọng các bạn đang bắt đầu với Ruby on Rails có thêm các kiến thức liên quan Macros để thực hiện tốt các dự án trong tương lai 😄. Cảm ơn!

Nguồn dịch: https://pragmaticstudio.com/tutorials/ruby-macros