Những điều nên biết khi sử dụng Metaprogramming trong Ruby
Bài đăng này đã không được cập nhật trong 3 năm
Bạn thường nghe nói Metaprogramming là cái mà chỉ dành riêng cho ngôn ngữ Ruby, và nó không đơn giản cho những lập trình viên trung bình. Nhưng sự thật là Metaprogramming không quá đáng sợ như vậy, bài viết này sẽ cung cấp những nền tảng cho những fresh developer để họ có thể sử dụng và gặt hái được những lợi ích từ nó.
Cần lưu ý rằng, metaprogramming rất rộng và nó nên được sử dụng một cách triệt để khi lập trình. Vì vậy, bài viết sẽ cố gắng để đưa ra một số ví dụ thực tế mà tất cả mọi người có thể sử dụng trong việc lập trình hằng ngày.
I, Metaprogramming
Metaprogramming là kỹ thuật cho phép bạn có thể viết code để sinh ra những code khác tự động khi chạy. Điều này có nghĩa là bạn có thể định nghĩa các method và lớp trong suốt thời gian chạy runtime. Quả là thú vi? Tóm lại, sử dụng metaprogramming bạn có thể mở lại và chỉnh sửa các lớp, bắt các method không tồn tại và tạo ra chúng. tạo ra những code DRY bằng việc tránh trùng lặp duplicate...
II, The basics
Trước khi đi sâu vào metaprogramming, chúng ta nên tìm hiểu một số điều cơ bản. Và cách tốt nhất để làm những điều đó là thông qua các ví dụ. Hãy bắt đầu từng bước một để hiểu về metaprogramming trong Ruby. Bạn hoàn toàn có thể đoán được đoạn code sau đ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 lớp với 2 method. Method đầu tiên trong lớp này là một class method và cái thứ 2 là một instance method. Đây là những lý thuyết cơ bản của ruby, nhưng có nhiều hơn cái đang xảy ra đằng sau dòng code này, cái mà chúng ta cần hiểu rõ trước khi chúng ta đi sâu hơn. Cần nhấn mạnh rằng, lớp Developer chính là một đối tượng. Trong Ruby, tất cả mọi thứ đều là đối tượng, bao gồm các lớp. khi Developer là một instance thì nó là một instance của lớp Class. Hình dưới đây sẽ mô tả mô hình đối tượng trong Ruby:
p Developer.class # Class
p Class.superclass # Module
p Module.superclass # Object
p Object.superclass # BasicObject
Một điều quan trọng quan tâm ở đây là ý nghĩa của từ khóa “self”. Method frontend là một method thông thường, được xác định như là một instance của lớp Developer, nhưng tại sao method backend lại là một class method? Tất cả các phần code thực thi trong Ruby được thực hiện bằng từ khóa đặc biệt “self”. Khi Ruby thông dịch bất cứ dòng code nào, nó luôn theo dõi giá trị của self cho bất cứ dòng code nào. self luôn refer đến vài đối tượng, nhưng đối tượng đó có thể thay đổi trên những code được thực thi. Cho ví dụ, trong định nghĩa của một lớp, self refer đến chính nó, chính là một thể instance của lớp Class:
class Developer
p self
end
# Developer
Trong instance methods, self tham chiếu đến một thể hiện của lớp Developer:
class Developer
def frontend
self
end
end
p Developer.new.frontend
# #<Developer:0x2c8a148>
Bên trong một class methods, self tham chiếu đến chính lớp đó, là một thể hiện của lớp Class:
class Developer
def self.backend
self
end
end
p Developer.backend
# Developer
Thật là thú vị, nhưng sau tất cả thì class method là gi? 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 thứ được gọi là metaclass hay còn được gọi là singleton class và eigenclass. Class method backend mà chúng ta đã định nghĩa ban đầu chính là một instance method được định nghĩa trong lớp metaclass của lớp Developer, được sử dụng cho đối tượng Developer. Một metaclass thực ra chính là một lớp, Ruby đã tạo ra và chèn nó vào hệ thống phân cấp thừa kế lưu giữ các phương thức lớp.
III, Metaclasses
Mỗi đối tượng trong Ruby đều có metaclass riêng của mình. Nó có vẻ vô hình đối với các nhà phát triển. Nhưng nó tồn tại và bạn có thể sử dụng nó dễ dàng. Khi lớp Developer của chúng ta là một đối tượng, nó cũng có một metaclass của chính nó. Ví dụ, hãy tạo một đối tượng của một lớp String và sử dụng metaclass của nó:
example = "I'm a string object"
def example.something
self.upcase
end
p example.something
# I'M A STRING OBJECT
Ở đây, chúng ta đã thêm một singleton method “something” đến một đối tượng. Sự khác nhau giữa class methods và singleton methods là class method được avaiable cho tất cả thể hiện của đối tượng lớp trong khi singleton method chỉ avaiable cho một thể hiện đơn lẻ. Class methods có phạm vị sử dụng rộng hơn singleton method, nhưng cả hai đều add thêm vào metaclass của đối tượng đó một method. Ví dụ trên có thể được viết lại như sau:
example = "I'm a string object"
class << example
def example.something
self.upcase
end
end
Cú pháp khác nhau nhưng ý nghĩa là tương tự. . Bây giờ chúng ta trở lại ví dụ ban đầu khi chúng ta tạo lớp Developer và tìm hiểu một số cú pháp khác được định nghĩa một class method:
class Developer
def self.backend
"I am backend developer"
end
end
Đây là định nghĩa cơ bản, được hầu hết mọi người sử dụng:
def Developer.backend
"I am backend developer"
end
Đoạn trên cũng có mục đích tương tự, chúng ta định nghĩa một class method backend cho Developer. Chúng ta đã không dùng self nhưng đã định nghĩa một method có ý nghĩa như một class method.
class Developer
class << self
def backend
"I am backend developer"
end
end
end
Chúng ta đang định nghĩa một class method., nhưng đang sử dụng cú pháp tương tự như khi định nghĩa một singleton method cho một đối tượng String.
class << Developer
def backend
"I am backend developer"
end
end
Bằng việc định nghĩa một block giống như trên, chúng ta đã thiết lập self đến metaclass của Developer. Kết quả là, method backend được add đến metaclass của Developer.
Hãy xem metaclass này thực thiên trong cây kế thừa như nào:
Như bạn đã thấy trong các ví dụ trước đó, không có bằng chứng chứng tỏ metaclass tồn tại nhưng chúng ta vẫn có thể sử dụng chúng để thấy được 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 nào đó thì với “self” bên trong method đó sẽ refer đến đối tượng của lớp này. Chúng ta có thể sử dụng cú pháp:class << self để thay đổi self đến một thể hiện của metaclass của lớp này.
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à ta sẽ thấy frontend là instance method của class Developer con backend là instance method của metaclass của Developer
p developer.class.instance_methods false
# [:frontend]
p developer.class.metaclass_example.instance_methods false
# [:backend]
Để get vào metaclass bạn không cần mở lại đối tượng mà chỉ cần sử dụng singleton_class mà Ruby cung cấp:
p developer.class.singleton_class.instance_methods false
# [:backend]
IV, Defining Methods Using “class_eval” and “instance_eval”
Một trong những cách để tạo class method 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"
Ta có thể thấy, trong trường hợp sử dụng instance_eval thì self ở trong luôn trỏ đến đối tượng Developer.
Một cách khác là sử dụng class_eval:
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>"
Với class_eval thì self ở trong một instance method sẽ trỏ đến đối tượng của lớp Developer.
V, Defining Missing Methods on the Fly
Một trong những phần của Metaprogramming là method_missing. Khi bạn gọi một method trên một đối tượng., đầu tiên Ruby sẽ vào trong lớp và duyệt các instance methods của nó. Nếu không tìm thấy method tương ứng, nó sẽ tiếp tục tìm trên các lớp cha. Nếu vẫn không tìm thấy, nó gọi đến một method khác có tên là method_missing là một instance method của Kernel.
define_method là một method được định nghĩa trong lớp Module được sử dụng để tạo các method động. Để sử dụng define_method, bạn gọi nó với tên của method mới và một block với tham số của block chính là tham số của method mới:
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!"
Đây là ví dụ sẽ thấy sự hữu ích của define_method:
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 này không DRY nhưng khi sử dụng define_method thì nó có thể 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, nếu chúng ta muốn add thêm một method mới coding_debug, chúng ta cần đặt “debug” vào trong mảng. Nhưng sử dụng method_missing thì chúng ta có thể fix nó như sau:
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ã trên sẽ định nghĩa các method có tên dạng coding_*** trong method_missing bằng define_method.
VI, Kết luận
Trên đây là những kiến thức cơ bản khi bạn mới làm quen với metaprogramming trong Ruby.Việc sử dụng metaprogramming sẽ giúp code của chúng ta trở nên thông thoáng và đẹp hơn. Thanks for read!
All rights reserved