Ruby Metaprogramming 1
Bài đăng này đã không được cập nhật trong 6 năm
Trong bài viết này, chúng ta sẽ nhìn vào một vài khía cạnh khác nhau của metaprograming trong Ruby. Để bắt đầu, metaprograming là gì?
Metaprogramming is the writing of computer programs with the ability to treat programs as their data. It means that a program could be designed to read, generate, analyze, or transform other programs and even modify itself while running.
Chúng ta sẽ tập trung vào việc làm thế nào ta có thể đọc và phân tích code trong Ruby, làm thế nào ta có thể gọi các phương thức(methods), hoặc gửi tin một cách tự động, và làm thế nào ta có thể tạo ra những methods mới trong khi chương trình đang được thực thi.
Asking Our Code Questions
Một trong những khía cạnh vượt trội của Ruby là có thể tự hỏi code của ta những câu hỏi về chính nó trong quá trình thực thi. Điều này cũng được biết đến như một sự tự xét (introspection). Cũng giống như chúng ta tự hỏi chính mình những câu hỏi như "Tại sao tôi ở đây?", code của ta cũng tương tự thế, mặc dù những câu hỏi đó có thể không tồn tại.
Am I able to respond to this method call?
Ta có thể hỏi bất cứ object nào liệu rằng nó có khả năng phản hồi một method call nào đó hay không bằng cách sử dụng respond_to?
method.
"Roberto Alomar".respond_to? :downcase
# => true
"Roberto Alomar".respond_to? :floor
# => false
What does my object ancestry chain look like?
Nếu bạn kiểm tra một ActiveRecord
model trong Rails 5, bạn sẽ thấy nó có đến tận 71 ông bà tổ tiên, bất ngờ chưa?
School.ancestors.size
# => 71
String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]
What instance variables and methods have been defined?
Chúng ta có thể sử dụng methods
để trả về tất cả những methods khả dụng của một object cụ thể và instance_variables
trả về một list những biến instance được định nghĩa cho object đó.
require 'date'
class Alpaca
attr_accessor :name, :birthdate
def initialize(name, birthdate)
@name = name
@birthdate = birthdate
end
def spit
"Putsuuey"
end
end
spitty = Alpaca.new('Spitty', Date.new(1990, 10, 10))
spitty.methods
# => [:name, :name=, :birthdate, :spit, :birthdate=, :instance_of?, :public_send, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :private_methods, :kind_of?, :instance_variables, :tap, :is_a?, :extend, :define_singleton_method, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :display, :send, :object_id, :to_s, :method, :public_method, :singleton_method, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods, :protected_methods, :frozen?, :public_methods, :singleton_methods, :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
spitty.instance_variables
# => [:@name, :@birthdate]
Sending Messages
Ruby là một ngôn ngữ động. Nó bao gồm một loạt các object có thể truyền các gói tin qua lại với nhau, tương tự như khi chúng ta nói "call a method". Hãy xem thử downcase
method của String
objects.
"Roberto Alomar".downcase
# => "roberto alomar"
Khi ta gọi method này sử dụng dấu chấm .
, điều ta đang thật sự nói là ta truyền 1 thông điệp đến String
, và nó quyết định cách trả lời thông điệp đó. Trong trường hợp này, nó trả lời với một phiên bản viết thường của chính nó.
Hãy tìm hiểu sâu hơn nữa, hãy chia hành động này thành 3 phần: Đầu tiên, "Roberto Alomar" là object, đối tượng sẽ nhận thông điệp. Dấu chấm (.
) nói cho đối tượng nhận thông điệp biết chúng ta đang gửi một lệnh hoặc một thông điệp nào đó. Và phần theo sau dấu chấm, downcase
là thông điệp mà chúng ta muốn gởi. Trong tiếng Anh, chúng ta có thể nói rằng "we are sending the downcase message to "Roberto Alomar". It figures out what to do or send back once it receives that message."
Với Ruby thì có thể dùng cách khác để thực hiện, bằng send
method:
"Roberto Alomar".send(:downcase)
# => "roberto alomar"
Thông thường chúng ta không sử dụng hình thức này, nhưng Ruby cho phép chúng ta gửi thông điệp (hoặc gọi một phương thức nào đó ) trong trường hợp này, cho phép lựa chọn việc gửi một thông điệp động (dynamic message) hoặc gọi phương thức một cách tự động.
method = :downcase
"Roberto Alomar".send(method)
# => "roberto alomar"
Trông có vẻ không nhiều nhặn gì, nhưng đây là một trong những cấu trúc mà Ruby cho phép chúng ta viết code động, code thậm chí không tồn tại khi bạn viết chúng. Trong phần tiếp theo, chúng ta sẽ xem xem làm cách nào để tạo ra code mới một cách tự động trong Ruby bằng cách sử dụng define_method
.
Generating New Methods
Một khía cạnh khác của metaprograming là Ruby cho chúng ta khả nặng tạo ra code mới trong lúc thực thi. Ta sẽ làm điều này bằng cách sử dụng một phương thức từ Module
class được gọi là define_method
. Cách nó hoạt động là, nó truyền một biểu tượng mà sẽ trở thành tên của phương thức mới, và tất cả được bọc trong một khối block như này:
class Person
define_method :greeting, -> { puts 'Hello!' }
end
Person.new.greeting
# => Hello!
Có thể bạn đã dùng delegate
method trước đó rồi, đi kèm ActiveSupport
với Rails và extends Module
. Điều này cho phép ta nói rằng, khi ta gọi một phương thức cụ thể, ta gọi method đó trên một object khác object hiện tại (self). Bạn có thể tham khảo thêm source code ở đây.
Hãy cùng tham khảo ví dụ bên dưới. Đầu tiên ta thêm 1 method mới vào Module
class, gọi là delegar
.
class Module
def delegar(method, to:)
define_method(method) do |*args, &block|
send(to).send(method, *args, &block)
end
end
end
Khi method này được gọi, nó sẽ định nghĩa 1 method mới với trách nhiệm làm việc với object khác, giống như một proxy.
class Receptionist
def phone(name)
puts "Hello #{name}, I've answered your call."
end
end
class Company
attr_reader :receptionist
delegar :phone, to: :receptionist
def initialize
@receptionist = Receptionist.new
end
end
company = Company.new
company.phone 'Leigh'
# => "Hello Leigh, I've answered your call."
Bạn có thể thấy ta đang gọi phone
method trên Company
, nhưng thật sự thì Receptionist
mới là kẻ trả lời.
Dollars and Cents
Có thể bạn từng nghe rằng ko nên lưu trữ và sử dụng tiền dưới dạng Float
bởi vấn đề này. Một trong những cách giải quyết là lưu dưới dạng cents. $10.25 sẽ được lưu trong db là 1025 cents.
Người dùng rõ ràng không muốn nhập vào đơn vị cents, nên chúng ta sẽ phải thêm ít code để chuyển đổi qua lại giữa 2 đơn vị dollars và cents. Ta sẽ dùng một chút metaprograming để giúp mọi thứ trông đơn giản hơn.
Hãy nhìn vào class Purchase
có một trường trong db là price_cents
. Trông như thế này:
class Purchase
attr_accessor :price_cents
extend MoneyFields
money_fields :price
end
Nếu là một ActiveRecord
object trong Rails, chúng ta sẽ không cần thêm dòng attr_accessor :price_cents
vì nó sẽ tự làm việc đó cho chúng ta, nhưng đây là ví dụ cho một Ruby phiên bản cũ hơn. Code này sẽ cho phép ta tương tác với trường đó, như sau:
purchase = Purchase.new
purchase.price = 10.25
purchase.price_cents
# => 1025
purchase.price_cents = 555
purchase.price
# => #<BigDecimal:7fbc7497ac88,'0.555E1',18(36)>
Nhưng method price
và price=
ở đâu ra vậy? Method money_fields
tạo ra 2 methods mới tương tác với price_cents
và price_cents=
.
module MoneyFields
require 'bigdecimal'
def money_fields(*fields)
fields.each do |field|
define_method field do
value_cents = send("#{field}_cents")
value_cents.nil? ? nil : BigDecimal.new(value_cents / BigDecimal.new("100"))
end
define_method "#{field}=" do |value|
value_cents = value.nil? ? nil : Integer(BigDecimal.new(String(value)) * 100)
send("#{field}_cents=", value_cents)
end
end
end
end
Để xem thử nó hoạt động có như mong đợi ko, đây là đoạn test kiểm tra các chuyển đổi qua lại khác nhau:
require 'minitest/autorun'
class PurchaseTest < MiniTest::Test
attr_reader :purchase
def setup
@purchase = Purchase.new
end
def test_reading_writing_dollars
purchase.price = 5.00
assert_equal purchase.price, 5.00
end
def test_converting_to_dollars
purchase.price_cents = 500
assert_equal purchase.price, 5.00
end
def test_converting_to_cents
purchase.price = 5.00
assert_equal purchase.price_cents, 500
end
def test_writing_dollars_from_string
purchase.price = "5.00"
assert_equal purchase.price_cents, 500
end
def test_nils
purchase.price = nil
assert_equal purchase.price, nil
end
def test_creating_methods
assert_equal Purchase.instance_methods(false).sort, [:price_cents, :price_cents=, :price, :price=].sort
end
def test_respond_to_dollars
assert_equal purchase.respond_to?(:price), true
assert_equal purchase.respond_to?(:price=), true
end
end
Conclusion
Metaprograming thật tuyệt vời nhưng chỉ khi nó được sử dụng ít. Nó có thể giúp bạn viết các đoạn code lặp lại dễ dàng hơn nhiều( như field money bên trên chẳng hạn), nó có thể giúp bạn debug và phân tích code dễ dàng, nhưng đôi khi nó cũng làm code của bạn khó đọc và khó hiểu hơn. Chỉ nên sử dụng metaprograming chỉ khi nó mang lại một lợi thế rõ ràng.
Nguồn: blog.codeship.com
All rights reserved