💎
【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
# ---
注意点とベストプラクティス
注意点
- メモリリークに注意
# 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
# 特異クラスは作成されず、メモリ効率が良い
- 可読性への影響
# 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
- 継承との関係
# 特異メソッドは継承されない
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(継承される)
ベストプラクティス
- クラスメソッドの定義には 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
- 一時的な振る舞いの変更に使用
# 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
- オブジェクト固有の状態を持つメソッド
# 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