LoginSignup
4
2

More than 3 years have passed since last update.

クラスの作りかたからオブジェクト指向へ

Posted at

クラスをどのように作るのか やクラスをどのように活かすのかといった考え方を学びます。今後のWebアプリケーション実装において重要な考え方について紹介します。

No 目的
(1) アプリケーション実装における、クラスの作り方を学ぶこと
(2) 複数のクラスが絡みあうアプリケーションの実装を理解すること

飲み物の自販機アプリケーションから学びます。

商品を補充し、→商品を選択、→お金を投入し、→購入のサイクルを考えます。

ファイルを用意しよう
実装に使用するディレクトリとRubyのファイルを用意しましょう。

ruby_application_training
application.rb

アプリケーション実装

用意すべきクラスを考える

アプリケーションを実装する前に、どのようなクラスを用意すべきなのか=このアプリケーションにおいて、どのような処理が存在するのか?

No 処理 クラス名
1 商品のデータを作成 Drink
2 ユーザーに販売している商品一覧表示 VendingMachine
3 購入したい商品決定 User
4 お金を投入 User
5 投入金額からお釣りの計算をする(購入処理) Purchase
クラス 意味 何をするのか
Drink 販売する飲み物 飲み物を実態のあるデータ(インスタンス)に
VendingMachine 自販機 ユーザーに販売している商品を見せて、投入金額からお釣り計算
User 購入する人 購入したい商品を決めて、お金を投入する

このアプリケーションには、このように少なくとも3つのクラスが必要ということになります。

「自販機アプリケーションなので、自販機クラスだけ用意すればいいのでは?」と考えましたが、その場合、「自販機が行うべきでないこと」も、自販機のクラスに含まれてしまうことになります。

単一責任の原則

アプリケーションの設計を考える上で、必要となる決まりの1つです。上記の自販機アプリケーションでは、自販機・飲み物・購入する人の役割が1つ程度になるように分割されています。このように、「1つのクラスは1つの振る舞いしか持たない」という原則を意識しないと、他者からみた時に意図のわからないアプリケーションが完成します。

application.rbに必要なクラスを定義

ruby_application_training/application.rb
class Drink
end

class VendingMachine
end

class User
end

クラスごとの処理を記述

商品が登録(商品となる飲み物のデータを生成)

ruby_application_training/application.rb
class Drink
  def initialize(name, fee)
    @name = name
    @fee = fee
  end

  def name
    @name
  end

  def fee
    @fee
  end
end

class VendingMachine
end

class User
end

puts "商品を用意してください。"
drinks = [] #配列の空箱を用意
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)  #<<は配列の要素追加
end

puts drinks
着目point

Drink.new(drink_name,drink_fee)として、Drinkクラスのインスタンスを生成。実引数として渡しているのは、飲み物の名称と金額です。Drinkクラス内では、与えられた値をもとに、インスタンス変数(@name,@fee)を生成。*drinks << Drink.new(drink_name,drink_fee)の記述で、用意した配列drinksに生成したインスタンスを追加

3つのデータが出力すれば成功です。これはDrinkクラスから生成したインスタンス(飲み物のデータ)です。3.timesを用いて3回入力しているので、3つのデータが生成。

商品を表示

商品の登録ができたので、それらをユーザーが購入できるように表示。商品の情報を表示するのはVendingMachineクラスの役割。(show)

ruby_application_training/application.rb
class Drink
  def initialize(name, fee)
    @name = name
    @fee = fee
  end

  def name
    @name #商品名表示
  end

  def fee
    @fee
  end
end

class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end
end

class User
end

puts "商品を用意してください。"
drinks = []
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)
end

vending_machine = VendingMachine.new(drinks)
vending_machine.show_drinks
着目point

vending_machine = VendingMachine.new(drinks)でVendingMachineクラスのインスタンスを生成す。インスタンス生成の際、3つの商品情報を含む配列drinksを、実引数として渡す。initializeメソッドで生成した@drinksには、受け取った配列の情報が代入。

vending_machine.show_drinksで生成したインスタンスに対してshow_drinksメソッドを適用。show_drinksメソッドは、@drinksに含まれる配列の情報を1つずつ表示しています。self.drinks.each do |drink|のselfは、show_drinksメソッドを適用されたインスタンス自身が、同じクラス内で定義したインスタンスメソッドdrinksを利用することを宣言。なお、このタイミングで1つ前のステップで記述したputs drinksは削除。

self

Rubyのインスタンスメソッド内でのselfという記述は特別。selfが書かれているインスタンスメソッドを適用したインスタンス自身が代入されている変数だと考える。VendingMachineクラスのインスタンスが代入されています。

puts "【#{i}】#{drink.name}: #{drink.fee}円"という記述は、Drinkクラスのインスタンスがインスタンスメソッド「name」と「fee」を利用しているます。この2つのメソッドの中身はそれぞれ@name@feeのみで、つまり自身のインスタンス変数の値を、戻り値とします。インスタンス変数の値のみを戻り値としたメソッドのことを特別にゲッターと呼ぶ。

ゲッター

クラスに設定したインスタンス変数の値を、インスタンスから読み取って表示するためだけに定義するメソッドです。
上のdrinkクラスのように、インスタンスを生成してそれぞれ別々の値をインスタンス変数に定義し、あとでその情報を利用することはよくあります。なので、クラスを定義する際はゲッターを用意することがほとんどです。

セッター

あるインスタンスが持つインスタンス変数の値を更新するためだけのメソッドのことをセッターと呼ぶ。具体的なセッターの定義例を以下に明示。

【例】sample.rb
class Human
  #humanクラスのインスタンス変数は@name, @ageとする。
  def initialize
    puts "名前を入力してください"
    @name = gets.chomp
    puts "年齢を入力してください"
    @age = gets.to_i
  end

  #ゲッター
  def name
    @name
  end

  def age
    @age
  end

  #セッター
  def name=(set)
    @name = set
  end

  def age=(set)
    @age = set
  end
end

セッターのメソッド名は必ずdef インスタンス変数名=とします。=が名前に含まれているのがポイント。

【例】sample.rb
  human = Human.new
  #initializeメソッドで@nameに"takashi", @ageに25を代入したとする
  #ゲッターでインスタンス変数の値を確認
  puts human.name
  #=> "takashi" #出力値
  puts human.age
  #=> 25     #出力値

  #セッターを利用して、インスタンス変数の値を書き換える
  human.name = "satoshi"
  human.age = 20

  #再びゲッターでインスタンス変数の値を確認
  puts human.name
  #=> "satoshi"
  puts human.age
  #=> 20

 セッターの使い方は少し特殊。実はRubyには、「名前に=がついたメソッドの呼び出しの際のメソッド名=の前後はいくら半角スペースを空けても構わない」という仕様と、「引数の()は省略できる」という仕様があります。これを利用して、あたかも変数代入かのようにセッターを利用します。
 上の例では、human.nameという変数に"satoshi"を再代入しているように感じます。しかし実態はhuman.name=("satoshi")となっており、name=という名前に=を含むメソッドが定義され引数を受け取って、メソッド内でインスタンス変数に再代入しています。

投入金額(商品購入)

登録した商品を表示するところまでできました。続いて、表示された商品から、自販機に投入する金額を決定します。投入金額を決めるのは購入者の役割なので、Userクラスに記述。

ruby_application_training/application.rb
class Drink
  def initialize(name, fee)
    @name = name
    @fee = fee
  end

  def name
    @name
  end

  def fee
    @fee
  end
end

class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end
end

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

  def money
    @money
  end
end

puts "商品を用意してください。"
drinks = []
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)
end

vending_machine = VendingMachine.new(drinks)
vending_machine.show_drinks

puts "あなたはお客さんです。投入金額を決めてください。"
money = gets.to_i
user = User.new(money)
着目point

user = User.new(money)で、Userクラスのインスタンスを生成して変数userに代入。

選んだ商品を購入

投入金額が足りない場合は、「投入金額が足りません」と表示。
ポイントは、「購入する商品の選択は購入者が行い、決済は自販機が行う」という点。UserクラスとVendingMachineクラスそれぞれに処理を記述します。

ruby_application_training/application.rb
class Drink
  def initialize(name, fee)
    @name = name
    @fee = fee
  end

  def name
    @name
  end

  def fee
    @fee
  end
end

class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end

  def pay(user)
    puts "商品を選んでください"
    chosen_drink = user.choose_drink
    change = user.money - self.drinks[chosen_drink].fee
    if change >= 0
      puts "ご利用ありがとうございました!お釣りは#{change}円です。"
    else
      puts "投入金額が足りません"
    end
  end
end

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

  def money
    @money
  end

  def choose_drink
    gets.to_i
  end
end

puts "商品を用意してください。"
drinks = []
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)
end

vending_machine = VendingMachine.new(drinks)
vending_machine.show_drinks

puts "あなたはお客さんです。投入金額を決めてください。"
money = gets.to_i
user = User.new(money)

vending_machine.pay(user)

VendingMachineクラスに定義した、payメソッドを呼び出しています。実引数としては、直前で定義したインスタンスuserを渡します。

payメソッドに注目しましょう。chosen_drink = user.choose_drinkでは、Userクラスに定義したchoose_drinkメソッドを呼び出しています。change = user.money - self.drinks[chosen_drink].feeでは、「ユーザーの投入金額」と「選ばれた商品の金額」の差分を計算して、その結果を変数changeに代入。changeにはお釣りの金額が含まれる。

VendingMachineクラスのメソッド内で、Userクラスのインスタンスについてメソッドを実行する必要です。Userクラスのインスタンスを、実引数として渡す必要があります。

ファイルを分割

飲み物自販機のアプリケーションですが、コードが長くなり、且つクラスの分かれ目も見分けにくくなっています。したがって、ファイルを分割し、1つのファイル(今回はapplication.rb)で読み込んであげるようにします。

この時に役立つのが、requireです。requireはRubyファイルを読み込むためのメソッドです。*よくライブラリ(ライブラリもあくまでRubyのファイルの集合体ですを読み込むために使用しました。

ライブラリを読み込む際は、requireに続いてライブラリの名前を記述するだけで完了しました。開発者自身で設置したRubyファイルを読み込むには、ディレクトリを明示して記述する必要があります。

クラスごとにファイルを分割し、requireを用いて読み込み

ruby_application_training/application.rb
require "./drink"
require "./vending_machine"
require "./user"

puts "商品を用意してください。"
drinks = []
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)
end

vending_machine = VendingMachine.new(drinks)
vending_machine.show_drinks

puts "あなたはお客さんです。投入金額を決めてください。"
money = gets.to_i
user = User.new(money)

vending_machine.pay(user)
ruby_application_training/drink.rb
class Drink
  def initialize(name, fee)
    @name = name
    @fee = fee
  end

  def name
    @name
  end

  def fee
    @fee
  end
end
ruby_application_training/user.rb
class User
  def initialize(money)
    @money = money
  end

  def money
    @money
  end

  def choose_drink
    gets.to_i
  end
end
ruby_application_training/vending_machine.rb
class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end

  def pay(user)
    puts "商品を選んでください"
    chosen_drink = user.choose_drink
    change = user.money - self.drinks[chosen_drink].fee
    if change >= 0
      puts "ご利用ありがとうございました!お釣りは#{change}円です。"
    else
      puts "投入金額が足りません"
    end
  end
end

機能の追加と単一責任の原則
ここまでで一通りの実装は完了しました。次に、以下のような追加実装を考えましょう。

機能追加:購入後に、スロットゲームを実行し、3桁の数字が揃ったらあたりでもう1本プレゼント

追加機能について考えよう

【例】スロットゲーム機能を追加した自販機
class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def play_slot
    result = []
    3.times do 
      result << rand(0..9)
    end
    puts "スロットゲームの結果は#{result.join}です!"
    if result[0] == result[1] && result[0] == result[2]
      puts "おめでとうございます!"
    else
      puts "残念でした〜"
    end 
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end

  def pay(user)
    puts "商品を選んでください"
    chosen_drink = user.choose_drink
    change = user.money - self.drinks[chosen_drink].fee
    if change >= 0
      puts "ご利用ありがとうございました!お釣りは#{change}円です。"
      play_slot
    else
      puts "投入金額が足りません"
    end
  end
end

このスロットゲームの機能も、飲み物・自販機・ユーザーとは異なる別のクラスとして取り扱うことにしましょう。このように、スロットゲームのような目に見えない機能や振る舞いも、役割が異なれば別クラスに分ける必要があります。


slot_game.rbを作成
-


```rb:ruby_application_training/application.rb
require "./drink"
require "./vending_machine"
require "./user"
require "./slot_game"

puts "商品を用意してください。"
drinks = []
3.times do |i|
  puts "商品名を入力してください。"
  drink_name = gets.chomp
  puts "金額を入力してください。"
  drink_fee = gets.to_i
  drinks << Drink.new(drink_name,drink_fee)
end

vending_machine = VendingMachine.new(drinks)
vending_machine.show_drinks

puts "あなたはお客さんです。投入金額を決めてください。"
money = gets.to_i
user = User.new(money)

vending_machine.pay(user)


続いて、slot_game.rbに、スロットゲームそのものの処理を記述しましょう。

```rb:ruby_application_training/slot_game.rb
class SlotGame
  def play_slot
    result = []
    3.times do 
      result << rand(0..9)
    end
    puts "スロットゲームの結果は#{result.join}です!"
    if result[0] == result[1] && result[0] == result[2]
      puts "おめでとうございます!"
    else
      puts "残念でした〜"
    end 
  end
end

3つのランダムな値を生成して配列resultに代入します。joinメソッドを用いて配列に含まれる値を、ひと続きに表示します。その後、8行目で3つの値が揃っているかどうかを判断。

ruby_application_training/vending_machine.rb
class VendingMachine
  def initialize(drinks)
    @drinks = drinks
  end

  def drinks
    @drinks
  end

  def show_drinks
    puts "いらっしゃいませ。以下の商品を販売しています"
    i = 0
    self.drinks.each do |drink|
      puts "【#{i}#{drink.name}: #{drink.fee}円"
      i += 1
    end
  end

  def pay(user)
    puts "商品を選んでください"
    chosen_drink = user.choose_drink
    change = user.money - self.drinks[chosen_drink].fee
    if change >= 0
      puts "ご利用ありがとうございました!お釣りは#{change}円です。"
      SlotGame.new.play_slot
    else
      puts "投入金額が足りません"
    end
  end
end

SlotGameクラスのインスタンスを生成しつつ、play_slotメソッドを呼び出しています。

クラスの決め方を振り返る

単一責任の原則を振り返ろう

単一責任の原則を最初に学び、データと処理のまとまりごとにクラスを分けて実装。上記のアプリケーションでは、飲み物・自販機・ユーザーといったように分けて実装。

追加で機能を実装する際も、既存のクラスの役割と異なれば、別のクラスを作成して実装しました。スロットゲームの機能を持ったSlotGameクラスがこれ.

データと処理のまとまりごとに分けて実装する考えを、オブジェクト指向という。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2