根據執行期資訊(資料庫欄位、設定檔、使用者輸入)建立方法,而非硬編碼。經典場景:ActiveRecord find_by_*、動態存取器、DSL 關鍵字。
class Legislator
# 假設真實查詢方法
def self.find(conditions = {})
puts "查詢: #{conditions.inspect}"
# 回傳模擬結果
OpenStruct.new(conditions)
end
# 1. 處理動態呼叫
def self.method_missing(method_sym, *arguments, &block)
# 轉字串比對模式
if method_sym.to_s =~ /^find_by_(.+)$/
attribute = $1.to_sym
# 呼叫真實查詢
find(attribute => arguments.first)
else
super
end
end
# 2. 讓 respond_to? 正常運作
def self.respond_to?(method_sym, include_private = false)
if method_sym.to_s =~ /^find_by_(.+)$/
true
else
super
end
end
end
# 測試
Legislator.find_by_first_name('John')
# 輸出: 查詢: {:first_name=>"John"}
Legislator.respond_to?(:find_by_first_name) # => true
Legislator.respond_to?(:undefined_method) # => false
method_missing 和 respond_to? 都有 /^find_by_(.+)$/,違反 DRY 原則。
class LegislatorDynamicFinderMatch
attr_reader :attribute
def initialize(method_sym)
@method_sym = method_sym
@attribute = nil
parse
end
def parse
if @method_sym.to_s =~ /^find_by_(.+)$/
@attribute = $1.to_sym
end
end
def match?
!@attribute.nil?
end
end
class Legislator
def self.method_missing(method_sym, *arguments, &block)
match = LegislatorDynamicFinderMatch.new(method_sym)
if match.match?
find(match.attribute => arguments.first)
else
super
end
end
def self.respond_to?(method_sym, include_private = false)
if LegislatorDynamicFinderMatch.new(method_sym).match?
true
else
super
end
end
end
class Legislator
def self.method_missing(method_sym, *arguments, &block)
match = LegislatorDynamicFinderMatch.new(method_sym)
if match.match?
# 關鍵:定義單例方法快取
define_dynamic_finder(method_sym, match.attribute)
# 立即呼叫新定義的方法
send(method_sym, *arguments, &block)
else
super
end
end
protected
def self.define_dynamic_finder(finder_name, attribute)
# 使用 class_eval 定義單例方法
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.#{finder_name}(value)
find(#{attribute.inspect} => value)
end
RUBY
end
end
# 第一次呼叫:走 method_missing,定義方法
Legislator.find_by_first_name('John')
# 第二次呼叫:直接命中定義的方法,不經 method_missing
Legislator.find_by_first_name('Jane')
# legislator.rb
require 'ostruct'
class LegislatorDynamicFinderMatch
attr_reader :attribute
def initialize(method_sym)
@attribute = $1.to_sym if method_sym.to_s =~ /^find_by_(.+)$/
end
def match?
!@attribute.nil?
end
end
class Legislator
def self.find(conditions = {})
OpenStruct.new(conditions)
end
def self.method_missing(method_sym, *arguments, &block)
match = LegislatorDynamicFinderMatch.new(method_sym)
if match.match?
define_dynamic_finder(method_sym, match.attribute)
send(method_sym, *arguments, &block)
else
super
end
end
def self.respond_to?(method_sym, include_private = false)
LegislatorDynamicFinderMatch.new(method_sym).match? || super
end
protected
def self.define_dynamic_finder(finder_name, attribute)
singleton_class.class_eval do
define_method(finder_name) do |value|
find(attribute => value)
end
end
end
end
# spec/legislator_spec.rb
require_relative 'legislator'
RSpec.describe LegislatorDynamicFinderMatch do
describe '#match?' do
it 'matches find_by_first_name' do
match = LegislatorDynamicFinderMatch.new(:find_by_first_name)
expect(match.attribute).to eq(:first_name)
expect(match).to be_match
end
it 'does not match invalid' do
match = LegislatorDynamicFinderMatch.new(:invalid_method)
expect(match.attribute).to be_nil
expect(match).not_to be_match
end
end
end
RSpec.describe Legislator do
describe 'dynamic find_by_first_name' do
it 'calls find with correct conditions' do
expect(Legislator).to receive(:find).with(first_name: 'John')
Legislator.find_by_first_name('John')
end
it 'responds to dynamic finder' do
expect(Legislator).to respond_to(:find_by_first_name)
expect(Legislator).not_to respond_to(:undefined_method)
end
it 'caches method after first call' do
# 第一次呼叫定義方法
Legislator.find_by_first_name('John')
# 驗證方法已定義在 singleton_class
expect(Legislator.singleton_class.instance_methods(false))
.to include(:find_by_first_name)
end
end
end
class ActiveRecordDynamicMatch
attr_reader :finder_type, :attributes
def initialize(method_name)
@method_name = method_name.to_s
parse
end
def parse
# find_by_name_and_email
# find_or_create_by_name
# find_or_initialize_by_email_and_age
if @method_name =~ /^(find|find_or_create|find_or_initialize)_by_(.+)$/
@finder_type = $1.to_sym
@attributes = $2.split('_and_').map(&:to_sym)
end
end
def match?
!@attributes.nil?
end
end
class ModelBase
def self.method_missing(name, *args, &block)
match = ActiveRecordDynamicMatch.new(name)
if match.match?
define_dynamic_finder(name, match)
send(name, *args, &block)
else
super
end
end
def self.respond_to?(name, include_private = false)
ActiveRecordDynamicMatch.new(name).match? || super
end
protected
def self.define_dynamic_finder(name, match)
singleton_class.class_eval do
define_method(name) do |*values|
conditions = match.attributes.zip(values).to_h
case match.finder_type
when :find
find_by(conditions)
when :find_or_create
find_or_create_by(conditions)
when :find_or_initialize
find_or_initialize_by(conditions)
end
end
end
end
# 佔位方法
def self.find_by(conditions); puts "find_by #{conditions}"; end
def self.find_or_create_by(conditions); puts "find_or_create_by #{conditions}"; end
def self.find_or_initialize_by(conditions); puts "find_or_initialize_by #{conditions}"; end
end
# 測試
class User < ModelBase; end
User.find_by_name_and_email('John', 'john@example.com')
User.find_or_create_by_email('jane@example.com')
User.find_or_initialize_by_name_and_age('Bob', 30)
class OptimizedDynamic
# 類別變數快取已定義方法
@@defined_finders = Set.new
def self.method_missing(name, *args, &block)
match = DynamicMatch.new(name)
if match.match? && !@@defined_finders.include?(name)
define_finder(name, match)
@@defined_finders << name
end
super # 此時方法已定義,super 會呼叫真實方法
end
end
class StaticDynamic
# 啟動時一次性定義所有可能方法
KNOWN_ATTRIBUTES = [:name, :email, :age, :address, :phone]
KNOWN_ATTRIBUTES.each do |attr|
define_singleton_method("find_by_#{attr}") do |value|
find(attr => value)
end
end
# 組合查詢:預先定義常見組合
[[:name, :email], [:name, :age]].each do |attrs|
method_name = "find_by_#{attrs.join('_and_')}"
define_singleton_method(method_name) do |*values|
find(attrs.zip(values).to_h)
end
end
end
| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 屬性固定、組合有限 | 啟動時 define_method | 效能最好、IDE 支援、可見性高 |
| 屬性動態(如資料庫欄位) | method_missing + 快取 | 無法預知所有組合 |
| DSL 關鍵字 | method_missing | 語法靈活、執行期決定 |
| 委派現有物件 | define_method 複製 | 避免 method_missing 開銷 |
respond_to_missing? → respond_to? 失效super → 繼承鏈中斷method_missing 內呼叫不存在方法 → 無限遞迴method_missing 效能差class_eval 字串插值注入風險 → 用 define_method 區塊形式Ruby 動態方法建立核心模式:method_missing 攔截 → 匹配模式 → define_method 定義真實方法快取 → send 執行 → respond_to_missing? 協同。ActiveRecord 的 DynamicFinderMatch 是教科書級架構:提取匹配邏輯、分離關注點、支援多種查詢類型。現代開發原則:能靜態定義就靜態定義,必須動態時務必快取。理解 singleton_class、class_eval、define_method 三者關係,能精準控制方法定義位置(實例方法 vs 類別方法)。