首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 更多程式設計知識

Ruby 動態方法建立完整指南:method_missing、respond_to?、define_method、DynamicFinderMatch 模式與快取最佳實務

     編輯:更多程式設計知識

動態方法核心需求

根據執行期資訊(資料庫欄位、設定檔、使用者輸入)建立方法,而非硬編碼。經典場景:ActiveRecord find_by_*、動態存取器、DSL 關鍵字。

基礎實作:method_missing + respond_to?


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 原則。

解決方案:提取 DynamicFinderMatch 類別


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

進階:快取動態方法(定義真實方法避免再次走 method_missing)


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')

完整可測試實作(含 RSpec)


# 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

ActiveRecord 風格:多屬性動態查詢


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_eval


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

替代方案:方法委派 + define_method(無 method_missing)


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

選擇指南:method_missing vs define_method

場景推薦方案理由
屬性固定、組合有限啟動時 define_method效能最好、IDE 支援、可見性高
屬性動態(如資料庫欄位)method_missing + 快取無法預知所有組合
DSL 關鍵字method_missing語法靈活、執行期決定
委派現有物件define_method 複製避免 method_missing 開銷

常見錯誤清單

  1. 忘記 respond_to_missing? → respond_to? 失效
  2. 忘記 super → 繼承鏈中斷
  3. method_missing 內呼叫不存在方法 → 無限遞迴
  4. 未快取 → 每次呼叫都走 method_missing 效能差
  5. class_eval 字串插值注入風險 → 用 define_method 區塊形式

總結

Ruby 動態方法建立核心模式:method_missing 攔截 → 匹配模式 → define_method 定義真實方法快取 → send 執行 → respond_to_missing? 協同。ActiveRecord 的 DynamicFinderMatch 是教科書級架構:提取匹配邏輯、分離關注點、支援多種查詢類型。現代開發原則:能靜態定義就靜態定義,必須動態時務必快取。理解 singleton_class、class_eval、define_method 三者關係,能精準控制方法定義位置(實例方法 vs 類別方法)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved