💎

【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: []
#   ==================================================

注意点とベストプラクティス

注意点

  1. パフォーマンスへの影響
# 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)
  1. セキュリティリスク
# 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
  1. 可読性の低下
# 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

ベストプラクティス

  1. 明示的なインターフェースを提供
# 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
  1. 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
  1. デバッグしやすいコードを書く
# 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