💎
【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)
注意点とベストプラクティス
注意点
- 深い継承階層は避ける
# 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
- 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"
- 継承 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
ベストプラクティス
- リスコフの置換原則
# 親クラスのインスタンスを子クラスで置き換え可能に
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
- インターフェース分離
# 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