💎

Ruby基礎・クラスとオブジェクト

に公開

クラスとオブジェクト

クラスは内部にデータを保持し、保持しているデータを利用する独自のメソッドを持つ
保持するデータを定義し、保持するデータに対するメソッドを定義できるため、勝手にデータを追加したり、勝手にメソッドで操作できないように定義

  • データとメソッドの関連付け:関連するデータと処理を一箇所にまとめられる
  • カプセル化:データの内部表現を隠蔽し、外部からの不正なアクセスを防ぐ
  • 再利用性:同じ設計のオブジェクトを何度でも作成
  • 保守性:コードの修正箇所が明確になり、メンテナンスが容易

関連の用語

オブジェクト指向プログラミングでは特有の用語がある

クラス

クラスはオブジェクトの設計図(テンプレート)であり、オブジェクトが持つべきデータ(インスタンス変数)と動作(メソッド)を定義

# クラスの確認
"hello".class  # => String
123.class      # => Integer
[1, 2, 3].class # => Array

# クラス自身もオブジェクト
String.class   # => Class

オブジェクト・インスタンス・レシーバー

クラスをもとに作られたデータの塊のこと同じ概念を指すが、文脈によって呼び方が変わる

用語の使い分け:

  • オブジェクト:最も一般的な呼び方・Rubyでは全てがオブジェクト
  • インスタンス:クラスとの関係を強調する際に使用・Userクラスのインスタンスのように表現
  • レシーバー:メソッド呼び出しの文脈で使用・メソッドを呼び出された側(受信側)というニュアンス
# オブジェクト/インスタンスの作成
user = User.new("太郎")

# この時:
# - userは「オブジェクト」である
# - userは「Userクラスのインスタンス」である
# - user.nameを呼ぶ時、userは「レシーバー」である

user.name  # userがレシーバー、nameがメソッド(メッセージ)

メソッド・メッセージ

オブジェクトが持つ動作や振る舞いを定義したもの

メソッド:オブジェクトが持つ動作や振る舞い
何かしらの処理をひとまとめにして名前をつけ、何度も利用できるようにしたもの

メッセージ:レシーバーに送られるメソッド呼び出しのこと
オブジェクト指向の考え方では「メソッドを呼び出す」ことを「メッセージを送る」と表現

メソッドとメッセージの関係:

user.greet("太郎")

# この時:
# - userはレシーバー(メッセージの受信者)
# - greetはメッセージ(メソッド名)
# - "太郎"は引数
# つまり「userオブジェクトにgreetというメッセージを"太郎"という引数付きで送っている」

状態・ステート

オブジェクトが内部に保持するデータのこと
状態(ステート):オブジェクトごとに保管されるデータのこと・インスタンス変数で表現

  • 同じクラスから作られたオブジェクトでも、状態(データ)は異なる
  • 状態はインスタンス変数(@で始まる変数)で保持される
  • オブジェクトの状態はメソッドによって変更できる
# 例: 2つのUserオブジェクトは異なる状態を持つ
user1 = User.new("太郎", 25)  # 名前="太郎", 年齢=25 という状態
user2 = User.new("花子", 30)  # 名前="花子", 年齢=30 という状態

属性・アトリビュート・プロパティ

オブジェクトの状態を外部から読み書きできる値のこと
内部の状態(インスタンス変数)へのインターフェースを提供する

属性(アトリビュート/プロパティ):オブジェクトから取得できる値のこと

  • 属性は通常、ゲッターメソッド・セッターメソッドを通じて公開される
  • 内部のインスタンス変数(@name)と外部の属性(name)は必ずしも一致しない
  • attr_accessor、attr_reader、attr_writerで簡単に定義できる
class User
  attr_accessor :name  # name属性を定義(読み書き可能)
  attr_reader :age     # age属性を定義(読み取り専用)

  def initialize(name, age)
    @name = name  # インスタンス変数
    @age = age
  end
end

user = User.new("太郎", 25)
user.name           # => "太郎" (属性の取得)
user.name = "次郎"  # 属性の変更

クラスの定義

Rubyでクラスを定義するにはclassキーワードを使用

命名規則:

  • クラス名は大文字で始める(必須)
  • スネークケースではなく、キャメルケース(UpperCamelCase)で記述する
  • 複数の単語を組み合わせる場合、各単語の先頭を大文字にする
class クラス名
  # クラスの内容をここに記述
end

# 良い例
class User
end

class UserAccount
end

class ShoppingCart
end

# 悪い例
class user          # 小文字で始まっている(エラーになる)
end

class User_Account  # スネークケースを使っている(動くが推奨されない)
end

オブジェクト作成とinitializeメソッド

クラスからオブジェクトを作成する際はnewメソッドを使う
この際に自動的に呼ばれるのがinitializeメソッド

# オブジェクトの作成
user = User.new

# 引数付きでの作成
user = User.new("太郎", 25)

initialize 動作の流れ

class User
  def initialize(name)
    puts "initializeメソッドが呼ばれました"
    @name = name
  end
end

# User.newを実行すると...
user = User.new("太郎")
# => "initializeメソッドが呼ばれました"

# 1. Userクラスの新しいオブジェクトがメモリ上に作成される
# 2. そのオブジェクトのinitializeメソッドが自動的に呼ばれる
# 3. initializeメソッドで初期化処理が実行される
# 4. 初期化されたオブジェクトが返される

initializeメソッド

オブジェクト作成時の初期状態を設定する役割を持つ

  • インスタンス変数の初期化を一箇所で管理できる
  • オブジェクトの必要な初期値「最低限必要なデータ」を確実に設定できる
  • initializeはnewメソッドによって自動的に呼び出され、外部から直接呼び出すことはできない
class Book
  attr_accessor :title, :author, :pages

  # initializeメソッド: newメソッド呼び出し時に自動実行
  def initialize(title, author, pages)
    @title = title   # インスタンス変数に初期値を設定
    @author = author
    @pages = pages
  end
end

# オブジェクト作成と初期化
book1 = Book.new("Harry Potter", "JK Rowling", 400)
book2 = Book.new("Lord of the Rings", "Tolkien", 500)

puts book1.title   # => "Harry Potter"
puts book2.author  # => "Tolkien"
  • オブジェクト作成時に属性を設定できる
  • @記号でインスタンス変数を表す
  • newメソッド呼び出し時にinitializeが自動実行される

クラス内のメソッド定義

インスタンスメソッドの定義

各オブジェクト(インスタンス)が持つ動作や振る舞いを定義するメソッド
クラスの内部でメソッドを定義するとインスタンスメソッド

class Student
  attr_accessor :name, :major, :gpa

  def initialize(name, major, gpa)
    @name = name
    @major = major
    @gpa = gpa
  end

  # インスタンスメソッド: オブジェクトの状態(@gpa)を使って判定
  def has_honors
    if @gpa >= 3.5
      return true
    end
    return false
  end

  # より簡潔な書き方
  def has_honors?  # 真偽値を返すメソッドには?をつける慣習
    @gpa >= 3.5
  end

  # 自己紹介メソッド: 複数のインスタンス変数を使用
  def introduce
    "私は#{@name}です。#{@major}を専攻しています。"
  end
end

student1 = Student.new("Jim", "Business", 2.6)
student2 = Student.new("Pam", "Art", 3.6)

puts student1.has_honors?  # => false
puts student2.has_honors?  # => true
puts student2.introduce    # => "私はPamです。Artを専攻しています。"
  • メソッドでオブジェクトの属性(インスタンス変数)にアクセスできる
  • @変数でインスタンス変数を参照する
  • 同じクラスのオブジェクトでも、状態が違えば異なる結果を返す

インスタンス変数とアクセサメソッド

オブジェクトの状態を保持するインスタンス変数
インスタンス変数とは同じインスタンス(同じオブジェクト)の内部で共有される変数

インスタンス変数を外部から読み書きするためのアクセサメソッド
アクセサメソッド:ゲッターメソッドとセッターメソッドを総称

ゲッターメソッド:インスタンス変数はクラスの外部から参照するためのメソッド
セッターメソッド:インスタンス変数を外部から変更するための変更用メソッド

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

  def hello
    "I am #{@name}"
  end

  # ゲッターメソッド: 値を取得する
  def name
    @name
  end

  # セッターメソッド: 値を設定する
  def name=(value)
    @name = value
  end
end

user = User.new("test")
user.hello      # => "I am test"
user.name       # => "test" (ゲッターを使用)
user.name = "新しい名前"  # セッターを使用
user.hello      # => "I am 新しい名前"

attr_accessor, attr_reader, attr_writerの使い分け

インスタンス変数の内容を外部から読み書きするのであれば、アクセサメソッドを簡単に定義

  • attr_accessor: 通常の属性で、読み書き両方が必要な場合
  • attr_reader: 一度設定したら変更されたくない属性(ID、作成日時など)
  • attr_writer: ほとんど使わない(パスワードなど特殊なケースのみ)
class User
  # 読み書き両方を許可
  attr_accessor :email

  # 読み取り専用(書き込み不可)
  attr_reader :id

  # 書き込み専用(読み取り不可) ※実際にはあまり使われない
  attr_writer :password

  def initialize(id, email)
    @id = id        # IDは読み取り専用にする(変更されたくない)
    @email = email  # メールアドレスは変更可能
  end

  # パスワードは書き込み専用なので、直接読み取れない
  def check_password(input)
    @password == input  # 検証メソッド経由で比較のみ可能
  end
end

user = User.new(1, "test@example.com")

# attr_reader (読み取り専用)
puts user.id        # => 1
# user.id = 2       # エラー! attr_readerなので書き込みできない

# attr_accessor (読み書き可能)
puts user.email           # => "test@example.com"
user.email = "new@example.com"  # 書き込み可能
puts user.email           # => "new@example.com"

# attr_writer (書き込み専用)
user.password = "secret123"  # 書き込みは可能
# puts user.password         # エラー! attr_writerなので読み取りできない
user.check_password("secret123")  # => true (メソッド経由なら確認可能)


class Product
  attr_reader :name, :price

  def initialize(name, price)
    @name = name
    self.price = price  # セッターメソッドを使う(バリデーション適用)
  end

  # カスタムセッター: 負の価格を防ぐ
  def price=(value)
    if value < 0
      raise ArgumentError, "価格は0以上でなければなりません"
    end
    @price = value
  end
end

product = Product.new("ノートPC", 100000)
puts product.price  # => 100000

product.price = 80000  # OK
# product.price = -100 # エラー! ArgumentError: 価格は0以上でなければなりません

クラスメソッドの定義

クラス自体に属するメソッドで、インスタンスを作成せずに呼び出せる
インスタンスの状態に依存しない処理を実装する際に使用

インスタンスメソッドとの違い

  • インスタンスメソッド: 各オブジェクト(インスタンス)に対して呼び出す、インスタンス変数(@変数)にアクセスできる
  • クラスメソッド: クラス自体に対して呼び出す、インスタンス変数にはアクセスできない

クラスメソッドを使う場面
クラスに関連は深いが、各インスタンスに含まれるデータは使わないメソッドの場合、クラスメソッドの方が使い勝手が良くなる

class User
  # 方法1: def self.メソッド名
  def self.create_guest
    new("ゲスト", "guest@example.com")
  end

  # 方法2: class << self 構文
  class << self
    def count
      # データベースから全ユーザー数を取得するなど
      100
    end
  end

  def initialize(name, email)
    @name = name
    @email = email
  end
end

# 呼び出し: クラス名.クラスメソッド名
guest = User.create_guest  # インスタンスを作らずに呼び出せる
total = User.count         # => 100

#実用的な例:
class Temperature
  attr_reader :celsius

  def initialize(celsius)
    @celsius = celsius
  end

  # クラスメソッド: 華氏から摂氏への変換
  def self.from_fahrenheit(fahrenheit)
    celsius = (fahrenheit - 32) * 5.0 / 9.0
    new(celsius)  # Temperatureオブジェクトを返す
  end

  # クラスメソッド: ケルビンから摂氏への変換
  def self.from_kelvin(kelvin)
    celsius = kelvin - 273.15
    new(celsius)
  end

  # インスタンスメソッド: 華氏で取得
  def to_fahrenheit
    @celsius * 9.0 / 5.0 + 32
  end
end

# クラスメソッドを使ってオブジェクト作成
temp1 = Temperature.new(25)              # 直接摂氏で作成
temp2 = Temperature.from_fahrenheit(77)  # 華氏から作成
temp3 = Temperature.from_kelvin(298.15)  # ケルビンから作成

puts temp2.celsius        # => 25.0
puts temp1.to_fahrenheit  # => 77.0 (インスタンスメソッド)

クラスの定義応用

定数

クラス内で変更されることのない値を定義する際に使用
定数名は大文字で始め、アルファベット大文字と数字とアンダースコアで構成

  • 定数名は大文字で始める(慣習的に全て大文字で書くことが多い)
  • クラス外部からクラス名::定数名でアクセスできる
  • 再代入すると警告が出るが、実際には変更可能(注意が必要)
  • クラス全体で共有される値を保持するのに適している

実用的な例:

class Order
  # 配送料の定数
  SHIPPING_FEE = 500
  FREE_SHIPPING_THRESHOLD = 3000

  # ステータスの定数
  STATUS_PENDING = "pending"
  STATUS_CONFIRMED = "confirmed"
  STATUS_SHIPPED = "shipped"
  STATUS_DELIVERED = "delivered"

  attr_accessor :total_amount, :status

  def initialize(total_amount)
    @total_amount = total_amount
    @status = STATUS_PENDING
  end

  def shipping_fee
    if @total_amount >= FREE_SHIPPING_THRESHOLD
      0
    else
      SHIPPING_FEE
    end
  end

  def final_amount
    @total_amount + shipping_fee
  end
end

order = Order.new(2500)
puts order.shipping_fee   # => 500 (3000円未満なので送料あり)
puts order.final_amount   # => 3000

# 外部から定数を参照
puts Order::SHIPPING_FEE              # => 500
puts Order::FREE_SHIPPING_THRESHOLD   # => 3000

メモ化・遅延初期化

計算コストが高い処理の結果をキャッシュして再利用する技法
nilガード(||=演算子)をクラスの内部で使われることがある

メモ化(Memoization)とは:

  • 一度計算した結果を保存(キャッシュ)して、次回以降は保存した値を返す技法
  • 計算コストが高い処理を何度も実行しないで済む

遅延初期化(Lazy Initialization)とは:

  • 実際に値が必要になるまで初期化を遅らせる技法
  • 不要な処理を避けてパフォーマンスを向上させる
class User
  attr_reader :name

  def initialize(name)
    @name = name
  end

  # メモ化の例: 一度計算したら結果を保存
  def profile
    # @profileがnilの場合のみ、重い処理を実行
    @profile ||= generate_profile
  end

  private

  def generate_profile
    puts "プロフィールを生成中..." # 重い処理をシミュレート
    "#{@name}のプロフィール"
  end
end

user = User.new("太郎")
puts user.profile  # => "プロフィールを生成中..." → "太郎のプロフィール"
puts user.profile  # => "太郎のプロフィール" (2回目は生成処理をスキップ)

注意点:

class Counter
  def count
    @count ||= 0
    @count += 1
  end
end

counter = Counter.new
puts counter.count  # => 1
puts counter.count  # => 2
puts counter.count  # => 3

# ||= は初回nilの時だけ代入されるが、
# その後の += は毎回実行される

selfキーワード

コンテキストに応じて「自分自身」を参照する特別なキーワード
インスタンスメソッド内ではインスタンス自身を、クラスメソッド定義ではクラス自身を指す

selfの基本:
インスタンス自身を指すselfキーワードがある
メソッドの内部で他のメソッドを呼び出す際は暗黙的にselfを呼び出している
selfは省略可能だが、明示的にselfをメソッドにつけても問題はない

  1. インスタンスメソッド内のself: そのインスタンス自身を指す
class User
  attr_accessor :name, :email

  def initialize(name, email)
    @name = name
    @email = email
  end

  def introduce
    # selfは省略可能(暗黙的にselfが使われている)
    puts "私の名前は#{name}です"  # self.nameと同じ
    puts "メールは#{self.email}です"  # 明示的にselfをつけても良い
  end

  def myself
    self  # このインスタンス自身を返す
  end
end

user = User.new("太郎", "taro@example.com")
user.introduce
puts user.myself == user  # => true (selfはuserインスタンス自身)
  1. クラスメソッドの定義: クラス自身を指す
class Product
  # def self.メソッド名 の self はProductクラス自身を指す
  def self.category
    "商品"
  end

  class << self  # self = Productクラス
    def all
      "全商品を取得"
    end
  end
end
  1. クラス定義のトップレベル: クラス自身を指す
class Config
  puts self  # => Config (クラス自身)

  # クラス変数の初期化などで使える
  @@instance_count = 0

  def initialize
    @@instance_count += 1
  end

  def self.instance_count
    @@instance_count
  end
end

ローカル変数との違い

class Rectangle
  attr_accessor :width, :height

  def initialize(width, height)
    @width = width
    @height = height
  end

  def resize(new_width, new_height)
    # selfなしの場合 - ローカル変数への代入
    width = new_width   # ローカル変数widthに代入(@widthは変わらない)
    height = new_height # ローカル変数heightに代入(@heightは変わらない)

    puts "ローカル変数 width: #{width}"   # => new_widthの値
    puts "インスタンス変数 @width: #{@width}"  # => 元の値のまま
  end

  def resize_correctly(new_width, new_height)
    # selfありの場合 - セッターメソッドの呼び出し
    self.width = new_width   # @widthが更新される
    self.height = new_height # @heightが更新される
  end
end

rect = Rectangle.new(10, 20)
rect.resize(30, 40)
puts rect.width  # => 10 (変わっていない!)

rect.resize_correctly(30, 40)
puts rect.width  # => 30 (正しく更新された)

クラスメソッドをインスタンスメソッドで呼び出す場合:

class User
  def self.total_count
    100  # 全ユーザー数
  end

  def initialize(name)
    @name = name
  end

  def show_total
    # 方法1: クラス名を直接指定
    puts "総ユーザー数: #{User.total_count}"

    # 方法2: self.classを使う(より汎用的)
    puts "総ユーザー数: #{self.class.total_count}"
  end
end

user = User.new("太郎")
user.show_total

# self.classを使う利点: 継承時に柔軟
class AdminUser < User
  def self.total_count
    10  # 管理者ユーザー数
  end
end

admin = AdminUser.new("管理者")
admin.show_total  # self.classを使えばAdminUser.total_countが呼ばれる
  • インスタンスメソッド内: インスタンス自身を指す
  • クラスメソッド定義: クラス自身を指す
  • セッターメソッド呼び出し: selfを明示する必要がある(ローカル変数との区別のため)
  • 他のメソッド呼び出し: 通常は省略可能(暗黙的にselfが使われる)

クラスの継承

既存のクラス(親クラス)の機能を引き継いで新しいクラス(子クラス)を作成する仕組み
スーパークラス(親クラス)を継承するサブクラス(子クラス)がある

正しいかどうかは「サブクラスはスーパークラスの一種」(サブ is-a スーパー)で判断する

  • サブクラスはスーパークラスを特化(具体化)したもの
  • スーパークラスはサブクラスを汎化(抽象化)したもの

継承の利点:

  • コードの再利用: 共通の機能をスーパークラスにまとめられる
  • 保守性の向上: 共通処理の修正が一箇所で済む
  • 拡張性: 既存のクラスを変更せずに機能を追加できる
  • ポリモーフィズム: 同じインターフェースで異なる実装を提供できる
# 良い例: 犬は動物の一種 (Dog is-a Animal)
class Animal
end

class Dog < Animal  # 犬は動物である → 継承が適切
end

# 悪い例: 車はエンジンの一種ではない
class Engine
end

# class Car < Engine  # 車はエンジンである → 間違い!
# end

# 正しい例: 車はエンジンを持つ (has-a関係)
class Car
  def initialize
    @engine = Engine.new  # コンポジション(委譲)を使う
  end
end

標準ライブラリの継承関係

Rubyの全てのクラスは階層構造を持ち、最終的にBasicObjectクラスに辿り着く
この仕組みにより、全てのオブジェクトが共通の基本メソッドを持つ

継承関係の頂点にいるのがBasicObjectクラス

  • BasicObject: 最も基本的な機能のみを持つ(ほとんど何もない)
  • Object: BasicObjectを継承し、通常必要な基本メソッドを提供(puts, print, class, is_a?など)
# 文字列オブジェクトのクラス階層を確認
s = "foobar"         # 文字列リテラル
s.class              # => String
s.class.superclass   # => Object

# クラス階層を辿る
String.superclass                    # => Object
Object.superclass                    # => BasicObject
BasicObject.superclass               # => nil (頂点に到達)

# 他のクラスも同様
Array.superclass       # => Object
Hash.superclass        # => Object
Integer.superclass     # => Numeric
Numeric.superclass     # => Object

クラスを継承したクラスを作る

<演算子を使って既存のクラスを継承した新しいクラスを定義できる
継承したクラスは親クラスの全てのメソッドとインスタンス変数を引き継ぐ

基本構文:

class ChildClass < ParentClass  # ChildClassがParentClassを継承
end

実践的な例:

# 親クラス: 動物
class Animal
  attr_accessor :name, :age

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

  def speak
    "何か音を出す"
  end

  def sleep
    "#{@name}が眠っています"
  end
end

# 子クラス: 犬 (動物を継承)
class Dog < Animal
  attr_accessor :breed  # 犬固有の属性

  def initialize(name, age, breed)
    super(name, age)  # 親クラスのinitializeを呼ぶ
    @breed = breed
  end

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

  # 犬固有のメソッド
  def fetch
    "#{@name}がボールを取ってきた"
  end
end

# 使用例
dog = Dog.new("ポチ", 3, "柴犬")
puts dog.name       # => "ポチ" (親から継承した属性)
puts dog.speak      # => "ワンワン!" (オーバーライドしたメソッド)
puts dog.sleep      # => "ポチが眠っています" (親から継承したメソッド)
puts dog.fetch      # => "ポチがボールを取ってきた" (犬固有のメソッド)
puts dog.breed      # => "柴犬" (犬固有の属性)

スーパークラスのメソッドを呼び出す

superを使うとスーパークラスの同名メソッドを呼び出せる
子クラスで処理を追加しつつ、親クラスの処理も実行したい場合に便利
スーパークラスとサブクラスで実行する処理が変わらないのであれば、定義やsuperを呼ばなくても自動的に処理される

superの使い方:

class Vehicle
  attr_accessor :brand, :model

  def initialize(brand, model)
    @brand = brand
    @model = model
  end

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

class Car < Vehicle
  attr_accessor :doors

  def initialize(brand, model, doors)
    super(brand, model)  # 親クラスのinitializeを呼ぶ(引数を渡す)
    @doors = doors       # 子クラス固有の初期化
  end

  def info
    # 親クラスのinfoを呼び出して、追加情報を付け加える
    "#{super} (#{@doors}ドア)"
  end
end

car = Car.new("Toyota", "Corolla", 4)
puts car.info  # => "Toyota Corolla (4ドア)"

メソッドのオーバーライド

サブクラスはスーパークラスと同名のメソッドを定義することで処理を上書きすることができる
これをメソッドのオーバーライド(override)と呼ぶ

class Animal
  def speak
    "何か音を出す"
  end
end

class Dog < Animal
  # speakメソッドをオーバーライド(完全に上書き、superは呼ばない)
  def speak
    "ワンワン!"
  end
end

class Cat < Animal
  # speakメソッドをオーバーライド(親の処理も含める)
  def speak
    "#{super}... ニャー"
  end
end

dog = Dog.new
puts dog.speak  # => "ワンワン!" (親のメソッドは使わない)

cat = Cat.new
puts cat.speak  # => "何か音を出す... ニャー" (親のメソッドも使う)

クラスメソッドの継承

クラスを継承するとクラスメソッドも継承されるが、活用することは少ない

class Parent
  def self.company_name
    "親会社"
  end
end

class Child < Parent
  # クラスメソッドもオーバーライドできる
  def self.company_name
    "子会社 (親: #{super})"
  end
end

puts Parent.company_name  # => "親会社"
puts Child.company_name   # => "子会社 (親: 親会社)"

メソッドの可視性

メソッドのアクセス制御により、クラスの外部からのアクセスを制限できる
カプセル化を実現し、クラスの内部実装を隠蔽する重要な機能

  • public: どこからでも呼び出せる
  • private: クラス内部からのみ呼び出せる
  • protected: クラスとサブクラスから呼び出せる

public(公開メソッド):
publicメソッドはクラスの外部からでも自由に呼び出せるメソッド
特に指定しない場合、全てのメソッドはpublicになる(initializeを除く)

private(非公開メソッド):
privateメソッドは外部に公開されないメソッド
クラス外からは呼び出せず、内部のみで使える

protected(制限付き公開メソッド):
protectedメソッドはメソッドを定義したクラス自身とサブクラスのインスタンスメソッドからレシーバー付きで呼び出せる

class BankAccount
  def initialize(balance)
    @balance = balance
  end

  # public メソッド(明示的に書く必要はない)
  public

  def deposit(amount)
    @balance += amount
    log_transaction("入金", amount)
  end

  def withdraw(amount)
    if can_withdraw?(amount)  # privateメソッドを呼ぶ
      @balance -= amount
      log_transaction("出金", amount)
    else
      "残高不足です"
    end
  end

  def balance
    @balance
  end

  # private メソッド(クラス内部でのみ使用)
  private

  def can_withdraw?(amount)
    @balance >= amount
  end

  def log_transaction(type, amount)
    puts "[#{type}] #{amount}円"
  end
end

account = BankAccount.new(10000)
account.deposit(5000)     # => OK (publicメソッド)
puts account.balance      # => 15000 (publicメソッド)
account.withdraw(3000)    # => OK (publicメソッド)

# account.can_withdraw?(1000)  # エラー! privateメソッドは外部から呼べない
# account.log_transaction("test", 100)  # エラー! privateメソッド

定数(クラス外部からのアクセス)

クラス内で定義した定数は、クラスの外部から直接参照することも可能
::演算子を使ってアクセスする

外部からのアクセス方法:

クラス名::定数名

重要な注意点:
Rubyにおける定数は、絶対に変更できない値ではなく、変更しようと思えばいくらでも変更できる値であるが、変更時には警告が表示される

class Config
  VERSION = "1.0.0"
  MAX_USERS = 100
end

# 外部から定数にアクセス
puts Config::VERSION    # => "1.0.0"
puts Config::MAX_USERS  # => 100

# 定数を変更することは可能だが警告が出る
Config::VERSION = "2.0.0"
# => warning: already initialized constant Config::VERSION
# => warning: previous definition of VERSION was here

puts Config::VERSION  # => "2.0.0" (変更されている)

定数の freeze:
真に変更不可能にしたい場合はfreezeメソッドを使う

class Config
  ALLOWED_TYPES = ["admin", "user", "guest"].freeze

  def self.valid_type?(type)
    ALLOWED_TYPES.include?(type)
  end
end

# ALLOWED_TYPES.push("super_admin")  # エラー! freezeされているので変更不可

入れ子になったクラス定義

クラスの中にクラスを定義できる
クラス名の予期せぬ衝突を防ぐ名前空間(namespace)を作る際に使用される

基本構文:

class 外部クラス
  class 内部クラス
  end
end

名前空間の例:

# HTTPライブラリの名前空間
class HTTP
  # HTTP::Request クラス
  class Request
    def initialize(url)
      @url = url
    end

    def send
      "#{@url}にリクエストを送信"
    end
  end

  # HTTP::Response クラス
  class Response
    def initialize(status_code)
      @status_code = status_code
    end

    def success?
      @status_code == 200
    end
  end
end

# 使用例
request = HTTP::Request.new("https://example.com")
puts request.send  # => "https://example.comにリクエストを送信"

response = HTTP::Response.new(200)
puts response.success?  # => true

モジュール

複数のクラスで共通のメソッドを再利用したり、名前空間を作成したりするための仕組み
インスタンスを作成できず、継承もできない点がクラスと異なる

モジュールの定義

module モジュール名
  # メソッドの定義
end

ミックスイン - include

モジュールのメソッドをインスタンスメソッドとして追加する

module Greetable
  def greet
    "こんにちは"
  end
end

class User
  include Greetable  # モジュールをミックスイン
end

user = User.new
puts user.greet  # => "こんにちは"

ミックスイン - extend

モジュールのメソッドをクラスメソッドとして追加する

module Greetable
  def greet
    "こんにちは"
  end
end

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

puts User.greet  # => "こんにちは"

名前空間

モジュール内にクラスを定義して、クラス名の衝突を防ぐ

module App
  class User
    def initialize(name)
      @name = name
    end
  end
end

# モジュール名::クラス名でアクセス
user = App::User.new("太郎")

エラーハンドリング

プログラム実行中に発生するエラー(例外)を適切に処理する仕組み
例外が発生してもプログラムを停止せずに処理を継続できる

基本構文

begin
  # 例外が発生する可能性のある処理
  result = 10 / 0
rescue
  # 例外が発生した時の処理
  puts "エラーが発生しました"
end

特定の例外を捕捉:

begin
  result = 10 / 0
rescue ZeroDivisionError
  puts "0で割ることはできません"
rescue TypeError => e
  puts "型エラー: #{e.message}"
end

ensure

例外の有無に関わらず必ず実行される処理を定義

begin
  file = File.open("data.txt")
  # ファイル処理
rescue => e
  puts "エラー: #{e.message}"
ensure
  file.close if file  # 必ず実行される
end

主要な例外クラス

  • StandardError: 通常の例外の基底クラス
  • ZeroDivisionError: 0で除算した時
  • TypeError: 型が不正な時
  • ArgumentError: 引数が不正な時
  • NoMethodError: 存在しないメソッドを呼んだ時
  • NameError: 未定義の変数や定数を参照した時
  • RuntimeError: 一般的な実行時エラー

例外の発生

raiseメソッドで意図的に例外を発生させる

def withdraw(amount)
  raise ArgumentError, "金額は正の数でなければなりません" if amount <= 0
  # 処理
end

ベストプラクティス

  • 安易にrescueを使わない: 異常時はプログラムを停止して原因調査する
  • rescueしたら情報を残す: ログなどに記録して後で調査できるようにする
  • 例外の範囲を絞る: 必要最小限の範囲でrescueを使用する
  • 条件分岐を優先: 予測可能な場合はif文で対処する
  • テストを書く: 例外処理も動作確認する

参考資料

プロを目指す人のためのRuby入門[改訂2版⁠]⁠ 言語仕様からテスト駆動開発・デバッグ技法まで | 技術評論社
を非常に参考にしました。

Discussion