💎

【Ruby 48日目】オブジェクト指向 - initializeメソッド

に公開

はじめに

Rubyのinitializeメソッドについて、Ruby 3.4の仕様に基づいて詳しく解説します。

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

基本概念

initializeメソッドは、オブジェクトの初期化を行う特別なメソッドです:

  • コンストラクタ - newメソッド呼び出し時に自動的に実行される
  • プライベートメソッド - 外部から直接呼び出すことはできない
  • 初期化処理 - インスタンス変数の設定やセットアップ処理を行う
  • 引数の受け取り - newに渡された引数がそのまま渡される

initializeメソッドを理解することで、オブジェクトの適切な初期化が可能になります。

基本的な使い方

シンプルなinitialize

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def introduce
    "I am #{@name}, #{@age} years old"
  end
end

person = Person.new("Alice", 25)
puts person.introduce  #=> I am Alice, 25 years old

デフォルト値を持つinitialize

class Product
  def initialize(name, price = 0, stock = 0)
    @name = name
    @price = price
    @stock = stock
  end

  def display
    "#{@name}: ¥#{@price} (在庫: #{@stock})"
  end
end

product1 = Product.new("ノートPC", 80000, 10)
puts product1.display  #=> ノートPC: ¥80000 (在庫: 10)

product2 = Product.new("マウス", 1500)
puts product2.display  #=> マウス: ¥1500 (在庫: 0)

product3 = Product.new("キーボード")
puts product3.display  #=> キーボード: ¥0 (在庫: 0)

キーワード引数を使ったinitialize

class User
  def initialize(name:, email:, age: nil, role: :member)
    @name = name
    @email = email
    @age = age
    @role = role
  end

  def to_s
    details = "Name: #{@name}, Email: #{@email}"
    details += ", Age: #{@age}" if @age
    details += ", Role: #{@role}"
    details
  end
end

user1 = User.new(name: "Alice", email: "alice@example.com", age: 25, role: :admin)
puts user1  #=> Name: Alice, Email: alice@example.com, Age: 25, Role: admin

user2 = User.new(name: "Bob", email: "bob@example.com")
puts user2  #=> Name: Bob, Email: bob@example.com, Role: member

ブロックを受け取るinitialize

class Configuration
  def initialize
    @settings = {}
    yield self if block_given?
  end

  def set(key, value)
    @settings[key] = value
  end

  def get(key)
    @settings[key]
  end

  def to_h
    @settings
  end
end

config = Configuration.new do |c|
  c.set(:host, "localhost")
  c.set(:port, 3000)
  c.set(:debug, true)
end

puts config.to_h.inspect  #=> {:host=>"localhost", :port=>3000, :debug=>true}

継承とsuperの使用

class Animal
  def initialize(name)
    @name = name
    puts "Animal initialized: #{@name}"
  end

  def speak
    "..."
  end
end

class Dog < Animal
  def initialize(name, breed)
    super(name)  # 親クラスのinitializeを呼ぶ
    @breed = breed
    puts "Dog initialized: #{@breed}"
  end

  def speak
    "Woof! I'm #{@name}, a #{@breed}"
  end
end

dog = Dog.new("Pochi", "Shiba")
#=> Animal initialized: Pochi
#   Dog initialized: Shiba

puts dog.speak  #=> Woof! I'm Pochi, a Shiba

バリデーション付きinitialize

class BankAccount
  attr_reader :account_number, :balance

  def initialize(account_number, initial_balance = 0)
    raise ArgumentError, "Account number must be a string" unless account_number.is_a?(String)
    raise ArgumentError, "Account number cannot be empty" if account_number.empty?
    raise ArgumentError, "Initial balance cannot be negative" if initial_balance < 0

    @account_number = account_number
    @balance = initial_balance
    @created_at = Time.now
  end

  def display
    "Account: #{@account_number}, Balance: ¥#{@balance}, Created: #{@created_at}"
  end
end

account = BankAccount.new("ACC-001", 10000)
puts account.display
#=> Account: ACC-001, Balance: ¥10000, Created: 2025-11-24 10:00:00

# エラーケース
begin
  BankAccount.new("", 5000)
rescue ArgumentError => e
  puts "Error: #{e.message}"  #=> Error: Account number cannot be empty
end

begin
  BankAccount.new("ACC-002", -1000)
rescue ArgumentError => e
  puts "Error: #{e.message}"  #=> Error: Initial balance cannot be negative
end

よくあるユースケース

ケース1: 依存性の注入

他のオブジェクトへの依存を初期化時に注入します。

class Logger
  def log(message)
    puts "[#{Time.now}] #{message}"
  end
end

class DatabaseLogger
  def log(message)
    puts "[DB LOG] #{message}"
  end
end

class UserService
  def initialize(logger = Logger.new)
    @logger = logger
  end

  def create_user(name, email)
    # ユーザー作成処理
    user = { name: name, email: email, id: rand(1000) }
    @logger.log("User created: #{name} (#{email})")
    user
  end

  def delete_user(id)
    @logger.log("User deleted: ID #{id}")
  end
end

# 通常のロガーを使用
service1 = UserService.new
service1.create_user("Alice", "alice@example.com")
#=> [2025-11-24 10:00:00] User created: Alice (alice@example.com)

# データベースロガーを注入
service2 = UserService.new(DatabaseLogger.new)
service2.create_user("Bob", "bob@example.com")
#=> [DB LOG] User created: Bob (bob@example.com)

ケース2: ファクトリーパターンとの組み合わせ

initializeをprivateにして、ファクトリーメソッド経由でのみ生成できるようにします。

class Connection
  attr_reader :type, :config

  private_class_method :new

  def initialize(type, config)
    @type = type
    @config = config
    @connected = false
  end

  def self.database(host, port, database)
    config = {
      host: host,
      port: port,
      database: database
    }
    new(:database, config)
  end

  def self.api(endpoint, api_key)
    config = {
      endpoint: endpoint,
      api_key: api_key
    }
    new(:api, config)
  end

  def self.file(path)
    config = { path: path }
    new(:file, config)
  end

  def connect
    @connected = true
    "Connected to #{@type}: #{@config.inspect}"
  end
end

db_conn = Connection.database("localhost", 5432, "myapp")
puts db_conn.connect
#=> Connected to database: {:host=>"localhost", :port=>5432, :database=>"myapp"}

api_conn = Connection.api("https://api.example.com", "secret-key")
puts api_conn.connect
#=> Connected to api: {:endpoint=>"https://api.example.com", :api_key=>"secret-key"}

# 直接newは呼べない
# Connection.new(:test, {})  # NoMethodError

ケース3: 遅延初期化

必要になるまで初期化を遅延させます。

class DataProcessor
  def initialize(source)
    @source = source
    @data = nil
    @processed = nil
  end

  def data
    @data ||= load_data
  end

  def processed_data
    @processed ||= process_data
  end

  private

  def load_data
    puts "Loading data from #{@source}..."
    sleep(0.5)  # データ読み込みをシミュレート
    ["item1", "item2", "item3"]
  end

  def process_data
    puts "Processing data..."
    sleep(0.5)  # データ処理をシミュレート
    data.map(&:upcase)
  end
end

processor = DataProcessor.new("database")
puts "Processor created"  #=> Processor created(まだデータは読み込まれていない)

puts processor.data.inspect
#=> Loading data from database...
#   ["item1", "item2", "item3"]

puts processor.data.inspect  # 再度呼んでもキャッシュから返される
#=> ["item1", "item2", "item3"](Loading...は表示されない)

puts processor.processed_data.inspect
#=> Processing data...
#   ["ITEM1", "ITEM2", "ITEM3"]

ケース4: 複雑なオブジェクト構築

ビルダーパターンと組み合わせて複雑なオブジェクトを構築します。

class EmailBuilder
  attr_reader :email

  def initialize
    @email = Email.new
  end

  def from(address)
    @email.from = address
    self
  end

  def to(address)
    @email.to = address
    self
  end

  def subject(text)
    @email.subject = text
    self
  end

  def body(text)
    @email.body = text
    self
  end

  def attach(filename)
    @email.attachments << filename
    self
  end

  def build
    raise "Sender is required" if @email.from.nil?
    raise "Recipient is required" if @email.to.nil?
    raise "Subject is required" if @email.subject.nil?
    @email
  end
end

class Email
  attr_accessor :from, :to, :subject, :body
  attr_reader :attachments

  def initialize
    @from = nil
    @to = nil
    @subject = nil
    @body = ""
    @attachments = []
  end

  def send
    puts "Sending email:"
    puts "  From: #{@from}"
    puts "  To: #{@to}"
    puts "  Subject: #{@subject}"
    puts "  Body: #{@body}"
    puts "  Attachments: #{@attachments.join(', ')}" unless @attachments.empty?
    "Email sent successfully"
  end
end

email = EmailBuilder.new
  .from("sender@example.com")
  .to("recipient@example.com")
  .subject("Meeting Tomorrow")
  .body("Let's meet at 10 AM")
  .attach("agenda.pdf")
  .attach("slides.pptx")
  .build

puts email.send
#=> Sending email:
#     From: sender@example.com
#     To: recipient@example.com
#     Subject: Meeting Tomorrow
#     Body: Let's meet at 10 AM
#     Attachments: agenda.pdf, slides.pptx
#   Email sent successfully

ケース5: 継承チェーンでの初期化

複数階層の継承で適切にinitializeを連鎖させます。

class Vehicle
  attr_reader :make, :model

  def initialize(make, model)
    @make = make
    @model = model
    puts "Vehicle initialized: #{@make} #{@model}"
  end

  def info
    "#{@make} #{@model}"
  end
end

class Car < Vehicle
  attr_reader :doors

  def initialize(make, model, doors = 4)
    super(make, model)
    @doors = doors
    puts "Car initialized: #{@doors} doors"
  end

  def info
    "#{super}, #{@doors} doors"
  end
end

class ElectricCar < Car
  attr_reader :battery_capacity

  def initialize(make, model, doors = 4, battery_capacity:)
    super(make, model, doors)
    @battery_capacity = battery_capacity
    puts "ElectricCar initialized: #{@battery_capacity}kWh battery"
  end

  def info
    "#{super}, #{@battery_capacity}kWh battery"
  end

  def range
    @battery_capacity * 5  # 簡易的な航続距離計算
  end
end

tesla = ElectricCar.new("Tesla", "Model 3", 4, battery_capacity: 75)
#=> Vehicle initialized: Tesla Model 3
#   Car initialized: 4 doors
#   ElectricCar initialized: 75kWh battery

puts tesla.info  #=> Tesla Model 3, 4 doors, 75kWh battery
puts "Range: #{tesla.range}km"  #=> Range: 375km

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

注意点

  1. initializeは常にprivate
class Example
  def initialize(value)
    @value = value
  end
end

obj = Example.new(10)

# initializeは直接呼べない
# obj.initialize(20)  # NoMethodError: private method `initialize' called
  1. superの引数に注意
class Parent
  def initialize(a, b)
    @a = a
    @b = b
  end
end

class Child1 < Parent
  def initialize(a, b, c)
    super(a, b)  # 明示的に引数を指定
    @c = c
  end
end

class Child2 < Parent
  def initialize(a, b, c)
    # BAD: superは全引数を渡そうとする
    # super  # ArgumentError: wrong number of arguments (given 3, expected 2)
    super(a, b)  # GOOD: 必要な引数のみ渡す
    @c = c
  end
end

child = Child1.new(1, 2, 3)  # OK
  1. 循環参照に注意
# BAD: 循環参照が発生する可能性
class Node
  attr_accessor :value, :parent

  def initialize(value, parent = nil)
    @value = value
    @parent = parent
    parent.children << self if parent
  end
end

# GOOD: 循環参照を避ける
class BetterNode
  attr_accessor :value
  attr_reader :children

  def initialize(value)
    @value = value
    @children = []
  end

  def add_child(child)
    @children << child
  end
end

ベストプラクティス

  1. 必須パラメータとオプションパラメータを明確に
# GOOD: キーワード引数で必須とオプションを明確に
class User
  def initialize(name:, email:, age: nil, role: :member)
    @name = name      # 必須
    @email = email    # 必須
    @age = age        # オプション
    @role = role      # オプション(デフォルト値あり)
  end
end

# 必須パラメータがないとエラー
# User.new(age: 25)  # ArgumentError: missing keywords: :name, :email
  1. バリデーションは早期に
# GOOD: initializeでバリデーション
class Rectangle
  def initialize(width, height)
    raise ArgumentError, "Width must be positive" unless width > 0
    raise ArgumentError, "Height must be positive" unless height > 0

    @width = width
    @height = height
  end

  def area
    @width * @height
  end
end

# エラーはオブジェクト作成時に発生
# rect = Rectangle.new(-5, 10)  # ArgumentError
  1. 複雑な初期化は別メソッドに分離
# GOOD: 初期化処理を分離
class Application
  def initialize(config_file)
    @config = load_config(config_file)
    @database = setup_database
    @cache = setup_cache
    @logger = setup_logger
  end

  private

  def load_config(file)
    # 設定ファイルの読み込み
    { host: "localhost", port: 3000 }
  end

  def setup_database
    # データベース接続のセットアップ
    "DB Connection"
  end

  def setup_cache
    # キャッシュのセットアップ
    {}
  end

  def setup_logger
    # ロガーのセットアップ
    Logger.new(STDOUT)
  end
end

Ruby 3.4での改善点

  • Prismパーサーによる最適化 - initializeメソッドの解析が高速化
  • YJITの最適化 - オブジェクト生成とinitialize呼び出しが効率的に
  • メモリ管理の改善 - 初期化時のメモリ割り当てが最適化
  • エラーメッセージの改善 - パラメータエラーがより明確に
# Ruby 3.4では、大量のオブジェクト生成が高速
class Point
  def initialize(x, y)
    @x = x
    @y = y
  end

  def distance_from_origin
    Math.sqrt(@x**2 + @y**2)
  end
end

# 大量のオブジェクト生成が最適化されている
points = 100000.times.map { |i| Point.new(i, i * 2) }

まとめ

この記事では、initializeメソッドについて以下の内容を学びました:

  • 基本概念と重要性 - コンストラクタ、プライベートメソッド、初期化処理
  • 基本的な使い方と構文 - デフォルト値、キーワード引数、ブロック、super
  • 実践的なユースケース - 依存性注入、ファクトリー、遅延初期化、ビルダー、継承チェーン
  • 注意点とベストプラクティス - super引数、バリデーション、メソッド分離

initializeメソッドを適切に使うことで、オブジェクトの初期化が確実に行えます。

参考資料

Discussion