🔰

Ruby/Railsで学ぶオブジェクト指向入門 クラスとオブジェクトって何ぞや

に公開

はじめに

こんにちは!本記事は、Ruby/Railsを使ってオブジェクト指向プログラミングの基礎について記載しています。本記事はオブジェクト指向編の続きとなります。
https://zenn.dev/osakayakyu/articles/aeaee96101a053

クラス定義に関して、サンプルコードをもとに記載しております!!

2. クラスとオブジェクトの基礎

2.1 クラスの定義とオブジェクトの生成

Rubyでクラスを定義するにはclassキーワードを使います。クラス名は大文字で始める必要があります。

class Person
  # クラスの内容(メソッドや属性など)
end

クラスからオブジェクト(インスタンス)を生成するには、newメソッドを使います。

person = Person.new

より実用的な例を見てみましょう:

class User
  attr_accessor :name, :email
  
  def initialize(name, email)
    @name = name
    @email = email
  end
  
  def greeting
    "こんにちは、#{@name}です!"
  end
end

# オブジェクトの生成
user1 = User.new("田中", "tanaka@example.com")
user2 = User.new("佐藤", "sato@example.com")

# オブジェクトのメソッドとプロパティにアクセス
puts user1.name     # => 田中
puts user1.greeting # => こんにちは、田中です!

puts user2.name     # => 佐藤
puts user2.greeting # => こんにちは、佐藤です!

# 属性の変更
user1.name = "田中修正"
puts user1.greeting # => こんにちは、田中修正です!

2.2 フィールド(プロパティ)とメソッドの基本

クラスには主に2種類の構成要素があります:

  1. インスタンス変数(フィールド/プロパティ):オブジェクトのデータを保持します
  2. メソッド:オブジェクトの振る舞いや機能を定義します

インスタンス変数

Rubyでは、インスタンス変数は@で始まります。これらの変数はオブジェクトのインスタンスごとに独立した値を持ちます。

class Product
  def initialize(name, price)
    @name = name    # @nameはインスタンス変数
    @price = price  # @priceはインスタンス変数
  end
end

デフォルトでは、インスタンス変数は外部から直接アクセスできません。アクセスするには、ゲッターメソッドとセッターメソッドを定義するか、Rubyのattr_accessorattr_readerattr_writerといったマクロを使います。

class Product
  # name属性の読み書きメソッドを生成
  attr_accessor :name
  
  # price属性の読み取り専用メソッドを生成
  attr_reader :price
  
  def initialize(name, price)
    @name = name
    @price = price
  end
  
  # priceを変更するカスタムメソッド
  def price=(new_price)
    if new_price >= 0  # 負の価格は許可しない
      @price = new_price
    else
      puts "価格は0以上である必要があります"
    end
  end
end

product = Product.new("本", 1500)
puts product.name   # => 本
puts product.price  # => 1500

product.name = "参考書"
puts product.name   # => 参考書

product.price = 2000
puts product.price  # => 2000

product.price = -500  # => 価格は0以上である必要があります
puts product.price    # => 2000(変更されていない)

メソッド

メソッドはオブジェクトの振る舞いを定義します。Rubyでは、メソッドの最後に評価された式が自動的に戻り値になります(returnは省略可能です)。

class Calculator
  def add(a, b)
    a + b  # 最後の式が戻り値になる
  end
  
  def subtract(a, b)
    a - b
  end
  
  def multiply(a, b)
    return a * b  # returnを明示的に使うこともできる
  end
  
  def divide(a, b)
    if b == 0
      return "ゼロ除算はできません"
    end
    a / b.to_f  # 整数除算を避けるためにto_fで浮動小数点に変換
  end
end

calc = Calculator.new
puts calc.add(5, 3)       # => 8
puts calc.subtract(10, 4) # => 6
puts calc.multiply(2, 6)  # => 12
puts calc.divide(10, 2)   # => 5.0
puts calc.divide(10, 0)   # => ゼロ除算はできません

2.3 コンストラクタの役割

コンストラクタとは、オブジェクトが生成されるときに自動的に呼び出される特別なメソッドです。Rubyでは、コンストラクタはinitializeメソッドとして定義します。

コンストラクタの主な役割:

  • インスタンス変数の初期化
  • オブジェクト生成時に必要な処理の実行
class Book
  attr_reader :title, :author, :pages, :published_date
  
  def initialize(title, author, pages, published_date = nil)
    @title = title
    @author = author
    @pages = pages
    @published_date = published_date || Time.now  # 引数がnilの場合は現在時刻を使用
    
    puts "「#{@title}」の本が作成されました"  # オブジェクト生成時のメッセージ
  end
  
  def info
    info_str = "タイトル: #{@title}, 著者: #{@author}, ページ数: #{@pages}"
    if @published_date
      info_str += ", 出版日: #{@published_date.strftime('%Y-%m-%d')}"
    end
    info_str
  end
end

# コンストラクタに引数を渡してオブジェクトを生成
book1 = Book.new("Ruby入門", "山田太郎", 300)
# => 「Ruby入門」の本が作成されました

book2 = Book.new("Rails実践ガイド", "鈴木花子", 450, Time.new(2023, 5, 15))
# => 「Rails実践ガイド」の本が作成されました

puts book1.info
# => タイトル: Ruby入門, 著者: 山田太郎, ページ数: 300, 出版日: 2023-12-10

puts book2.info
# => タイトル: Rails実践ガイド, 著者: 鈴木花子, ページ数: 450, 出版日: 2023-05-15

コンストラクタに引数のデフォルト値を設定したり、条件分岐を入れたりすることで、オブジェクト生成時の柔軟性を高めることができます。

2.4 実践問題と解説(2問)

問題1: 銀行口座クラスの作成

銀行口座を表すクラスBankAccountを作成してください。以下の機能を実装します:

  • 口座番号、所有者名、残高を保持する
  • 入金できる(depositメソッド)
  • 出金できる(withdrawメソッド)- ただし残高が不足している場合は出金できない
  • 口座情報を表示できる(display_infoメソッド)
  • 別の口座に送金できる(transferメソッド)

解答

class BankAccount
  attr_reader :account_number, :owner_name, :balance
  
  def initialize(account_number, owner_name, initial_balance = 0)
    @account_number = account_number
    @owner_name = owner_name
    @balance = initial_balance
    puts "#{owner_name}さんの口座(#{account_number})が作成されました。初期残高: #{initial_balance}円"
  end
  
  def deposit(amount)
    if amount > 0
      @balance += amount
      puts "#{amount}円を入金しました。残高: #{@balance}円"
      return true
    else
      puts "入金額は0より大きい値を指定してください"
      return false
    end
  end
  
  def withdraw(amount)
    if amount <= 0
      puts "出金額は0より大きい値を指定してください"
      return false
    elsif amount > @balance
      puts "残高不足です。残高: #{@balance}円, 出金要求額: #{amount}円"
      return false
    else
      @balance -= amount
      puts "#{amount}円を出金しました。残高: #{@balance}円"
      return true
    end
  end
  
  def display_info
    puts "口座情報: #{@account_number}, 所有者: #{@owner_name}, 残高: #{@balance}円"
  end
  
  def transfer(target_account, amount)
    puts "#{target_account.owner_name}さんの口座(#{target_account.account_number})に#{amount}円送金します"
    
    if withdraw(amount)  # 自分の口座から出金
      if target_account.deposit(amount)  # 相手の口座に入金
        puts "送金が完了しました"
        return true
      else
        # 入金に失敗した場合、出金分を戻す
        @balance += amount
        puts "送金に失敗しました。出金分を返却します。残高: #{@balance}円"
        return false
      end
    else
      puts "出金できなかったため、送金に失敗しました"
      return false
    end
  end
end

# 使用例
account1 = BankAccount.new("1234-5678", "山田太郎", 10000)
account2 = BankAccount.new("8765-4321", "鈴木花子", 5000)

account1.display_info
account2.display_info

account1.deposit(5000)
account1.withdraw(2000)

# 残高不足の場合
account2.withdraw(10000)

# 送金のテスト
account1.transfer(account2, 3000)

account1.display_info
account2.display_info

解説

このBankAccountクラスでは、以下のように銀行口座の機能を実装しています:

  1. インスタンス変数:口座番号(@account_number)、所有者名(@owner_name)、残高(@balance)を保持します。
  2. コンストラクタ:口座の初期化を行い、初期残高のデフォルト値を0としています。
  3. 入金メソッド(deposit:金額が正の値であることを確認してから残高に加算します。
  4. 出金メソッド(withdraw:金額が正の値であり、残高が十分であることを確認してから残高から減算します。
  5. 情報表示メソッド(display_info:口座情報を表示します。
  6. 送金メソッド(transfer:自分の口座から出金して相手の口座に入金します。出金や入金に失敗した場合はエラーメッセージを表示します。

このクラスはカプセル化の原則に従い、口座残高(@balance)を直接変更できないようにしています。代わりに、depositwithdrawなどのメソッドを通じてのみ残高を変更できます。これにより、不正な操作(例:負の金額の入金)を防ぐことができます。

問題2: 商品在庫管理システムの作成

商品在庫を管理するシステムを作成してください。以下のクラスを実装します:

  1. Productクラス:商品を表す

    • 商品ID、名前、価格、カテゴリーを保持する
    • 商品情報を表示できる
  2. Inventoryクラス:商品在庫を管理する

    • 複数の商品とその在庫数を保持する
    • 商品を追加できる(add_productメソッド)
    • 在庫を増やせる(add_stockメソッド)
    • 在庫を減らせる(remove_stockメソッド)
    • 在庫リストを表示できる(display_inventoryメソッド)
    • カテゴリー別に在庫を表示できる(display_by_categoryメソッド)

解答

class Product
  attr_reader :id, :name, :price, :category
  
  def initialize(id, name, price, category)
    @id = id
    @name = name
    @price = price
    @category = category
  end
  
  def display_info
    puts "商品ID: #{@id}, 名前: #{@name}, 価格: #{@price}円, カテゴリー: #{@category}"
  end
  
  def price=(new_price)
    if new_price >= 0
      @price = new_price
      puts "#{@name}の価格を#{@price}円に更新しました"
    else
      puts "価格は0以上である必要があります"
    end
  end
end

class Inventory
  def initialize
    @stock = {}  # 商品IDをキー、在庫数を値とするハッシュ
    @products = {}  # 商品IDをキー、商品オブジェクトを値とするハッシュ
  end
  
  def add_product(product, initial_stock = 0)
    if @products[product.id]
      puts "ID: #{product.id}の商品は既に登録されています"
      return false
    else
      @products[product.id] = product
      @stock[product.id] = initial_stock
      puts "#{product.name}#{initial_stock}個追加しました"
      return true
    end
  end
  
  def add_stock(product_id, amount)
    if !@products[product_id]
      puts "ID: #{product_id}の商品は登録されていません"
      return false
    elsif amount <= 0
      puts "追加する在庫数は1以上である必要があります"
      return false
    else
      @stock[product_id] += amount
      puts "#{@products[product_id].name}の在庫を#{amount}個追加しました。現在の在庫: #{@stock[product_id]}個"
      return true
    end
  end
  
  def remove_stock(product_id, amount)
    if !@products[product_id]
      puts "ID: #{product_id}の商品は登録されていません"
      return false
    elsif amount <= 0
      puts "減らす在庫数は1以上である必要があります"
      return false
    elsif @stock[product_id] < amount
      puts "在庫が足りません。現在の在庫: #{@stock[product_id]}個, 要求数: #{amount}個"
      return false
    else
      @stock[product_id] -= amount
      puts "#{@products[product_id].name}の在庫を#{amount}個減らしました。現在の在庫: #{@stock[product_id]}個"
      return true
    end
  end
  
  def display_inventory
    puts "===== 在庫リスト ====="
    if @products.empty?
      puts "商品がありません"
    else
      @products.each do |id, product|
        puts "#{product.name}: #{@stock[id]}個"
        product.display_info
        puts "---------------------"
      end
    end
  end
  
  def display_by_category(category)
    puts "===== #{category}カテゴリーの在庫 ====="
    category_products = @products.values.select { |product| product.category == category }
    
    if category_products.empty?
      puts "#{category}カテゴリーの商品はありません"
    else
      category_products.each do |product|
        puts "#{product.name}: #{@stock[product.id]}個"
        product.display_info
        puts "---------------------"
      end
    end
  end
end

# 使用例
inventory = Inventory.new

# 商品の作成
laptop = Product.new(1, "ノートパソコン", 80000, "電子機器")
phone = Product.new(2, "スマートフォン", 60000, "電子機器")
desk = Product.new(3, "デスク", 15000, "家具")
chair = Product.new(4, "椅子", 8000, "家具")

# 在庫に商品を追加
inventory.add_product(laptop, 5)
inventory.add_product(phone, 10)
inventory.add_product(desk, 3)
inventory.add_product(chair, 7)

# 在庫の表示
inventory.display_inventory

# 在庫の追加と削減
inventory.add_stock(1, 3)
inventory.remove_stock(2, 2)

# 在庫不足の場合
inventory.remove_stock(3, 5)

# カテゴリー別の表示
inventory.display_by_category("電子機器")
inventory.display_by_category("家具")
inventory.display_by_category("食品")  # 存在しないカテゴリー

解説

この在庫管理システムは、ProductクラスとInventoryクラスの2つのクラスで構成されています:

  1. Productクラス

    • 商品の基本情報(ID、名前、価格、カテゴリー)を保持します。
    • display_infoメソッドで商品の詳細情報を表示します。
    • price=メソッドで価格を更新できますが、不正な値(負の値)は設定できないようにしています。
  2. Inventoryクラス

    • 商品とその在庫数を管理します。
    • 2つのハッシュを使用して情報を管理しています:
      • @stock:商品IDをキー、在庫数を値とするハッシュ
      • @products:商品IDをキー、商品オブジェクトを値とするハッシュ
    • add_productメソッドで新しい商品を追加します。
    • add_stockremove_stockメソッドで在庫数を増減します。
    • display_inventoryメソッドで全ての在庫を表示します。
    • display_by_categoryメソッドでカテゴリー別に在庫を表示します。

このシステムでは、オブジェクト指向の考え方に沿って以下のような設計がされています:

  • カプセル化:各クラスは必要なデータとメソッドを内部に持ち、外部から直接アクセスされるべきでない情報(例:@stockハッシュ)は隠蔽されています。
  • 責任の分離:商品に関する情報はProductクラスが、在庫管理はInventoryクラスが担当しています。
  • データの検証:入力値が適切であるかどうかをメソッド内で検証しています(例:在庫数や価格が正の値であることなど)。

これにより、商品の追加、在庫の更新、在庫状況の表示などの操作が整理された形で実装されています。

終わりに

ここまでお読み頂きありがとうございます!それでは3章でお会いしましょう!

追記(2025年月4月10日)

本記事の3章目となるカプセル化についての記事を執筆しました!ぜひお読みくださいませ!!
https://zenn.dev/osakayakyu/articles/75303ace6a706e

Discussion