💎
【Ruby 49日目】オブジェクト指向 - メタプログラミング基礎
はじめに
Rubyのメタプログラミング基礎について、Ruby 3.4の仕様に基づいて詳しく解説します。
この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。
基本概念
メタプログラミングは、プログラムがプログラム自身を操作する技術です:
- コードを書くコード - 実行時にメソッドやクラスを動的に定義できる
- リフレクション - オブジェクトやクラスの情報を実行時に調べられる
- 動的性 - Rubyの柔軟性を最大限に活用できる
- DRY原則 - 繰り返しを避け、より簡潔なコードを書ける
メタプログラミングを理解することで、Rubyの真の力を引き出すことができます。
基本的な使い方
sendメソッドで動的にメソッドを呼び出す
class Calculator
def add(a, b)
a + b
end
def subtract(a, b)
a - b
end
def multiply(a, b)
a * b
end
end
calc = Calculator.new
# 通常のメソッド呼び出し
puts calc.add(10, 5) #=> 15
# sendを使った動的呼び出し
method_name = :subtract
puts calc.send(method_name, 10, 5) #=> 5
# ユーザー入力から動的に選択
operation = "multiply"
puts calc.send(operation.to_sym, 10, 5) #=> 50
public_sendで安全に呼び出す
class BankAccount
def initialize(balance)
@balance = balance
end
def deposit(amount)
@balance += amount
end
private
def internal_calculation
@balance * 0.01
end
end
account = BankAccount.new(1000)
# sendはprivateメソッドも呼べる
puts account.send(:internal_calculation) #=> 10.0
# public_sendはpublicメソッドのみ
puts account.public_send(:deposit, 500) #=> 1500
# public_sendでprivateメソッドを呼ぶとエラー
# account.public_send(:internal_calculation) # NoMethodError
define_methodで動的にメソッドを定義
class Person
ATTRIBUTES = [:name, :age, :email]
# 動的にゲッターとセッターを定義
ATTRIBUTES.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
person = Person.new
person.name = "Alice"
person.age = 25
person.email = "alice@example.com"
puts person.name #=> Alice
puts person.age #=> 25
puts person.email #=> alice@example.com
class_evalでクラスのコンテキストで評価
class MyClass
end
# class_evalでクラスにメソッドを追加
MyClass.class_eval do
def greet
"Hello from MyClass"
end
def self.class_method
"This is a class method"
end
end
obj = MyClass.new
puts obj.greet #=> Hello from MyClass
puts MyClass.class_method #=> This is a class method
instance_evalでインスタンスのコンテキストで評価
class Person
def initialize(name)
@name = name
end
end
person = Person.new("Alice")
# instance_evalでインスタンス変数に直接アクセス
person.instance_eval do
puts @name #=> Alice
@age = 25
end
# instance_evalで特異メソッドを定義
person.instance_eval do
def special_greeting
"Special hello from #{@name}"
end
end
puts person.special_greeting #=> Special hello from Alice
respond_to?でメソッドの存在を確認
class Calculator
def add(a, b)
a + b
end
private
def secret_method
"secret"
end
end
calc = Calculator.new
puts calc.respond_to?(:add) #=> true
puts calc.respond_to?(:subtract) #=> false
# privateメソッドの確認
puts calc.respond_to?(:secret_method) #=> false
puts calc.respond_to?(:secret_method, true) #=> true(includePrivateをtrue)
methodsでメソッド一覧を取得
class Example
def public_method1
end
def public_method2
end
private
def private_method
end
protected
def protected_method
end
end
obj = Example.new
# publicメソッドの一覧
puts obj.methods.grep(/method/)
#=> [:public_method1, :public_method2]
# privateメソッドの一覧
puts obj.private_methods.grep(/method/)
#=> [:private_method]
# protectedメソッドの一覧
puts obj.protected_methods.grep(/method/)
#=> [:protected_method]
よくあるユースケース
ケース1: 動的なアクセサメソッドの生成
複数の属性に対して自動的にゲッター・セッターを生成します。
class Configuration
SETTINGS = [:host, :port, :username, :password, :timeout]
def initialize
@config = {}
end
# 各設定項目に対してゲッター・セッターを自動生成
SETTINGS.each do |setting|
define_method(setting) do
@config[setting]
end
define_method("#{setting}=") do |value|
@config[setting] = value
end
# 真偽値チェックメソッドも生成
define_method("#{setting}?") do
!@config[setting].nil?
end
end
def to_h
@config
end
end
config = Configuration.new
config.host = "localhost"
config.port = 3000
config.username = "admin"
config.timeout = 30
puts config.host #=> localhost
puts config.port #=> 3000
puts config.host? #=> true
puts config.password? #=> false
puts config.to_h.inspect
#=> {:host=>"localhost", :port=>3000, :username=>"admin", :timeout=>30}
ケース2: DSLの構築
ドメイン特化言語を作成します。
class RouteBuilder
def initialize
@routes = []
end
def get(path, &block)
add_route(:get, path, block)
end
def post(path, &block)
add_route(:post, path, block)
end
def put(path, &block)
add_route(:put, path, block)
end
def delete(path, &block)
add_route(:delete, path, block)
end
def routes
@routes
end
private
def add_route(method, path, handler)
@routes << { method: method, path: path, handler: handler }
end
end
# DSLを使ったルート定義
routes = RouteBuilder.new
routes.instance_eval do
get "/users" do
"List all users"
end
post "/users" do
"Create a user"
end
get "/users/:id" do |id|
"Show user #{id}"
end
delete "/users/:id" do |id|
"Delete user #{id}"
end
end
# ルートを実行
routes.routes.each do |route|
puts "#{route[:method].upcase} #{route[:path]}"
result = route[:handler].call(123) if route[:path].include?(":id")
result ||= route[:handler].call
puts " => #{result}"
puts
end
#=> GET /users
# => List all users
#
# POST /users
# => Create a user
#
# GET /users/:id
# => Show user 123
#
# DELETE /users/:id
# => Delete user 123
ケース3: 動的なバリデーションメソッド
属性に対して動的にバリデーションを追加します。
class Validator
def self.validates(attribute, **options)
if options[:presence]
define_method("validate_#{attribute}_presence") do
value = instance_variable_get("@#{attribute}")
if value.nil? || (value.respond_to?(:empty?) && value.empty?)
raise "#{attribute} cannot be empty"
end
end
end
if options[:length]
define_method("validate_#{attribute}_length") do
value = instance_variable_get("@#{attribute}")
return if value.nil?
if options[:length][:minimum]
if value.length < options[:length][:minimum]
raise "#{attribute} is too short (minimum is #{options[:length][:minimum]})"
end
end
if options[:length][:maximum]
if value.length > options[:length][:maximum]
raise "#{attribute} is too long (maximum is #{options[:length][:maximum]})"
end
end
end
end
if options[:format]
define_method("validate_#{attribute}_format") do
value = instance_variable_get("@#{attribute}")
return if value.nil?
unless value.match?(options[:format])
raise "#{attribute} has invalid format"
end
end
end
end
def validate
self.class.instance_methods.grep(/^validate_/).each do |method|
send(method)
end
true
end
end
class User < Validator
attr_accessor :username, :email, :password
validates :username, presence: true, length: { minimum: 3, maximum: 20 }
validates :email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :password, presence: true, length: { minimum: 8 }
def initialize(username, email, password)
@username = username
@email = email
@password = password
end
end
# 正しい入力
user1 = User.new("alice", "alice@example.com", "password123")
puts user1.validate #=> true
# バリデーションエラー
begin
user2 = User.new("ab", "invalid-email", "pass")
user2.validate
rescue => e
puts "Error: #{e.message}" #=> Error: username is too short (minimum is 3)
end
ケース4: プラグインシステム
動的にプラグインを読み込むシステムを構築します。
class PluginManager
def initialize
@plugins = {}
end
def register(name, plugin_class)
@plugins[name] = plugin_class
end
def load(name, *args)
plugin_class = @plugins[name]
raise "Plugin #{name} not found" unless plugin_class
plugin_class.new(*args)
end
def available_plugins
@plugins.keys
end
end
# プラグインの基底クラス
class Plugin
def execute
raise NotImplementedError, "Subclass must implement execute method"
end
end
# プラグインの実装
class LoggerPlugin < Plugin
def initialize(log_level = :info)
@log_level = log_level
end
def execute(message)
"[#{@log_level.upcase}] #{message}"
end
end
class EmailPlugin < Plugin
def initialize(smtp_server)
@smtp_server = smtp_server
end
def execute(to, subject, body)
"Sending email to #{to} via #{@smtp_server}: #{subject}"
end
end
class CachePlugin < Plugin
def initialize
@cache = {}
end
def execute(key, value = nil)
if value
@cache[key] = value
"Cached: #{key} => #{value}"
else
@cache[key] || "Not found"
end
end
end
# プラグインマネージャーの使用
manager = PluginManager.new
manager.register(:logger, LoggerPlugin)
manager.register(:email, EmailPlugin)
manager.register(:cache, CachePlugin)
puts "Available plugins: #{manager.available_plugins.join(', ')}"
#=> Available plugins: logger, email, cache
# プラグインを動的にロード
logger = manager.load(:logger, :debug)
puts logger.execute("Application started")
#=> [DEBUG] Application started
email = manager.load(:email, "smtp.example.com")
puts email.execute("user@example.com", "Hello", "Test message")
#=> Sending email to user@example.com via smtp.example.com: Hello
cache = manager.load(:cache)
puts cache.execute(:user_1, { name: "Alice", age: 25 })
#=> Cached: user_1 => {:name=>"Alice", :age=>25}
puts cache.execute(:user_1)
#=> {:name=>"Alice", :age=>25}
ケース5: オブジェクトのインスペクション
実行時にオブジェクトの情報を詳しく調べます。
class ObjectInspector
def self.inspect_object(obj)
puts "=" * 50
puts "Object Inspection"
puts "=" * 50
puts "\nClass: #{obj.class}"
puts "Object ID: #{obj.object_id}"
puts "Superclass: #{obj.class.superclass}"
puts "\nAncestors:"
obj.class.ancestors.each_with_index do |ancestor, index|
puts " #{index + 1}. #{ancestor}"
end
puts "\nInstance Variables:"
obj.instance_variables.each do |ivar|
value = obj.instance_variable_get(ivar)
puts " #{ivar} = #{value.inspect}"
end
puts "\nPublic Methods (excluding inherited):"
methods = obj.methods - Object.methods
methods.sort.each do |method|
puts " - #{method}"
end
puts "\nPrivate Methods:"
private_methods = obj.private_methods - Object.private_methods
private_methods.sort.each do |method|
puts " - #{method}"
end
puts "\nMethod Lookup:"
if methods.any?
sample_method = methods.first
method_obj = obj.method(sample_method)
puts " Method: #{sample_method}"
puts " Owner: #{method_obj.owner}"
puts " Parameters: #{method_obj.parameters.inspect}"
end
puts "=" * 50
end
end
class Example
attr_accessor :name, :value
def initialize(name, value)
@name = name
@value = value
@created_at = Time.now
end
def process
"Processing #{@name}"
end
private
def internal_method
@value * 2
end
end
obj = Example.new("Test", 100)
ObjectInspector.inspect_object(obj)
#=> ==================================================
# Object Inspection
# ==================================================
#
# Class: Example
# Object ID: ...
# Superclass: Object
#
# Ancestors:
# 1. Example
# 2. Object
# 3. Kernel
# 4. BasicObject
#
# Instance Variables:
# @name = "Test"
# @value = 100
# @created_at = ...
#
# Public Methods (excluding inherited):
# - name
# - name=
# - process
# - value
# - value=
#
# Private Methods:
# - internal_method
#
# Method Lookup:
# Method: name
# Owner: Example
# Parameters: []
# ==================================================
注意点とベストプラクティス
注意点
- パフォーマンスへの影響
# BAD: 毎回sendを使うと遅い
1000.times do
obj.send(:method_name)
end
# GOOD: 直接呼び出すほうが速い
1000.times do
obj.method_name
end
# ACCEPTABLE: メソッド名が動的な場合のみsendを使う
method = user_input.to_sym
obj.send(method) if obj.respond_to?(method)
- セキュリティリスク
# BAD: ユーザー入力をそのまま使うのは危険
user_input = params[:method]
obj.send(user_input.to_sym) # 任意のメソッド実行が可能
# GOOD: ホワイトリストで制限
ALLOWED_METHODS = [:method1, :method2, :method3]
method = user_input.to_sym
if ALLOWED_METHODS.include?(method)
obj.public_send(method)
else
raise "Method not allowed"
end
- 可読性の低下
# BAD: 過度なメタプログラミングは読みにくい
class Foo
%w[a b c d e f].each do |letter|
define_method("process_#{letter}") do |arg|
send("helper_#{letter}", arg) if respond_to?("helper_#{letter}")
end
end
end
# GOOD: 必要な場合のみ使う
class Foo
def process_a(arg)
helper_a(arg) if respond_to?(:helper_a)
end
def process_b(arg)
helper_b(arg) if respond_to?(:helper_b)
end
end
ベストプラクティス
- 明示的なインターフェースを提供
# GOOD: メタプログラミングで生成したメソッドもドキュメント化
class User
# Defines getter and setter for: name, email, age
[:name, :email, :age].each do |attr|
define_method(attr) { instance_variable_get("@#{attr}") }
define_method("#{attr}=") { |v| instance_variable_set("@#{attr}", v) }
end
end
- respond_to?を適切に実装
# GOOD: 動的メソッドに対してrespond_to?も対応
class DynamicMethods
def method_missing(method, *args)
if method.to_s.start_with?("find_by_")
attribute = method.to_s.sub("find_by_", "")
# 検索処理
else
super
end
end
def respond_to_missing?(method, include_private = false)
method.to_s.start_with?("find_by_") || super
end
end
- デバッグしやすいコードを書く
# GOOD: エラーメッセージを明確に
class SafeInvoker
def invoke(method_name, *args)
unless respond_to?(method_name)
raise NoMethodError, "Method '#{method_name}' not found in #{self.class}"
end
send(method_name, *args)
rescue => e
puts "Error invoking #{method_name}: #{e.message}"
puts e.backtrace.first(5)
raise
end
end
Ruby 3.4での改善点
- Prismパーサーによる最適化 - メタプログラミング関連の構文解析が高速化
- YJITの最適化 - sendやdefine_methodのパフォーマンス向上
- エラーメッセージの改善 - method_missingなどのエラーがより明確に
- メモリ効率の向上 - 動的メソッド定義のメモリ使用量が削減
# Ruby 3.4では、メタプログラミングのパフォーマンスが向上
class MetaBenchmark
100.times do |i|
define_method("method_#{i}") do
"Result #{i}"
end
end
end
obj = MetaBenchmark.new
# 動的に定義されたメソッドの呼び出しも高速
100.times { |i| obj.send("method_#{i}") }
まとめ
この記事では、メタプログラミング基礎について以下の内容を学びました:
- 基本概念と重要性 - コードを書くコード、リフレクション、動的性
- 基本的な使い方 - send、define_method、class_eval、instance_eval
- 実践的なユースケース - 動的アクセサ、DSL、バリデーション、プラグイン、インスペクション
- 注意点とベストプラクティス - パフォーマンス、セキュリティ、可読性
メタプログラミングを適切に使うことで、より柔軟で保守性の高いコードを書けます。
Discussion