Metaprogramming trong Ruby: sức mạnh, method lookup và giới hạn

Metaprogramming là code tạo hoặc thay đổi behavior của code. Ruby làm việc này tự nhiên vì class/module là object và method table có thể sửa runtime.

Dynamic dispatch

1
2
attribute = :name
user.public_send(attribute)

Ưu tiên public_send khi input không được phép gọi private method. Allowlist method name từ user; dynamic dispatch không biến authorization thành tùy chọn.

Định nghĩa method

1
2
3
4
5
6
7
class Serializer
%i[name email].each do |attribute|
define_method("serialize_#{attribute}") do |record|
record.public_send(attribute).to_s
end
end
end

define_method giữ lexical closure, khác def mở scope mới.

class_evalinstance_eval

1
2
3
4
5
6
7
8
9
10
11
User.class_eval do
def display_name
name
end
end

User.instance_eval do
def lookup(id)
find(id)
end
end
  • class_eval định nghĩa instance behavior trên class/module.
  • instance_eval đổi self sang object; method định nghĩa ở đây thành singleton method của object đó.

Dùng khi xây DSL/framework boundary, không dùng để làm code business “ngầu”.

method_missing

1
2
3
4
5
6
7
8
def method_missing(name, *args)
return super unless dynamic_attribute?(name)
read_dynamic_attribute(name)
end

def respond_to_missing?(name, include_private = false)
dynamic_attribute?(name) || super
end

Nếu override method_missing, phải đồng bộ respond_to_missing?. Nếu tập method biết trước, define_method dễ debug hơn.

Scope gates

class, module, def mở lexical scope mới. Block giữ outer locals:

1
2
3
4
5
prefix = "audit"

Service.class_eval do
define_method(:tag) { prefix }
end

Rủi ro

  • stack trace khó đọc;
  • IDE/static analysis kém;
  • public API sinh runtime khó discover;
  • monkey patch xung đột version;
  • eval với input tạo code execution.

Rule thực dụng: nếu loop/collection/ordinary object giải quyết được, dùng chúng trước. Metaprogramming tốt làm API đơn giản hơn cho nhiều caller; metaprogramming tệ chuyển complexity vào bóng tối rồi gọi đó là magic.

Reference