Ruby

オブジェクト指向

Ruby

クラス・継承・モジュール・Mixin

クラスの定義

attr_accessor・initialize・可視性

classes.rb ruby
class BankAccount
  # アクセサの定義
  attr_reader   :owner              # getter のみ
  attr_writer   :email              # setter のみ
  attr_accessor :balance            # getter + setter

  # クラス変数・クラスインスタンス変数
  @@count = 0                       # クラス変数(継承で共有)
  @interest_rate = 0.02             # クラスインスタンス変数(継承で独立)

  def initialize(owner, balance = 0)
    @owner   = owner
    @balance = balance.to_f
    @@count += 1
  end

  # インスタンスメソッド
  def deposit(amount)
    @balance += amount
    self  # メソッドチェーン用
  end

  # クラスメソッド
  def self.count = @@count
  def self.interest_rate = @interest_rate

  # メソッドの可視性
  def public_method = '誰でも呼べる'

  protected
  def protected_method = '同じクラス・サブクラスから呼べる'

  private
  def private_method = 'このクラスのインスタンスからのみ'
  def validate_amount(amount)
    raise ArgumentError, '金額は正の数' unless amount.positive?
  end

  # private は self. でも呼べない(Ruby 2.7 以前)
  # Ruby 3.0+ は private メソッドに self. を使えない
end

acc = BankAccount.new('Alice', 1000)
acc.deposit(500).deposit(200)  # メソッドチェーン
puts BankAccount.count  # 1

継承とモジュール

super・オーバーライド・include・extend・prepend

inheritance.rb ruby
# 継承(単一継承)
class Animal
  attr_reader :name
  def initialize(name) = (@name = name)
  def speak = raise NotImplementedError, "#{self.class} must implement speak"
  def to_s  = "#{self.class.name}(#{name})"
end

class Dog < Animal
  def speak = 'ワン!'
  def fetch(item)
    "#{name}#{item}を取ってきた!"
  end
end

class GoldenRetriever < Dog
  def speak
    super + ' ワン!'  # 親のメソッドを呼ぶ
  end
end

# モジュール(Mixin)
module Swimmable
  def swim = "#{name}が泳いでいる"
end

module Runnable
  def run(speed: 'fast') = "#{name}#{speed}で走っている"
end

class Duck < Animal
  include Swimmable      # インスタンスメソッドを追加
  include Runnable
  def speak = 'クワッ!'
end

duck = Duck.new('ドナルド')
duck.swim   # 'ドナルドが泳いでいる'
duck.run    # 'ドナルドがfastで走っている'

# extend: クラスメソッドとして追加
module ClassMethods
  def create(attrs)
    new(attrs[:name])
  end
end
Duck.extend(ClassMethods)

# prepend: メソッドルックアップチェーンの前に挿入
module Logging
  def speak
    result = super
    puts "[LOG] speak called, returned: #{result}"
    result
  end
end
Dog.prepend(Logging)

# 祖先チェーン
Duck.ancestors
# [Duck, Runnable, Swimmable, Animal, Object, Kernel, BasicObject]

メタプログラミング

method_missing・define_method・open class・respond_to_missing?

metaprogramming.rb ruby
# オープンクラス(既存クラスの拡張)
class Integer
  def factorial
    return 1 if self <= 1
    self * (self - 1).factorial
  end

  def times_do
    i = 0
    while i < self
      yield i
      i += 1
    end
  end
end

5.factorial  # 120

# Refinements(スコープを絞ったオープンクラス)
module StringExtensions
  refine String do
    def palindrome?
      self == self.reverse
    end
  end
end

using StringExtensions  # このファイル内でのみ有効
'racecar'.palindrome?   # true

# define_method(動的メソッド定義)
class MyClass
  %w[foo bar baz].each do |name|
    define_method("say_#{name}") do
      puts name
    end
  end
end

MyClass.new.say_foo  # 'foo'

# method_missing(未定義メソッドの捕捉)
class DynamicFinder
  def method_missing(name, *args)
    if name.to_s.start_with?('find_by_')
      attr = name.to_s.sub('find_by_', '')
      "#{attr}=#{args.first}で検索"
    else
      super  # 処理できない場合は super を呼ぶ
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?('find_by_') || super
  end
end

finder = DynamicFinder.new
finder.find_by_name('Alice')   # 'name=Aliceで検索'
finder.respond_to?(:find_by_id) # true