💎

【Ruby 45日目】オブジェクト指向 - 特異メソッドと特異クラス

に公開

はじめに

Rubyの特異メソッドと特異クラスについて、Ruby 3.4の仕様に基づいて詳しく解説します。

この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。

基本概念

特異メソッドと特異クラスは、Rubyのメタプログラミングの核心をなす概念です:

  • 特異メソッド(Singleton Method) - 特定のオブジェクトにのみ定義されるメソッド
  • 特異クラス(Singleton Class) - オブジェクト固有の隠されたクラス(eigenclassとも呼ばれる)
  • クラスメソッドの実体 - クラスメソッドは実際には特異メソッド
  • メソッド探索 - 特異クラスはメソッド探索チェーンに含まれる

特異メソッドを理解することで、Rubyのオブジェクトモデルの深い理解が得られます。

基本的な使い方

特異メソッドの定義

# オブジェクト固有のメソッドを定義
obj = "Hello"

def obj.shout
  self.upcase + "!!!"
end

puts obj.shout  #=> HELLO!!!

# 他の文字列オブジェクトには影響しない
other_str = "World"
# other_str.shout  # NoMethodError

特異クラスへのアクセス

obj = "Hello"

# 特異クラスを取得
singleton_class = obj.singleton_class

puts singleton_class  #=> #<Class:#<String:0x...>>

# 特異クラスにメソッドを定義
singleton_class.define_method(:reverse_shout) do
  self.reverse.upcase
end

puts obj.reverse_shout  #=> OLLEH

class << self構文

obj = "Hello"

class << obj
  def special_method
    "This is special: #{self}"
  end

  def another_method
    "Another special method"
  end
end

puts obj.special_method  #=> This is special: Hello
puts obj.another_method  #=> Another special method

クラスメソッドと特異メソッド

class MyClass
  # クラスメソッドは実際には特異メソッド
  def self.class_method1
    "Class method 1"
  end

  # class << self で複数のクラスメソッドを定義
  class << self
    def class_method2
      "Class method 2"
    end

    def class_method3
      "Class method 3"
    end
  end
end

puts MyClass.class_method1  #=> Class method 1
puts MyClass.class_method2  #=> Class method 2
puts MyClass.class_method3  #=> Class method 3

# クラスの特異クラスを確認
puts MyClass.singleton_class  #=> #<Class:MyClass>

特異メソッドの確認

obj = "Hello"

def obj.custom_method
  "Custom"
end

# 特異メソッドのリストを取得
puts obj.singleton_methods  #=> [:custom_method]

# 特異メソッドが定義されているか確認
puts obj.singleton_methods.include?(:custom_method)  #=> true

# 通常のメソッドと区別
puts obj.methods.include?(:upcase)  #=> true(Stringクラスのメソッド)
puts obj.singleton_methods.include?(:upcase)  #=> false

よくあるユースケース

ケース1: 設定オブジェクトのカスタマイズ

特定のオブジェクトにのみ振る舞いを追加します。

class Configuration
  attr_accessor :host, :port

  def initialize(host, port)
    @host = host
    @port = port
  end

  def url
    "http://#{@host}:#{@port}"
  end
end

# 通常の設定
normal_config = Configuration.new("localhost", 3000)
puts normal_config.url  #=> http://localhost:3000

# 特定の設定オブジェクトにのみSSL機能を追加
ssl_config = Configuration.new("secure.example.com", 443)

def ssl_config.url
  "https://#{@host}:#{@port}"
end

def ssl_config.ssl?
  true
end

puts ssl_config.url    #=> https://secure.example.com:443
puts ssl_config.ssl?   #=> true

# 通常の設定には影響しない
puts normal_config.url  #=> http://localhost:3000
# normal_config.ssl?  # NoMethodError

ケース2: モックオブジェクトの作成

テストでモックオブジェクトを簡単に作成します。

class UserRepository
  def find(id)
    # 実際のデータベースアクセス
    { id: id, name: "User #{id}" }
  end
end

# 本番コード
real_repo = UserRepository.new
puts real_repo.find(1).inspect  #=> {:id=>1, :name=>"User 1"}

# テスト用のモック
mock_repo = UserRepository.new

def mock_repo.find(id)
  # テスト用のダミーデータ
  { id: id, name: "Test User", email: "test@example.com" }
end

def mock_repo.reset
  puts "Mock repository reset"
end

puts mock_repo.find(1).inspect  #=> {:id=>1, :name=>"Test User", :email=>"test@example.com"}
mock_repo.reset  #=> Mock repository reset

ケース3: デコレーターパターン

既存のオブジェクトに機能を動的に追加します。

class Logger
  def log(message)
    puts message
  end
end

logger = Logger.new
logger.log("Normal message")  #=> Normal message

# タイムスタンプ付きロガーに変換
timestamped_logger = Logger.new

class << timestamped_logger
  alias_method :original_log, :log

  def log(message)
    timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
    original_log("[#{timestamp}] #{message}")
  end
end

timestamped_logger.log("Timestamped message")
#=> [2025-11-21 10:00:00] Timestamped message

# 元のロガーは影響を受けない
logger.log("Still normal")  #=> Still normal

ケース4: ファクトリーパターンの拡張

生成されたオブジェクトに特定の振る舞いを追加します。

class Product
  attr_reader :name, :price

  def initialize(name, price)
    @name = name
    @price = price
  end

  def display
    "#{@name}: ¥#{@price}"
  end
end

class ProductFactory
  def self.create_standard(name, price)
    Product.new(name, price)
  end

  def self.create_premium(name, price)
    product = Product.new(name, price)

    # プレミアム商品専用のメソッドを追加
    class << product
      def premium?
        true
      end

      def display
        "⭐ #{@name}: ¥#{@price} (Premium)"
      end

      def premium_discount(rate)
        discounted_price = (@price * (1 - rate)).round
        "Premium discount applied: ¥#{discounted_price}"
      end
    end

    product
  end

  def self.create_limited_edition(name, price, stock)
    product = Product.new(name, price)

    # 限定版商品専用のメソッドを追加
    class << product
      attr_accessor :stock

      def limited?
        true
      end

      def available?
        @stock > 0
      end

      def display
        status = available? ? "在庫あり" : "売り切れ"
        "🎁 #{@name}: ¥#{@price} (限定 #{@stock}個 - #{status})"
      end
    end

    product.stock = stock
    product
  end
end

standard = ProductFactory.create_standard("ノートPC", 80000)
premium = ProductFactory.create_premium("高級ノートPC", 150000)
limited = ProductFactory.create_limited_edition("限定モデル", 200000, 10)

puts standard.display  #=> ノートPC: ¥80000
puts premium.display   #=> ⭐ 高級ノートPC: ¥150000 (Premium)
puts limited.display   #=> 🎁 限定モデル: ¥200000 (限定 10個 - 在庫あり)

puts premium.premium_discount(0.1)  #=> Premium discount applied: ¥135000
puts limited.available?  #=> true

# 標準商品にはこれらのメソッドはない
# standard.premium?  # NoMethodError

ケース5: DSLの実装

特異クラスを使ってDSLを構築します。

class TaskRunner
  def initialize
    @tasks = []
  end

  def add_task(name, &block)
    @tasks << { name: name, block: block }
  end

  def run
    @tasks.each do |task|
      puts "Running: #{task[:name]}"
      task[:block].call
      puts "Completed: #{task[:name]}"
      puts "---"
    end
  end

  def self.define(&block)
    runner = new
    runner.instance_eval(&block)
    runner
  end
end

# DSLを使ったタスク定義
runner = TaskRunner.define do
  # instance_eval内なので、selfはTaskRunnerのインスタンス

  add_task("Setup") do
    puts "Setting up environment..."
  end

  add_task("Build") do
    puts "Building project..."
  end

  add_task("Test") do
    puts "Running tests..."
  end

  # 特定のrunnerインスタンスにのみヘルパーメソッドを追加
  def self.quick_mode
    puts "[QUICK MODE] Skipping some tasks..."
    @tasks = @tasks.first(1)
  end
end

runner.run
#=> Running: Setup
#   Setting up environment...
#   Completed: Setup
#   ---
#   Running: Build
#   Building project...
#   (etc.)

# 別のrunnerでクイックモードを使用
quick_runner = TaskRunner.define do
  add_task("Quick Task 1") { puts "Task 1" }
  add_task("Quick Task 2") { puts "Task 2" }
  add_task("Quick Task 3") { puts "Task 3" }
end

quick_runner.quick_mode
quick_runner.run
#=> [QUICK MODE] Skipping some tasks...
#   Running: Quick Task 1
#   Task 1
#   Completed: Quick Task 1
#   ---

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

注意点

  1. メモリリークに注意
# BAD: 大量のオブジェクトに特異メソッドを定義
objects = []
10000.times do |i|
  obj = "Object #{i}"
  def obj.custom_method
    "Custom: #{self}"
  end
  objects << obj
end

# 各オブジェクトが特異クラスを持つためメモリ使用量が増加

# GOOD: 共通の振る舞いはクラスメソッドに
class CustomString < String
  def custom_method
    "Custom: #{self}"
  end
end

objects = []
10000.times do |i|
  objects << CustomString.new("Object #{i}")
end
# 特異クラスは作成されず、メモリ効率が良い
  1. 可読性への影響
# BAD: 過度な特異メソッドの使用
obj1 = Object.new
def obj1.method1; "1"; end
def obj1.method2; "2"; end
def obj1.method3; "3"; end

obj2 = Object.new
def obj2.method1; "A"; end
def obj2.method2; "B"; end

# オブジェクトごとに異なる振る舞いで混乱

# GOOD: クラスで振る舞いを定義
class Type1
  def method1; "1"; end
  def method2; "2"; end
  def method3; "3"; end
end

class Type2
  def method1; "A"; end
  def method2; "B"; end
end

obj1 = Type1.new
obj2 = Type2.new
  1. 継承との関係
# 特異メソッドは継承されない
class Parent
end

parent = Parent.new
def parent.special_method
  "Special"
end

class Child < Parent
end

child = Child.new
# child.special_method  # NoMethodError(継承されない)

# クラスメソッドも特異メソッドなので継承される
class BaseClass
  def self.class_method
    "Base class method"
  end
end

class DerivedClass < BaseClass
end

puts DerivedClass.class_method  #=> Base class method(継承される)

ベストプラクティス

  1. クラスメソッドの定義には class << self を使う
# GOOD: 関連するクラスメソッドをまとめる
class User
  class << self
    attr_accessor :default_role

    def find(id)
      # ユーザー検索
    end

    def create(attributes)
      # ユーザー作成
    end

    def authenticate(username, password)
      # 認証
    end
  end
end

User.default_role = :member
  1. 一時的な振る舞いの変更に使用
# GOOD: テストやデバッグで一時的に振る舞いを変更
class APIClient
  def fetch(url)
    # 実際のHTTPリクエスト
    "Real data from #{url}"
  end
end

client = APIClient.new

# テスト用にモック化
def client.fetch(url)
  "Mock data for #{url}"
end

puts client.fetch("/users")  #=> Mock data for /users
  1. オブジェクト固有の状態を持つメソッド
# GOOD: 特定のインスタンスにのみキャッシュ機能を追加
class ExpensiveCalculation
  def calculate(input)
    puts "Calculating for #{input}..."
    sleep(1)
    input * 2
  end
end

# キャッシュ機能を追加
obj = ExpensiveCalculation.new

class << obj
  def calculate(input)
    @cache ||= {}
    @cache[input] ||= super
  end

  def clear_cache
    @cache = {}
  end
end

puts obj.calculate(5)  #=> Calculating for 5... \n 10
puts obj.calculate(5)  #=> 10(キャッシュから)
obj.clear_cache
puts obj.calculate(5)  #=> Calculating for 5... \n 10(再計算)

Ruby 3.4での改善点

  • Prismパーサーによる最適化 - 特異メソッド定義の解析が高速化
  • YJITの最適化 - 特異メソッド呼び出しのパフォーマンス向上
  • メモリ効率の改善 - 特異クラスのメモリ使用量が削減
  • デバッグ情報の向上 - 特異メソッドのスタックトレースがより詳細に
# Ruby 3.4では、特異メソッドの定義と呼び出しが効率的
class PerformanceTest
  def self.benchmark
    start_time = Time.now

    obj = Object.new
    1000.times do
      def obj.test_method
        "test"
      end
      obj.test_method
    end

    Time.now - start_time
  end
end

puts "Time: #{PerformanceTest.benchmark}s"

まとめ

この記事では、特異メソッドと特異クラスについて以下の内容を学びました:

  • 基本概念と重要性 - オブジェクト固有のメソッド、特異クラス、クラスメソッドの実体
  • 基本的な使い方と構文 - def obj.method、class << obj、singleton_class
  • 実践的なユースケース - カスタマイズ、モック、デコレーター、ファクトリー、DSL
  • 注意点とベストプラクティス - メモリ使用量、可読性、一時的な振る舞いの変更

特異メソッドと特異クラスを理解することで、Rubyの柔軟性を最大限に活用できます。

参考資料

Discussion