💎

【Ruby 8日目】基本文法 - 継承とMix-in

に公開

はじめに

Rubyの継承とMix-inについて、Ruby 3.4の仕様に基づいて詳しく解説します。これらはオブジェクト指向プログラミングの核となる概念で、コードの再利用性と保守性を高めます。

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

基本概念

継承とMix-inの主な特徴:

  • 継承: クラス間でコードを共有する仕組み
  • Mix-in: モジュールを使って機能を追加する仕組み
  • 多重継承の代替: Rubyは単一継承だが、Mix-inで柔軟性を確保

Rubyでは継承とMix-inを組み合わせることで、柔軟で保守性の高い設計が可能です。

基本的な使い方

単純な継承

# 親クラス
class Vehicle
  attr_accessor :speed

  def initialize(name)
    @name = name
    @speed = 0
  end

  def start
    puts "#{@name}が起動しました"
  end

  def accelerate(amount)
    @speed += amount
    puts "現在の速度: #{@speed}km/h"
  end
end

# 子クラス
class Car < Vehicle
  def initialize(name, fuel_type)
    super(name)  # 親クラスのinitializeを呼ぶ
    @fuel_type = fuel_type
  end

  def honk
    puts "プップー!"
  end
end

class Bicycle < Vehicle
  def ring_bell
    puts "リンリン!"
  end
end

# 使用例
car = Car.new("セダン", "ガソリン")
car.start          # => セダンが起動しました
car.accelerate(50) # => 現在の速度: 50km/h
car.honk           # => プップー!

bike = Bicycle.new("ロードバイク")
bike.start          # => ロードバイクが起動しました
bike.ring_bell      # => リンリン!

superの使い方

class Animal
  def initialize(name)
    @name = name
  end

  def introduce
    "私は#{@name}です"
  end

  def speak
    "..."
  end
end

class Dog < Animal
  def initialize(name, breed)
    super(name)  # 親のinitializeを呼ぶ(引数あり)
    @breed = breed
  end

  def introduce
    "#{super}#{@breed}の犬です"  # 親のメソッドを呼んで結果を使う
  end

  def speak
    "ワンワン!"  # 親のメソッドをオーバーライド
  end
end

dog = Dog.new("ポチ", "柴犬")
puts dog.introduce  # => 私はポチです、柴犬の犬です
puts dog.speak      # => ワンワン!

Mix-in(include)

# モジュール定義
module Swimmable
  def swim
    "泳いでいます"
  end
end

module Flyable
  def fly
    "飛んでいます"
  end
end

# クラスに機能を追加
class Duck
  include Swimmable
  include Flyable

  def quack
    "ガーガー!"
  end
end

class Fish
  include Swimmable

  def breathe
    "えら呼吸"
  end
end

duck = Duck.new
puts duck.swim   # => 泳いでいます
puts duck.fly    # => 飛んでいます
puts duck.quack  # => ガーガー!

fish = Fish.new
puts fish.swim    # => 泳いでいます
# fish.fly        # => NoMethodError

extend(クラスメソッドとして追加)

module Countable
  def count
    @count ||= 0
  end

  def increment
    @count ||= 0
    @count += 1
  end
end

class User
  extend Countable  # クラスメソッドとして追加
end

User.increment
User.increment
puts User.count  # => 2

prepend(メソッドチェーンの先頭に挿入)

module Logging
  def save
    puts "[LOG] 保存処理開始"
    result = super  # 元のsaveを呼ぶ
    puts "[LOG] 保存処理完了"
    result
  end
end

class User
  prepend Logging  # メソッドチェーンの先頭に

  def save
    puts "ユーザーを保存中..."
    true
  end
end

user = User.new
user.save
# [LOG] 保存処理開始
# ユーザーを保存中...
# [LOG] 保存処理完了

よくあるユースケース

ケース1: 複数の共通機能を持つクラス群

# 共通機能をモジュール化
module Timestampable
  attr_reader :created_at, :updated_at

  def initialize(*args, **kwargs)
    super
    @created_at = Time.now
    @updated_at = Time.now
  end

  def touch
    @updated_at = Time.now
  end
end

module Publishable
  def publish
    @published_at = Time.now
    @status = :published
  end

  def unpublish
    @status = :draft
  end

  def published?
    @status == :published
  end
end

module Taggable
  def add_tag(tag)
    @tags ||= []
    @tags << tag unless @tags.include?(tag)
  end

  def tags
    @tags || []
  end
end

# 記事クラス
class Article
  include Timestampable
  include Publishable
  include Taggable

  attr_accessor :title, :body

  def initialize(title:, body:)
    @title = title
    @body = body
    @status = :draft
    @tags = []
    super()
  end
end

# 動画クラス
class Video
  include Timestampable
  include Publishable

  attr_accessor :title, :url

  def initialize(title:, url:)
    @title = title
    @url = url
    @status = :draft
    super()
  end
end

# 使用例
article = Article.new(title: "Ruby入門", body: "Rubyの基本...")
article.add_tag("プログラミング")
article.add_tag("Ruby")
article.publish

puts article.created_at
puts article.published?  # => true
puts article.tags        # => ["プログラミング", "Ruby"]

ケース2: テンプレートメソッドパターン

# 抽象基底クラス
class DataExporter
  def export(data)
    validate_data(data)
    formatted = format_data(data)
    output = write_header + formatted + write_footer
    save_output(output)
  end

  private

  def validate_data(data)
    raise ArgumentError, "データが空です" if data.nil? || data.empty?
  end

  def format_data(data)
    raise NotImplementedError, "サブクラスで実装してください"
  end

  def write_header
    ""
  end

  def write_footer
    ""
  end

  def save_output(output)
    puts output
  end
end

# CSV形式
class CsvExporter < DataExporter
  private

  def format_data(data)
    data.map { |row| row.join(",") }.join("\n")
  end

  def write_header
    "# CSV Export\n"
  end
end

# JSON形式
class JsonExporter < DataExporter
  private

  def format_data(data)
    require 'json'
    data.to_json
  end

  def write_header
    "{\n  \"data\": "
  end

  def write_footer
    "\n}"
  end
end

# 使用例
data = [
  ["名前", "年齢"],
  ["Alice", 30],
  ["Bob", 25]
]

csv = CsvExporter.new
csv.export(data)
# # CSV Export
# 名前,年齢
# Alice,30
# Bob,25

json = JsonExporter.new
json.export([{ name: "Alice", age: 30 }])
# {
#   "data": [{"name":"Alice","age":30}]
# }

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

module Cacheable
  def find(id)
    @cache ||= {}

    if @cache.key?(id)
      puts "キャッシュから取得: #{id}"
      @cache[id]
    else
      result = super
      @cache[id] = result
      result
    end
  end

  def clear_cache
    @cache = {}
  end
end

module Loggable
  def find(id)
    puts "[LOG] 検索開始: ID=#{id}"
    result = super
    puts "[LOG] 検索完了: #{result}"
    result
  end
end

class User
  prepend Cacheable  # キャッシュ機能を追加
  prepend Loggable   # ログ機能を追加

  @@users = {
    1 => { id: 1, name: "Alice" },
    2 => { id: 2, name: "Bob" }
  }

  def self.find(id)
    puts "データベースから取得: #{id}"
    @@users[id]
  end
end

# 使用例
User.find(1)  # ログ → データベース → キャッシュに保存
User.find(1)  # ログ → キャッシュから取得(高速)

ケース4: ストラテジーパターン

# 決済方法の基底クラス
class PaymentStrategy
  def pay(amount)
    raise NotImplementedError
  end
end

# 具体的な決済方法
class CreditCardPayment < PaymentStrategy
  def initialize(card_number)
    @card_number = card_number
  end

  def pay(amount)
    "クレジットカード(****#{@card_number[-4..]})で#{amount}円を決済"
  end
end

class CashPayment < PaymentStrategy
  def pay(amount)
    "現金で#{amount}円を決済"
  end
end

class DigitalWalletPayment < PaymentStrategy
  def initialize(wallet_id)
    @wallet_id = wallet_id
  end

  def pay(amount)
    "デジタルウォレット(#{@wallet_id})で#{amount}円を決済"
  end
end

# 注文クラス
class Order
  attr_reader :amount

  def initialize(amount)
    @amount = amount
  end

  def checkout(payment_strategy)
    puts payment_strategy.pay(amount)
  end
end

# 使用例
order = Order.new(5000)

order.checkout(CreditCardPayment.new("1234567812345678"))
# => クレジットカード(****5678)で5000円を決済

order.checkout(CashPayment.new)
# => 現金で5000円を決済

order.checkout(DigitalWalletPayment.new("USER_123"))
# => デジタルウォレット(USER_123)で5000円を決済

メソッド探索順序(Method Lookup)

module M1
  def greet
    "M1"
  end
end

module M2
  def greet
    "M2"
  end
end

class Parent
  def greet
    "Parent"
  end
end

class Child < Parent
  prepend M1
  include M2

  def greet
    "Child"
  end
end

child = Child.new
puts child.greet  # => "M1"

# メソッド探索順序を確認
puts Child.ancestors
# => [M1, Child, M2, Parent, Object, Kernel, BasicObject]

# 探索順序:
# 1. prepend されたモジュール (M1)
# 2. クラス自身 (Child)
# 3. include されたモジュール (M2)
# 4. 親クラス (Parent)
# 5. ... (Object, Kernel, BasicObject)

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

注意点

  1. 深い継承階層は避ける
# BAD: 5階層以上の継承
class A
end
class B < A
end
class C < B
end
class D < C
end
class E < D
end

# GOOD: 浅い継承 + モジュール
class Base
end

module Feature1
end
module Feature2
end

class MyClass < Base
  include Feature1
  include Feature2
end
  1. Mix-inの順序に注意
module A
  def method
    "A"
  end
end

module B
  def method
    "B"
  end
end

class MyClass
  include A
  include B  # 後から include した方が優先される
end

puts MyClass.new.method  # => "B"
  1. 継承 vs コンポジション
# BAD: is-a関係でない継承
class Stack < Array
  # スタックは配列ではない
end

# GOOD: コンポジション
class Stack
  def initialize
    @items = []
  end

  def push(item)
    @items.push(item)
  end

  def pop
    @items.pop
  end
end

ベストプラクティス

  1. リスコフの置換原則
# 親クラスのインスタンスを子クラスで置き換え可能に
class Bird
  def fly
    "飛びます"
  end
end

# BAD: ペンギンは飛べない(原則違反)
class Penguin < Bird
  def fly
    raise "ペンギンは飛べません"
  end
end

# GOOD: 飛べる鳥と飛べない鳥を分ける
module Flyable
  def fly
    "飛びます"
  end
end

class Sparrow < Bird
  include Flyable
end

class Penguin < Bird
  def swim
    "泳ぎます"
  end
end
  1. インターフェース分離
# GOOD: 小さなモジュールに分割
module Readable
  def read
  end
end

module Writable
  def write(data)
  end
end

module Executable
  def execute
  end
end

class File
  include Readable
  include Writable
end

class Script
  include Readable
  include Executable
end

Ruby 3.4での改善点

  • YJIT最適化: 継承チェーンの探索が高速化
  • メモリ効率: モジュールのインクルードが効率化
  • prependの改善: prependされたモジュールのパフォーマンスが向上

まとめ

この記事では、継承とMix-inについて以下の内容を学びました:

  • 継承の基本とsuperの使い方
  • Mix-in(include、extend、prepend)の違い
  • 実践的なデザインパターン
  • メソッド探索順序の理解
  • 注意点とベストプラクティス

継承とMix-inを適切に使い分けることで、柔軟で保守性の高いコードを書けるようになります。

参考資料

Discussion