Ruby 的「鉤子方法」是元編程的核心利器,讓程式碼能在類別/模組定義、繼承、混入、方法呼叫等關鍵時機自動執行邏輯。Rails、Devise、ActiveRecord、Rake 等頂級框架大量仰賴鉤子實現聲明式 API、自動註冊、領域特定語言 (DSL)。本文系統性解析五大核心鉤子:included、extended、prepended、inherited、method_missing,含語意、參數、執行時機、框架原始碼實戰案例與最佳實務。
| 鉤子 | 觸發時機 | 接收參數 | 常見用途 | 框架實例 |
|---|---|---|---|---|
included | include Module 時 | base (include 者,Class/Module) | 混入實例方法、自動擴展類別方法、註冊驗證/回調 | Devise Models::Validatable, ActiveSupport Concern |
extended | extend Module 時 | base (extend 者,Class/Module/Object) | 混入類別方法/單例方法、註冊類別層級行為 | ActiveModel Callbacks, ActiveRecord Scoping |
prepended | prepend Module 時 (Ruby 2.0+) | base (prepend 者) | 方法鏈前置、覆寫宿主方法、AOP 切面 | Rails ActiveSupport::Concern (內部), 熱重載 gem |
inherited | class Child < Parent 時 | child_class (子類別) | 單例模式強制、自動註冊子類別、DSL 基類 | Rails Application, ActiveRecord Base |
method_missing | 呼叫不存在方法時 | symbol (方法名), *args, &block | 動態代理、委派、DSL 語法糖、幽靈方法 | Rake TaskArguments, ActiveRecord AttributeMethods, Builder |
module Person
def self.included(base)
puts "#{base} included #{self}"
# base 是 include 此模組的類別 (Class 物件)
# 此處可對 base 進行元編程操作
end
def name
"My name is Person"
end
end
class User
include Person
end
User.new.name # 輸出: "User included Person" 然後 "My name is Person"
Devise 實戰:devise :validatable 觸發 include Devise::Models::Validatable,其 included 鉤子執行 base.class_eval { validates_presence_of :email ... },將驗證宏「注入」使用者模型,極其優雅。
module Person
def self.extended(base)
puts "#{base} extended #{self}"
end
def name
"My name is Person"
end
end
class User
extend Person # 類別方法
end
User.name # "My name is Person"
# 物件單例方法
u = User.new
u.extend Person # 僅 u 擁有 name 方法
u.name # "My name is Person"
User.new.name # NoMethodError
ActiveModel::Callbacks 實戰:extend ActiveModel::Callbacks 觸發 extended,內部執行 base.class_eval { include ActiveSupport::Callbacks },為模型類別註冊 Rails 回調機制 (before_save 等)。
module Person
def self.prepended(base)
puts "#{self} prepended to #{base}"
end
def name
"My name belongs to Person"
end
end
class User
prepend Person
def name
"My name belongs to User"
end
end
User.new.name # "Person prepended to User" 然後 "My name belongs to Person"
# Person 的 name 覆寫了 User 的 name
# 若 Person.name 呼叫 super,則會執行 User.name
關鍵差異:include 將模組放在方法鏈 宿主之後 (宿主可覆寫);prepend 放在 宿主之前 (模組可覆寫宿主,並可 super 呼叫宿主)。
class Person
def self.inherited(child)
puts "#{child} inherits #{self}"
# 可在此註冊子類別、強制單例、檢查條件
end
def name
"My name is Person"
end
end
class User < Person
end
# 輸出: "User inherits Person"
Rails Application 實戰:Rails::Application 的 inherited 檢查 Rails.application 是否已存在,若是則拋出異常 "You cannot have more than one Rails::Application",強制單一應用實例。
class Person
def method_missing(sym, *args)
"#{sym} not defined on #{self}"
end
def name
"My name is Person"
end
end
p = Person.new
p.name # "My name is Person"
p.address # "address not defined on #"
# 不拋出 NoMethodError,優雅處理
Rake 實戰:Rake::TaskArguments 定義 method_missing 呼叫 lookup(sym.to_sym),從內部 Hash 取得任務參數,實現 args.name、args[:name] 等動態存取語法。
require 'active_support/concern'
module MyConcern
extend ActiveSupport::Concern
included do
# 相當於 self.included(base)
validates :name, presence: true
scope :active, -> { where(active: true) }
end
class_methods do
# 相當於 self.extended(base) 但針對類別方法
def top(n)
limit(n).order(created_at: :desc)
end
end
# prepended/inherited 需手動定義
def self.prepended(base)
# ...
end
end
class User < ApplicationRecord
include MyConcern
end
class Proxy
def initialize(target)
@target = target
end
def method_missing(sym, *args, &block)
if @target.respond_to?(sym)
@target.public_send(sym, *args, &block)
else
super # 交給原本的 method_missing (拋出 NoMethodError)
end
end
# 關鍵:讓 respond_to? 正確回傳 true
def respond_to_missing?(sym, include_private = false)
@target.respond_to?(sym) || super
end
end
proxy = Proxy.new("hello")
proxy.upcase # "HELLO" (委派給 String#upcase)
proxy.respond_to?(:upcase) # true
| 坑點 | 後果 | 修正 |
|---|---|---|
| included/extended 中未呼叫 super | 繼承鏈上層鉤子不執行 (如 ActiveSupport::Concern) | 習慣在鉤子開頭 super 或使用 Concern DSL |
| prepend 導致 super 行為改變 | 宿主方法變成 super 目標,邏輯易搞混 | 明確文檔標註 prepend 用途,測試 super 呼叫鏈 |
| inherited 中修改子類別定義 | 子類別尚未完全定義,操作可能失效 | 僅做註冊/檢查,複雜操作延後到 Class.new 後或用 TracePoint |
| method_missing 未定義 respond_to_missing? | respond_to?(:ghost_method) 回傳 false,破壞 Ruby 內省 | 必須實作 respond_to_missing? 回傳 true |
| method_missing 捕獲所有錯誤 | 隱藏真實拼寫錯誤、除錯困難 | 限定處理特定模式 (如 /^find_by_/),其他 super |
| 濫用鉤子導致隱性耦合 | 類別行為難追蹤、測試困難 | 優先用明確的 include/extend、組合優於繼承、文檔化鉤子副作用 |
included 注入驗證宏、extended 註冊類別配置。inherited 註冊模型、included (透過 Concern) 定義關聯/驗證/範圍、method_missing 動態屬性/查詢方法 (find_by_email)。extended 注入回調機制。method_missing 任務參數動態存取。inherited 強制單例。included/class_methods DSL。ActiveSupport::Concern 封裝 included/class_methods,語意清晰、自動處理 super。super 時。respond_to_missing?:維護 Ruby 內省一致性。include MyModule 會自動做什麼。include/extend/prepend/inherit 觸發的副作用。Ruby 鉤子方法是「約定優於配置」的引擎心臟。深入理解其觸發時機、參數傳遞、方法鏈位置與框架級應用模式,能讓你從「使用框架」進化為「設計框架」,打造出極具表達力、可擴展、符合 Ruby 哲學的優雅 API。