Metaprogramming trong Ruby
Bài đăng này đã không được cập nhật trong 3 năm
Bài viết gốc: https://www.toptal.com/ruby/ruby-metaprogramming-cooler-than-it-sounds
Metaprogramming
Metaprogramming là một kỹ thuật mà bạn có thể viết code tự động sinh ra code trong thời gian chạy. Điều này có nghĩa là bạn có thể định nghĩa các phương thức cho một class ngay trong khi đang chạy một chương trình nào đó. Điên rồ, phải không? Tóm lại thì bằng cách sử dụng metaprogramming, bạn có thể sửa đổi các class, kiểm tra các phương thức không tồn tại và tự tạo ra chúng ngay lập tức, giúp cho code DRY, tránh lặp lại, v.v.
Cơ bản
Trước khi đi sâu vào tìm hiểu thêm, chúng ta cần lắm rõ được mức cơ bản. Và cách tốt nhất để làm điều đó là bằng ví dụ. Hãy cùng bắt đầu tìm hiểu Ruby metaprogramming từng bước một nhé. Bạn có lẽ sẽ đoán được đoạn code này đang làm gì:
class Developer
def self.backend
"I am backend developer"
end
def frontend
"I am frontend developer"
end
end
Chúng ta đã định nghĩa một class với hai methods. Phương thức đầu tiên là một class method, và phương thức thứ hai là của instance. Đây là ví dụ cơ bản trong Ruby nhưng có nhiều thứ chúng ta cần hiểu rõ hơn. Cần chỉ ra rằng bản thân lớp Developer thực sự là một đối tượng. Trong Ruby, mọi thứ đều là một đối tượng, bao gồm cả các class. Có nghĩ là Developer cũng là một instance, nó là một instance của lớp Class
. Đây là một mô hình thể hiện đối tượng Ruby trông như thế nào:
p Developer.class # Class
p Class.superclass # Module
p Module.superclass # Object
p Object.superclass # BasicObject
Điểm đặc biệt ở đây là chúng ta cần hiểu ý nghĩa của self
trong đoạn code trên. Phương thức frontend
là một phương thức cơ bản có sẵn trên instance của Developer
, nhưng tại sao backend
lại là một class method? Mỗi đoạn code được thực thi trong Ruby đều được thực thi trên một self
cụ thể. Khi trình thông dịch Ruby thực hiện chạy code nó luôn theo dõi giá trị của self
trên tất cả các dòng. self
luôn tham chiếu đến một đối tượng và nó cũng có thể được thay đổi dựa trên các đoạn mã được thực thi. Ví dụ, bên trong một định nghĩa class, self
sẽ tham chiếu đến chính class đó và là một instance của lớp Class
.
class Developer
p self
end
# Developer
Bên trong một instance method, self
tham chiếu đến instance của class hiện tại.
class Developer
def frontend
self
end
end
p Developer.new.frontend
# #<Developer:0x2c8a148>
Bên trong một class method, self
sẽ tham chiếu đến chính class hiện tại (điều này sẽ được nói đến thêm):
class Developer
def self.backend
self
end
end
p Developer.backend
# Developer
Vậy cuối cùng thì class method là gì? Trước khi trả lời câu hỏi đó, chúng ta cần đề cập đến sự tồn tại của một khái niệm gọi là metaclass, thương được biết đến là singleton class và eigenclass. Class method frontend
thực ra là một instance method của metaclass trên object Developer
. Metaclass về cơ bản là một lớp mà Ruby tạo ra và chèn vào hệ thống phân cấp kế thừa để giữ các phương thức của một lớp, do đó không can thiệp vào các instance được tạo ra từ lớp đó.
Metaclasses
Mỗi đối tượng trong Ruby đều có metaclass riêng. Ví dụ, chúng ta hãy tạo một đối tượng String
và thao tác với metaclass của nó:
example = "I'm a string object"
def example.something
self.upcase
end
p example.something
# I'M A STRING OBJECT
Những gì chúng ta làm ở đây là chúng ta đã thêm một singleton method something
vào một đối tượng. Sự khác biệt giữa các class method và singleton method là các class method có sẵn trên tất cả các trường hợp của một đối tượng lớp, trong khi các singleton method chỉ có sẵn cho một trường hợp đơn lẻ.
Ví dụ trước có thể được viết lại như thế này:
example = "I'm a string object"
class << example
def something
self.upcase
end
end
Cú pháp khác nhau nhưng nó có kết quả giống nhau. Bây giờ chúng ta hãy quay lại ví dụ trước, xem lại một cách khác để định nghĩa một class method:
class Developer
def self.backend
"I am backend developer"
end
end
Đây là một định nghĩa cơ bản mà hầu như mọi người đều sử dụng.
def Developer.backend
"I am backend developer"
end
Đây là cách tương tự, để chúng ta đã định nghĩa một class method backend
cho Developer
. Chúng ta không sử dụng self
nhưng vẫn có thể định nghĩa một class method:
class Developer
class << self
def backend
"I am backend developer"
end
end
end
Một lần nữa, chúng ta đang định nghĩa một class method, nhưng sử dụng cú pháp tương tự như cú pháp mà chúng ta đã sử dụng để định nghĩa một singleton method cho một đối tượng String
. Bạn có thể nhận thấy rằng chúng ta đã sử dụng self
ở đây để tham chiếu chính class Developer
. Đầu tiên, trong class Developer
, chúng ta biết self
tham chiếu đến chính class Developer
, tiếp theo viết class << self
làm cho self
tương đương với metaclass của Developer
. Sau đó định nghĩa phương thức backend
trong metaclass của Developer
.
class << Developer
def backend
"I am backend developer"
end
end
Với cách viết như trên, chúng ta đang đặt self
tham chiếu tới metaclass của Developer
bên trong block trên. Và backend
được định nghĩa là method của metaclass của Developer
thay vì Developer
.
Hãy xem metaclass này hoạt động như thế nào trong cây kế thừa:
Như bạn đã thấy trong các ví dụ trước, không có bằng chứng thực tế nào cho thấy metaclass thậm chí còn tồn tại. Nhưng chúng ta có thể sử dụng một thủ thuật nhỏ có thể cho chúng ta thấy sự tồn tại của lớp vô hình này:
class Object
def metaclass_example
class << self
self
end
end
end
Nếu chúng ta định nghĩa một instance method trong lớp Object
(vâng, chúng ta có thể mở lại bất kỳ lớp nào vào bất kỳ lúc nào, đó là một vẻ đẹp khác của metaprogramming) chúng ta có self
tham chiếu đến Object
, và sau đó dùng cú pháp class << self
để tham chiếu đến metaclass của object hiện tại. Phương thức trên trả về self
, hiện tại cũng chính là bản thân metaclass. Vì vậy, bằng cách gọi phương thức trên, chúng ta có thể nhận được metaclass của một đối tượng bất kỳ. Hãy viết lại lớp Developer
của chúng ta và bắt đầu khám phá một chút:
class Developer
def frontend
p "inside instance method, self is: " + self.to_s
end
class << self
def backend
p "inside class method, self is: " + self.to_s
end
end
end
developer = Developer.new
developer.frontend
# "inside instance method, self is: #<Developer:0x2ced3b8>"
Developer.backend
# "inside class method, self is: Developer"
p "inside metaclass, self is: " + developer.metaclass_example.to_s
# "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
Và hãy cùng kiểm tra, frontend
là một instance method của Developer
còn, backend
là của metaclass của Developer.
p developer.class.instance_methods false
# [:frontend]
p developer.class.metaclass_example.instance_methods false
# [:backend]
Ngoài ra bạn có thể chỉ cần sử dụng phương thức singleton_class
có sẵn trong Ruby để lấy metaclass mà không cần làm như trên.
p developer.class.singleton_class.instance_methods false
# [:backend]
Định nghĩa method sử dụng class_eval
và instance_eval
Có một cách nữa để tạo một class method, và đó là sử dụng instance_eval
:
class Developer
end
Developer.instance_eval do
p "instance_eval - self is: " + self.to_s
def backend
p "inside a method self is: " + self.to_s
end
end
# "instance_eval - self is: Developer"
Developer.backend
# "inside a method self is: Developer"
Phần code trên thì trình thông dịch của Ruby đánh giá ngữ cảnh là thực hiện trên một instance, ở đây là đối tượng Developer
. Và khi bạn định nghĩa một phương thức trên một đối tượng, bạn đang tạo class method hoặc một singleton method. Trong trường hợp này thì là class method. Chính xác thì là class method cũng là các singleton method nhưng là singleton method của một class, trong khi các trường hợp khác là singleton method của một đối tượng.
Còn với class_eval
, ngữ cảnh là thực hiện trên instance của một class, thay vì một instance. Nó tương đương với mở lại một class. Đây là cách class_eval có thể được sử dụng để tạo một phương thức instance:
Developer.class_eval do
p "class_eval - self is: " + self.to_s
def frontend
p "inside a method self is: " + self.to_s
end
end
# "class_eval - self is: Developer"
p developer = Developer.new
# #<Developer:0x2c5d640>
developer.frontend
# "inside a method self is: #<Developer:0x2c5d640>"
Tóm lại, khi bạn gọi phương thức class_eval, bạn thay đổi self
để tham chiếu đến class ban đầu còn khi bạn gọi instance_eval, self
thay đổi để tham chiếu đến metaclass của class ban đầu.
Định nghĩa một missing method
Một phần khác của metaprogramming là method_missing
. Khi bạn gọi phương thức này trên một đối tượng, đầu tiên Ruby kiểm tra class và duyệt qua các instance methods của nó. Nếu không tìm thấy phương thức nào nó sẽ tiếp tục với ancestors chain. Nếu Ruby vẫn không tìm thấy phương thức, nó sẽ gọi một phương thức khác có tên method_missing
, đây là một instance method của class Kernel
mà mọi đối tượng đều kế thừa.Vì chúng ta chắc chắn rằng Ruby sẽ gọi phương thức này cuối cùng cho các phương thức bị thiếu, chúng ta có thể sử dụng nó để thực hiện một số thủ thuật.
define_method
là một phương thức được định nghĩa trong lớp Module
mà bạn có thể sử dụng để tạo các method một cách linh động. Để sử dụng define_method
, bạn gọi nó với tên của phương thức mới và một block trong đó các tham số của block trở thành các tham số của phương thức mới. Sự khác biệt giữa việc sử dụng def
để tạo một phương thức và define_method
là gì? Không có nhiều sự khác biệt ngoại trừ bạn có thể sử dụng define_method
kết hợp với method_missing
để viết mã DRY. Nói một cách chính xác, bạn có thể sử dụng define_method
thay vì def
để định nghĩa phương thức trong một lớp, nhưng đó là một câu chuyện hoàn toàn khác. Hãy xem một ví dụ đơn giản:
class Developer
define_method :frontend do |*my_arg|
my_arg.inject(1, :*)
end
class << self
def create_backend
singleton_class.send(:define_method, "backend") do
"Born from the ashes!"
end
end
end
end
developer = Developer.new
p developer.frontend(2, 5, 10)
# => 100
p Developer.backend
# undefined method 'backend' for Developer:Class (NoMethodError)
Developer.create_backend
p Developer.backend
# "Born from the ashes!"
Điều này cho thấy cách thức dùng define_method
để tạo một phương thức instance mà không sử dụng def
. Tuy nhiên, chúng ta có thể làm được nhiều việc hơn với chúng. Hãy xem đoạn mã này:
class Developer
def coding_frontend
p "writing frontend"
end
def coding_backend
p "writing backend"
end
end
developer = Developer.new
developer.coding_frontend
# "writing frontend"
developer.coding_backend
# "writing backend"
Đoạn code trên không hề DRY, và chúng ta có thể dùng define_method
để làm nó DRY:
class Developer
["frontend", "backend"].each do |method|
define_method "coding_#{method}" do
p "writing " + method.to_s
end
end
end
developer = Developer.new
developer.coding_frontend
# "writing frontend"
developer.coding_backend
# "writing backend"
Đoạn code trên tốt hơn nhiều, nhưng vẫn chưa hoàn hảo. Tại sao? Nếu chúng ta thêm một method coding_debug
vào ví dụ trên, chúng ta cần thêm debug
vào mảng. Nhưng bằng cách sử dụng method_missing
, chúng tôi có thể khắc phục điều này:
class Developer
def method_missing method, *args, &block
return super method, *args, &block unless method.to_s =~ /^coding_\w+/
self.class.send(:define_method, method) do
p "writing " + method.to_s.gsub(/^coding_/, '').to_s
end
self.send method, *args, &block
end
end
developer = Developer.new
developer.coding_frontend
developer.coding_backend
developer.coding_debug
Đoạn mã này hơi phức tạp nên chúng ta hãy chia nhỏ nó ra. Việc gọi một phương thức không tồn tại sẽ kích hoạt method_missing
. Ở đây, chúng ta muốn tạo một phương thức mới chỉ khi tên phương thức bắt đầu bằng "coding_". Nếu không thì chỉ cần chạy super để thự thi mặc định. Còn lại thì sử dụng define_method
để định nghĩa phương thức mới hiện tại đang thiếu. Và như vậy chúng ta có thể làm code DRY. Ngoài ra thì define_method
là một private method Module
nên chúng ta cần dùng send
để gọi nó.
Kết thúc
Hy vọng rằng bài viết này có thể giúp bạn tiến gần hơn đến việc hiểu về metaprograming
và thậm chí có thể xây dựng DSL của riêng bạn, và bạn có thể sử dụng để viết mã hiệu quả hơn.
All rights reserved