💎
【Ruby 52日目】オブジェクト指向 - respond_to_missing?
はじめに
Rubyのrespond_to_missing?について、Ruby 3.4の仕様に基づいて詳しく解説します。
この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。
基本概念
respond_to_missing?は、method_missingと組み合わせて使う重要なメソッドです:
- respond_to?との整合性 - method_missingで対応するメソッドをrespond_to?でも認識させる
- メソッドの存在確認 - 動的メソッドが利用可能かどうかを正しく判定できる
- リフレクションのサポート - methodメソッドなどのリフレクション機能との互換性
- 必須の実装 - method_missingを使う場合は必ずrespond_to_missing?も実装すべき
respond_to_missing?を正しく実装することで、動的メソッドが透過的に機能します。
基本的な使い方
問題: respond_to_missing?なしの場合
class WithoutRespondToMissing
def method_missing(method_name, *args)
if method_name.to_s.start_with?("dynamic_")
"Dynamic method: #{method_name}"
else
super
end
end
end
obj = WithoutRespondToMissing.new
# メソッドは呼べる
puts obj.dynamic_hello #=> Dynamic method: dynamic_hello
# しかしrespond_to?は正しく動作しない
puts obj.respond_to?(:dynamic_hello) #=> false(不整合!)
解決: respond_to_missing?を実装
class WithRespondToMissing
def method_missing(method_name, *args)
if method_name.to_s.start_with?("dynamic_")
"Dynamic method: #{method_name}"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("dynamic_") || super
end
end
obj = WithRespondToMissing.new
# メソッドは呼べる
puts obj.dynamic_hello #=> Dynamic method: dynamic_hello
# respond_to?も正しく動作
puts obj.respond_to?(:dynamic_hello) #=> true(正しい!)
puts obj.respond_to?(:other_method) #=> false
基本的なパターン
class DynamicAccessor
def initialize
@data = {}
end
def method_missing(method_name, *args)
method_str = method_name.to_s
if method_str.end_with?("=")
# セッター
key = method_str.chomp("=").to_sym
@data[key] = args.first
elsif @data.key?(method_name)
# ゲッター
@data[method_name]
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
# セッターまたは既存のキー
method_str.end_with?("=") || @data.key?(method_name) || super
end
end
obj = DynamicAccessor.new
obj.name = "Alice"
obj.age = 25
puts obj.respond_to?(:name) #=> true
puts obj.respond_to?(:age) #=> true
puts obj.respond_to?(:email) #=> false
include_privateパラメータ
class PrivateChecker
def method_missing(method_name, *args)
if method_name.to_s.start_with?("public_")
"Public dynamic method"
elsif method_name.to_s.start_with?("private_")
"Private dynamic method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
if method_str.start_with?("public_")
true
elsif method_str.start_with?("private_")
include_private # include_privateがtrueの時のみ認識
else
super
end
end
end
obj = PrivateChecker.new
puts obj.respond_to?(:public_method) #=> true
puts obj.respond_to?(:private_method) #=> false
puts obj.respond_to?(:private_method, true) #=> true
superの重要性
class Base
def base_method
"Base method"
end
end
class Derived < Base
def method_missing(method_name, *args)
if method_name.to_s.start_with?("custom_")
"Custom method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("custom_") || super
end
end
obj = Derived.new
# 基底クラスのメソッドも認識
puts obj.respond_to?(:base_method) #=> true
puts obj.respond_to?(:custom_test) #=> true
puts obj.respond_to?(:unknown) #=> false
methodメソッドとの連携
class MethodSupport
def method_missing(method_name, *args, &block)
if method_name.to_s.start_with?("calc_")
operation = method_name.to_s.sub("calc_", "")
->(a, b) { a.send(operation, b) }
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("calc_") || super
end
end
obj = MethodSupport.new
# methodメソッドが正しく動作
add_method = obj.method(:calc_+)
puts add_method.call(10, 5) #=> 15
multiply_method = obj.method(:calc_*)
puts multiply_method.call(4, 3) #=> 12
よくあるユースケース
ケース1: Active Record風のfind_by_*メソッド
動的なfinder メソッドを実装します。
class User
attr_reader :id, :name, :email, :age
@@users = []
def initialize(id:, name:, email:, age:)
@id = id
@name = name
@email = email
@age = age
@@users << self
end
def self.method_missing(method_name, *args)
if method_name.to_s =~ /^find_by_(.+)$/
attribute = $1.to_sym
value = args.first
@@users.find { |user| user.send(attribute) == value }
elsif method_name.to_s =~ /^find_all_by_(.+)$/
attribute = $1.to_sym
value = args.first
@@users.select { |user| user.send(attribute) == value }
else
super
end
end
def self.respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
method_str =~ /^find_(all_)?by_.+$/ || super
end
def self.all
@@users
end
end
# データを作成
User.new(id: 1, name: "Alice", email: "alice@example.com", age: 25)
User.new(id: 2, name: "Bob", email: "bob@example.com", age: 30)
User.new(id: 3, name: "Charlie", email: "charlie@example.com", age: 25)
# 動的finderメソッド
alice = User.find_by_name("Alice")
puts alice.email #=> alice@example.com
users_25 = User.find_all_by_age(25)
puts users_25.map(&:name).join(", ") #=> Alice, Charlie
# respond_to?も正しく動作
puts User.respond_to?(:find_by_email) #=> true
puts User.respond_to?(:find_all_by_age) #=> true
puts User.respond_to?(:invalid_method) #=> false
ケース2: 設定オブジェクトの型安全性
型チェック付きの動的設定を実装します。
class TypedConfiguration
SCHEMA = {
host: String,
port: Integer,
ssl: [TrueClass, FalseClass],
timeout: Numeric,
retries: Integer
}
def initialize
@settings = {}
end
def method_missing(method_name, *args)
method_str = method_name.to_s
if method_str.end_with?("=")
key = method_str.chomp("=").to_sym
validate_and_set(key, args.first)
elsif SCHEMA.key?(method_name)
@settings[method_name]
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
key = method_str.end_with?("=") ? method_str.chomp("=").to_sym : method_name
SCHEMA.key?(key) || super
end
private
def validate_and_set(key, value)
unless SCHEMA.key?(key)
raise ArgumentError, "Unknown setting: #{key}"
end
expected_types = Array(SCHEMA[key])
unless expected_types.any? { |type| value.is_a?(type) }
raise TypeError, "#{key} must be #{expected_types.join(' or ')}, got #{value.class}"
end
@settings[key] = value
end
end
config = TypedConfiguration.new
# 正しい型で設定
config.host = "localhost"
config.port = 3000
config.ssl = true
config.timeout = 5.0
puts config.host #=> localhost
puts config.respond_to?(:port) #=> true
# 型エラー
begin
config.port = "invalid"
rescue TypeError => e
puts "Error: #{e.message}"
#=> Error: port must be Integer, got String
end
# 未定義のキー
puts config.respond_to?(:unknown) #=> false
ケース3: プロキシオブジェクトのデバッグ
メソッド呼び出しをログに記録します。
class LoggingProxy
def initialize(target, logger: $stdout)
@target = target
@logger = logger
@call_count = Hash.new(0)
end
def method_missing(method_name, *args, &block)
if @target.respond_to?(method_name)
@call_count[method_name] += 1
log_call(method_name, args)
start_time = Time.now
result = @target.send(method_name, *args, &block)
elapsed = Time.now - start_time
log_result(method_name, result, elapsed)
result
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
@target.respond_to?(method_name, include_private) || super
end
def stats
@call_count
end
private
def log_call(method_name, args)
@logger.puts "[#{Time.now}] Calling #{method_name} with #{args.inspect}"
end
def log_result(method_name, result, elapsed)
@logger.puts "[#{Time.now}] #{method_name} returned (#{elapsed}s): #{result.inspect}"
end
end
# 使用例
array = [1, 2, 3, 4, 5]
proxy = LoggingProxy.new(array)
proxy.map { |x| x * 2 }
#=> [2025-11-28...] Calling map with []
# [2025-11-28...] map returned (...s): [2, 4, 6, 8, 10]
proxy.select { |x| x > 2 }
#=> [2025-11-28...] Calling select with []
# [2025-11-28...] select returned (...s): [3, 4, 5]
puts proxy.stats.inspect
#=> {:map=>1, :select=>1}
# respond_to?も正しく動作
puts proxy.respond_to?(:map) #=> true
puts proxy.respond_to?(:select) #=> true
ケース4: Builderパターンの実装
流暢なインターフェースを持つビルダーを作成します。
class SQLQueryBuilder
VALID_OPERATORS = {
eq: "=",
ne: "!=",
gt: ">",
lt: "<",
gte: ">=",
lte: "<=",
like: "LIKE",
in: "IN"
}
def initialize(table)
@table = table
@conditions = []
@order = nil
@limit = nil
end
def method_missing(method_name, *args)
method_str = method_name.to_s
# where_column_operator パターン
if method_str =~ /^where_(.+)_(#{VALID_OPERATORS.keys.join('|')})$/
column = $1
operator = $2.to_sym
add_where(column, VALID_OPERATORS[operator], args.first)
self
# order_by_column_direction パターン
elsif method_str =~ /^order_by_(.+)_(asc|desc)$/
column = $1
direction = $2.upcase
@order = "ORDER BY #{column} #{direction}"
self
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
method_str =~ /^where_.+_(#{VALID_OPERATORS.keys.join('|')})$/ ||
method_str =~ /^order_by_.+_(asc|desc)$/ ||
super
end
def limit(n)
@limit = "LIMIT #{n}"
self
end
def to_sql
sql = "SELECT * FROM #{@table}"
sql += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
sql += " #{@order}" if @order
sql += " #{@limit}" if @limit
sql
end
private
def add_where(column, operator, value)
formatted_value = value.is_a?(String) ? "'#{value}'" : value
formatted_value = "(#{value.map { |v| v.is_a?(String) ? "'#{v}'" : v }.join(', ')})" if value.is_a?(Array)
@conditions << "#{column} #{operator} #{formatted_value}"
end
end
query = SQLQueryBuilder.new("users")
# メソッドの存在確認
puts query.respond_to?(:where_age_gt) #=> true
puts query.respond_to?(:where_name_eq) #=> true
puts query.respond_to?(:order_by_created_at_desc) #=> true
# クエリ構築
sql = query
.where_age_gte(18)
.where_status_eq("active")
.where_role_in(["admin", "moderator"])
.order_by_created_at_desc
.limit(10)
.to_sql
puts sql
#=> SELECT * FROM users WHERE age >= 18 AND status = 'active' AND role IN ('admin', 'moderator') ORDER BY created_at DESC LIMIT 10
ケース5: APIクライアントのラッパー
外部APIへのメソッド呼び出しを動的に処理します。
class APIClient
VALID_ENDPOINTS = {
users: "/api/users",
posts: "/api/posts",
comments: "/api/comments",
settings: "/api/settings"
}
def initialize(base_url)
@base_url = base_url
@headers = {}
end
def method_missing(method_name, *args)
method_str = method_name.to_s
# get_resource パターン
if method_str =~ /^get_(.+)$/
resource = $1.to_sym
perform_request(:get, resource, args.first)
# create_resource パターン
elsif method_str =~ /^create_(.+)$/
resource = $1.gsub(/_(\w)/) { $1.upcase }.to_sym # user_posts -> userPosts
singular = resource.to_s.sub(/s$/, "").to_sym
perform_request(:post, singular, args.first)
# update_resource パターン
elsif method_str =~ /^update_(.+)$/
resource = $1.gsub(/_(\w)/) { $1.upcase }.to_sym
singular = resource.to_s.sub(/s$/, "").to_sym
id = args[0]
data = args[1]
perform_request(:put, singular, data, id: id)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
resource_pattern = VALID_ENDPOINTS.keys.map(&:to_s).join('|')
method_str =~ /^(get|create|update)_(#{resource_pattern})$/ || super
end
private
def perform_request(method, resource, data = nil, id: nil)
endpoint = VALID_ENDPOINTS[resource]
endpoint += "/#{id}" if id
# 実際のHTTPリクエストの代わりにシミュレーション
{
method: method,
url: "#{@base_url}#{endpoint}",
data: data,
response: "Simulated response"
}
end
end
client = APIClient.new("https://api.example.com")
# respond_to?で確認
puts client.respond_to?(:get_users) #=> true
puts client.respond_to?(:create_posts) #=> true
puts client.respond_to?(:update_comments) #=> true
puts client.respond_to?(:invalid_method) #=> false
# APIリクエスト
result = client.get_users(limit: 10)
puts result.inspect
#=> {:method=>:get, :url=>"https://api.example.com/api/users", :data=>{:limit=>10}, :response=>"Simulated response"}
create_result = client.create_posts(title: "New Post", body: "Content")
puts create_result.inspect
#=> {:method=>:post, :url=>"https://api.example.com/api/posts", :data=>{:title=>"New Post", :body=>"Content"}, :response=>"Simulated response"}
注意点とベストプラクティス
注意点
- method_missingとrespond_to_missing?の一致
# BAD: ロジックが一致していない
class BadExample
def method_missing(method_name, *args)
if method_name.to_s.start_with?("get_")
"Getting"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("set_") # method_missingと異なる!
end
end
# GOOD: ロジックを一致させる
class GoodExample
def method_missing(method_name, *args)
if valid_method?(method_name)
"Getting"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
valid_method?(method_name) || super
end
private
def valid_method?(method_name)
method_name.to_s.start_with?("get_")
end
end
- superを必ず呼ぶ
# BAD: superを呼ばない
class NoSuper
def respond_to_missing?(method_name, include_private = false)
false # 常にfalseを返す
end
end
obj = NoSuper.new
puts obj.respond_to?(:to_s) #=> false(Objectのメソッドも認識しない!)
# GOOD: superを呼ぶ
class WithSuper
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("custom_") || super
end
end
obj = WithSuper.new
puts obj.respond_to?(:to_s) #=> true(正しく認識)
- パフォーマンスへの配慮
# BAD: 毎回正規表現を評価
class SlowCheck
def respond_to_missing?(method_name, include_private = false)
method_name.to_s =~ /^(get|set|find)_[a-z_]+_(by|with|for)_[a-z_]+$/ || super
end
end
# GOOD: シンプルなチェックから始める
class FastCheck
VALID_PREFIXES = %w[get_ set_ find_]
def respond_to_missing?(method_name, include_private = false)
method_str = method_name.to_s
VALID_PREFIXES.any? { |prefix| method_str.start_with?(prefix) } || super
end
end
ベストプラクティス
- 共通ロジックを抽出
# GOOD: 判定ロジックを共有
class WellStructured
def method_missing(method_name, *args)
if supports_dynamic_method?(method_name)
handle_dynamic_method(method_name, *args)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
supports_dynamic_method?(method_name) || super
end
private
def supports_dynamic_method?(method_name)
method_name.to_s.start_with?("dynamic_")
end
def handle_dynamic_method(method_name, *args)
"Handled: #{method_name}"
end
end
- ドキュメント化
# GOOD: 動的メソッドのパターンを明記
class DocumentedDynamic
# Dynamically responds to the following method patterns:
#
# - find_by_<attribute>(value): Finds records by attribute
# - find_all_by_<attribute>(value): Finds all records by attribute
# - where_<column>_<operator>(value): Adds WHERE clause
#
# Examples:
# obj.find_by_name("Alice")
# obj.find_all_by_age(25)
# obj.where_status_eq("active")
#
def method_missing(method_name, *args)
# implementation
end
def respond_to_missing?(method_name, include_private = false)
# implementation
end
end
- テストを書く
# GOOD: respond_to?の動作をテスト
class TestableClass
def method_missing(method_name, *args)
if method_name.to_s.start_with?("test_")
"Test method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("test_") || super
end
end
# テストコード
obj = TestableClass.new
raise "Test failed" unless obj.respond_to?(:test_something)
raise "Test failed" if obj.respond_to?(:invalid_method)
raise "Test failed" unless obj.test_something == "Test method"
puts "All tests passed!"
Ruby 3.4での改善点
- Prismパーサーによる最適化 - respond_to_missing?の解析が高速化
- YJITの最適化 - respond_to?とrespond_to_missing?の呼び出しが効率化
- エラーメッセージの改善 - 動的メソッドに関するエラーがより詳細に
- パフォーマンス向上 - メソッド探索のオーバーヘッド削減
# Ruby 3.4では、respond_to?のパフォーマンスが向上
class OptimizedClass
def method_missing(method_name, *args)
"Dynamic: #{method_name}"
end
def respond_to_missing?(method_name, include_private = false)
true
end
end
obj = OptimizedClass.new
# 大量のrespond_to?呼び出しも高速
1000.times do |i|
obj.respond_to?("method_#{i}".to_sym)
end
まとめ
この記事では、respond_to_missing?について以下の内容を学びました:
- 基本概念と重要性 - method_missingとの整合性、メソッド存在確認
- 基本的な使い方 - 実装パターン、include_privateパラメータ、superの重要性
- 実践的なユースケース - finder メソッド、型安全設定、プロキシ、ビルダー、APIクライアント
- 注意点とベストプラクティス - ロジックの一致、super呼び出し、共通ロジック抽出
respond_to_missing?を適切に実装することで、動的メソッドが透過的に機能します。
Discussion