Ruby Metaprogramming 1
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, ...
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. $$0.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