はじめに
この記事は「🎄RUNTEQ Advent Calendar 2023🎅」の18日目に参加用の記事です。
テーマは【初めて学んだ技術】ということで、改めてデザインパターンである【オブジェクト指向とSOLID原則】について書いていきます。
以前作ったじゃんけんアプリをもとにオブジェクト指向について考えながら、作り変えていきたいと思います。
古のジャンケンアプリ
学習初期にRubyの学習に合わせてジャンケンアプリを作っていました。
勝負回数を選択でき、CPUと対戦できる簡単なアプリです。
コードはこちらです。
パッと見でも???と思うようなコードが散見されています。
もはや愛くるしささえ感じます。
HAND_GU = "g"
HAND_CHOKI = "c"
HAND_PA = "p"
JANKENHANDS = [HAND_GU,HAND_CHOKI,HAND_PA]
NUMBEROFMATCHES = [1,3,5]
def create_number_of_matches
puts "何本勝負?(press #{NUMBEROFMATCHES.join(' or ')})"
number = gets.chomp.to_i
number_exists(number)
end
def do_rock_paper_scissor(number_of_matches)
victory = 0
defeat = 0
num = 0
while num < number_of_matches do
puts "#{num + 1}本目"
puts "じゃんけん…(press #{JANKENHANDS.join(' or ')})"
@cpu_hands = JANKENHANDS[rand(0..2)]
@my_hands = gets.chomp.to_s
hands_exists(@my_hands)
if @my_hands == @cpu_hands
aiko
else
show_hand
end
#結果の判定
if @my_hands == HAND_GU && @cpu_hands == HAND_CHOKI || @my_hands == HAND_CHOKI && @cpu_hands == HAND_PA || @my_hands == HAND_PA && @cpu_hands == HAND_GU
victory += 1
puts "勝ち!"
end
if @my_hands == HAND_GU && @cpu_hands == HAND_PA || @my_hands == HAND_CHOKI && @cpu_hands == HAND_GU || @my_hands == HAND_PA && @cpu_hands == HAND_CHOKI
defeat += 1
puts "負け!"
end
puts "#{victory}勝#{defeat}敗"
num += 1
end
if victory > defeat
puts "結果\n#{victory}勝#{defeat}敗であなたの勝ち"
else
puts "結果\n#{victory}勝#{defeat}敗であなたの負け"
end
end
def aiko
show_hand
puts "あいこで...(press #{JANKENHANDS.join(' or ')})"
@cpu_hands = JANKENHANDS[rand(0..2)]
@my_hands = gets.chomp.to_s
hands_exists(@my_hands)
end
def show_hand
if @cpu_hands == HAND_GU
puts 'CPU…グー'
elsif @cpu_hands == HAND_CHOKI
puts 'CPU…チョキ'
elsif @cpu_hands == HAND_PA
puts 'CPU…パー'
end
if @my_hands == HAND_GU
puts 'あなた…グー'
elsif @my_hands == HAND_CHOKI
puts 'あなた…チョキ'
elsif @my_hands == HAND_PA
puts 'あなた…パー'
end
end
def number_exists(number)
if NUMBEROFMATCHES.include?(number)
puts "#{number}本勝負を選びました"
number
else
puts "#{NUMBEROFMATCHES.join(',')}のいずれかの値を入力してください"
create_number_of_matches
end
end
def hands_exists(hands)
if JANKENHANDS.include?(hands)
true
else
puts "#{JANKENHANDS.join(',')}のいずれかの値を入力してください"
hands = gets.chomp.to_s
hands_exists(hands)
end
end
number_of_matches = create_number_of_matches
do_rock_paper_scissor(number_of_matches)
オブジェクト指向とは?
オブジェクト指向(object oriented)とは、コンピュータプログラムの設計や実装についての考え方の一つで、互いに密接に関連するデータと手続き(処理手順)を「オブジェクト」(object)と呼ばれる一つのまとまりとして定義し、様々なオブジェクトを組み合わせてプログラム全体を構築していく手法。
少し難しいですね。
もう少し調べると、こんな説明が↓↓↓
システム全体を、現実世界の物理的なモノ(object)に見立てた「オブジェクト」と呼ばれる小さな構成単位の組み合わせとして捉える。
なるほどなるほど。
現実世界の物理的なモノに見立ててプログラムを組むということでしょうか?
一方で、モノとして考えてコードを書いていけば良いことはわかったものの、具体的な書き方がわかりません。。。。。
そして神となる
とりあえず神になります。
余計なことは考えず、神となってじゃんけんアプリを作ってみましょう。
神「神じゃ。」
神はこの様に世界を作られる
神はいくらでも自由に世界をお作りになられることができます。
一方で、神が世界を作られる上で前提のルールがあります。
それがクラスとインスタンスです。
例えば、神がある世界(プログラム)
に人間(オブジェクト/モノ)
を誕生させたい時には、いきなり作ることは出来ません。
人間(オブジェクト/モノ)
を誕生させたい場合は、人間って何?という必ず設計図を書く必要があります。
この設計図をクラスと呼びます。
#クラス(設計図)を作る
class Human
end
そしてこのクラス(設計図)をもとに実際に誕生した人間=オブジェクト/モノをインスタンスと呼びます。
#1.クラス(設計図)を作る
class Human
end
#2.クラス(設計図)をもとに人間(インスタンス)を誕生させる
Human.new
神は全てを操れる
神は設計図であるHumanクラスを作り、Human.new
でこの世にオブジェクトを誕生させました。
一方で、クラスには現状何の情報も無いため、誕生させても人間の形をした空っぽのオブジェクトになってしまいます。
神「つまらん。」
そこで神は、この空っぽ人間を誕生させる時に、名前と性別という情報を与えることにしました。
class Human
#Human.newの際に実行される
def initialize(name, gender)
@name = name
@gender = gender
end
end
takahashi = Human.new("高橋", "男")
satoh = Human.new("佐藤", "女")
こうして無事に、名前と性別という情報を持ったHumanクラスのインスタンスを誕生させることが出来ました。
Rubyでは、この情報のことをインスタンス変数
と呼んでいます。
さっそく神は作った人間を操作することにしました。
神「高橋、佐藤と喋ってくれ〜」
高橋「......」
神「佐藤、何か食べてくれ〜」
佐藤「......」
神「ミスった。」
そう、神はお間違いになられました。
今の人間インスタンスは情報を持っただけで、食べたり、会話したりする反応をすることが出来ません。
取り急ぎ、神は設計図を書き直すことにしました。
class Human
def initialize(name, gender)
@name = name
@gender = gender
end
def talk_with(name)
puts "今から#{name}と話すよー"
end
def eat(food)
puts "今から#{food}を食べるよー"
end
end
takahashi = Human.new("高橋", "男")
satoh = Human.new("佐藤", "女")
takahashi.talk_with("佐藤")
satoh.eat("焼肉")
神「高橋、佐藤と喋ってくれ〜 takahashi.talk_with("佐藤")
」
高橋「今から佐藤と話すよー」
神「佐藤、焼肉を食べてくれ〜 satoh.eat("焼肉")
」
佐藤「今から焼肉を食べるよー」
無事に神は人間たちを思い通りに操ることが出来ました。
この様に誕生させたインスタンスに反応してもらうためには、メソッドを定義する必要があります。
def メソッド名
#反応してもらう内容
end
このように、プログラム上でクラス(設計図)
を作り、そのクラス(設計図)
を基にインスタンス(オブジェクト)
を誕生させ、インスタンス変数(情報)
やメソッド(反応)
を追加し操作することでプログラム(世界)
を完成させていきます。
このような流れでプログラムを行うことをオブジェクト指向プログラミングと呼びます。
神「オブジェクト指向で色んな世界を作るんじゃ」
これまでのまとめ
-
世界=プログラムを作るには、設計図=クラスと設計図から作られたモノ=インスタンスを準備する必要がある
-
設計図から作られたモノ=インスタンスには、情報と、反応してもらうためにメソッドを定義することができる
-
オブジェクト指向プログラミングとは、プログラム上で
クラス(設計図)
を作り、そのクラス(設計図)
を基にインスタンス(オブジェクト)
を誕生させ、インスタンス変数(情報)
やメソッド(反応)
を追加し操作することでプログラム(世界)
を完成させていくこと
神は暇なのでジャンケンができる世界を作ってみた
神「人間がジャンケンできる世界を作るんじゃ!」
神は早速作ることにしました。
ここで先ほどのルールから、ジャンケンに必要な設計図は何かを考えることにしました。
神「じゃんけんというゲームとジャンケンができる人間を用意すれば出来そうじゃ」
そして、神はこのジャンケンを楽しくするためにいくつかのルールを考えました。
ルール
- じゃんけんの勝負回数を1,3,5回と最初に選択できること
- 1人の人間を操作することができ、もう一方の人間はランダムで手を出してくる
- あいこの場合は、勝負回数に含まれずに仕切り直しにされること
- 最終的な結果発表をすること
神「早速設計図から作るのじゃ」
class Janken
end
class Human
end
class Cpu
end
そして、雛形を作ってジャンケンができそうか一旦確認してみることにしました。
神「設計図から人間とじゃんけんゲームを誕生させるのじゃ」
class Janken
def initialize
@game_rounds = 1 # 1回で仮置き
@human = Human.new # 人間プレイヤーのインスタンスを作成
@cpu = Cpu.new # CPUプレイヤーのインスタンスを作成
end
def start
puts "今回の勝負回数は#{1}回だよ、ジャンケン始めるよー"
end
end
class Human
def initialize
puts " 人間だよーじゃんけん始める準備はできたよ"
end
end
class Cpu
def initialize
puts " CPUだよーじゃんけん始める準備はできたよ"
end
end
janken = Janken.new
janken.start
# => 人間だよーじゃんけん始める準備はできたよ
# => CPUだよーじゃんけん始める準備はできたよ
# => 今回の勝負回数は1回だよ、ジャンケン始めるよー
神「早速ルールを作るのじゃ」
ルール1.ジャンケンの勝負回数を1,3,5回と最初に選択できること
次に勝負回数を選択できる様にしていきます。
先ほど設定したstart
の中に書いていきましょう。
class Janken
GAME_COUNT = [1,3,5]
def initialize
@game_rounds = 0
@human = Human.new # 人間プレイヤーのインスタンスを作成
@cpu = Cpu.new # CPUプレイヤーのインスタンスを作成
end
def start
# 勝負回数を選択できる
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_rounds = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_rounds
puts "#{@game_rounds}本勝負を選びました。"
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
start
end
end
end
# => 人間だよーじゃんけん始める準備はできたよ
# => CPUだよーじゃんけん始める準備はできたよ
# => 何本勝負?(press 1 or 3 or 5)
# => 1
# => 1本勝負を選びました。
神「いい感じじゃ。」
じゃんけんゲームを作る
次に実際にジャンケンの中身を作っていきましょう。
def start
# 勝負回数を選択できる
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_rounds = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_rounds
puts "#{@game_rounds}本勝負を選びました。"
#ジャンケンの中身
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
start
end
end
ルール2.人間を操作することができ、もう一方の人間はランダムで手を出してくる
game_count = gets.to_i
で入力された値を@game_rounds.times do |round| ~ end
で回数分処理を行う様にしています。
def start
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
@game_rounds.times do |round|
# 回数分じゃんけんが対戦できる
end
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
start
end
end
次に実際に対戦できる様に作っていきます。
まずは、人間
とCpu
がそれぞれ手を出せる様にします。
class Janken
GAME_COUNT = [1,3,5]
HANDS = { 'g' => 0, 'c' => 1, 'p' => 2 }
def initialize
@game_rounds = 0
@human = Human.new
@cpu = Cpu.new
end
def start
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
@game_rounds.times do |round|
# 回数分じゃんけんが対戦できる
human_hand = @human.choose_hand
cpu_hand = @cpu.choose_hand
puts "#{round + 1}回戦: あなたの手: #{human_hand}, CPUの手: #{HANDS.key(cpu_hand)}"
end
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
start
end
end
end
class Human
def initialize
puts "人間だよーじゃんけん始める準備はできたよ"
end
def choose_hand
puts "g、c、pから選んでください:"
challenge_hand = gets.chomp
if Janken::HANDS.key?(challenge_hand)
challenge_hand
else
puts "入力が不正です。もう一度選んでください。"
choose_hand
end
end
end
class Cpu
def initialize
puts "CPUだよーじゃんけん始める準備はできたよ"
end
def choose_hand
Janken::HANDS.keys.sample
end
end
人間側には@human.choose_hand
でジャンケンする手を選択できる様にしています。
CPU側には@cpu.choose_hand
内で、Janken::HANDS.values.sample
とすることで、ランダムに出す手を出力する様にしています。
神「人間よ、ジャンケンの手を選んでやるのじゃ!@human.choose_hand
」
神「チョキじゃ!(ターミナルでc
と入力)chosen_hand = gets.chomp
」
人間 「チョキ」
ルール3.あいこの場合は、勝負回数に含まれずに仕切り直しにされること
結果を表示させる前に、あいこのケースを実装していきます。
あいこの場合は、勝負回数に含まれず勝敗が着くまで、対戦できるようにします。
class Janken
GAME_COUNT = [1,3,5]
HANDS = { 'g' => 0, 'c' => 1, 'p' => 2 }
def initialize
@game_rounds = 0
@human = Human.new
@cpu = Cpu.new
end
def start
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
actual_round = 1
while actual_round <= @game_rounds
puts "#{actual_round}回戦"
human_hand = @human.choose_hand
cpu_hand = @cpu.choose_hand
puts "あなたの手: #{human_hand}, CPUの手: #{cpu_hand}"
result = (HANDS[human_hand] - HANDS[cpu_hand]) % 3
# 余りが0の場合は引き算の結果が3の倍数、つまり両者の手が同じ(あいこ)。
# 余りが1の場合はCPUの勝ち。
# 余りが2の場合はHumanの勝ち。
if result == 1
puts "あなたの負けです。"
elsif result == 2
puts "あなたの勝ちです!"
else
puts "あいこでした。もう一度じゃんけんします。"
next # あいこの場合は次のラウンドへは進まず、同じラウンドを再実行
end
actual_round += 1
end
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
self.start
end
end
end
class Human
def initialize
puts "人間だよーじゃんけん始める準備はできたよ"
end
def choose_hand
puts "g、c、pから選んでください:"
challenge_hand = gets.chomp
if Janken::HANDS.key?(challenge_hand)
challenge_hand
else
puts "入力が不正です。もう一度選んでください。"
choose_hand
end
end
end
class Cpu
def initialize
puts "CPUだよーじゃんけん始める準備はできたよ"
end
def choose_hand
Janken::HANDS.keys.sample
end
end
ルール4.最終的な結果発表をすること
次に最終的な結果を表示させます。
class Janken
GAME_COUNT = [1,3,5]
HANDS = { 'g' => 0, 'c' => 1, 'p' => 2 }
def initialize
@game_rounds = 0
@human = Human.new
@cpu = Cpu.new
@human_score = 0 #追加
@cpu_score = 0 #追加
end
def start
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
actual_round = 1
while actual_round <= @game_rounds
puts "#{actual_round}回戦"
human_hand = @human.choose_hand
cpu_hand = @cpu.choose_hand
puts "あなたの手: #{human_hand}, CPUの手: #{cpu_hand}"
result = (HANDS[human_hand] - HANDS[cpu_hand]) % 3
# 余りが0の場合は引き算の結果が3の倍数、つまり両者の手が同じ(あいこ)。
# 余りが1の場合はCPUの勝ち。
# 余りが2の場合はHumanの勝ち。
if result == 1
puts "あなたの負けです。"
@cpu_score += 1
elsif result == 2
puts "あなたの勝ちです!"
@human_score += 1
else
puts "あいこでした。もう一度じゃんけんします。"
next # あいこの場合は次のラウンドへは進まず、同じラウンドを再実行
end
actual_round += 1
end
# 最終的な結果を表示
puts '結果'
puts "#{@human_score}勝#{@cpu_score}敗であなたの#{ @human_score > @cpu_score ? '勝ち' : '負け' }です。"
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
self.start
end
end
end
神「完成じゃ!!!!!」
神気付く
喜びも束の間、神は何かの違和感を感じました。
def start
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
actual_round = 1
while actual_round <= @game_rounds
puts "#{actual_round}回戦"
human_hand = @human.choose_hand
cpu_hand = @cpu.choose_hand
puts "あなたの手: #{human_hand}, CPUの手: #{cpu_hand}"
result = (HANDS[human_hand] - HANDS[cpu_hand]) % 3
if result == 1
puts "あなたの負けです。"
@cpu_score += 1
elsif result == 2
puts "あなたの勝ちです!"
@human_score += 1
else
puts "あいこでした。もう一度じゃんけんします。"
next # あいこの場合は次のラウンドへは進まず、同じラウンドを再実行
end
actual_round += 1
end
puts '結果'
puts "#{@human_score}勝#{@cpu_score}敗であなたの#{ @human_score > @cpu_score ? '勝ち' : '負け' }です。"
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
self.start
end
end
神「なんか処理長くね、、??」
そうです、今のstart
はゲームカウントを入力させ、実際に対戦を行い、あいこの場合は再度対戦させ、最終的な結果を表示させるというたくさん役割のあるメソッドになっています。
神「他の神に見られたら、Janken.startだけで何してるかわからないな...」
Janken.start
とは書かれているものの、ロジックを最後まで読まないと何をしているかがわからない!という非常にストレスがかかる書き方になってしまっています。
ここに来てようやく、神はある原則のオブジェクト指向に基づいて世界を作るということを忘れていました。
SOLID原則
神「SOLID原則じゃ、、!!」
引用先: 「SOLIDの原則って何ですか?」って質問に答えたかった / What's SOLID principle
SOLID原則とはオブジェクト指向を設計する上で、柔軟で保守可能なコードを実現するための5つの原則の頭文字から取った総称です。
神はこの原則に基づいてコードを修正していくことにしました。
神「修正じゃ」
S (Single Responsibility) 単一責任の原則
クラスや関数は責任は一つにしておくべきという原則です。
今回の例では、start
メソッドに
1.対戦回数を選択する役割
2.じゃんけんを行う役割
3.最終的な結果を表示させる役割
とメソッド1つに対して、多くの役割を持たせてしまっております。
多くの役割を持つことは、影響範囲が大きく知らないうちに他の責任に影響を与える可能性が高くなります。
class Janken
GAME_COUNT = [1,3,5]
HANDS = { 'g' => 0, 'c' => 1, 'p' => 2 }
def initialize
@game_rounds = 0
@human = Human.new
@cpu = Cpu.new
@cpu_score = 0
@human_score = 0
end
def start
choose_game_count
play_rounds
display_result
end
private
def choose_game_count
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
choose_game_count
end
end
def play_rounds
actual_round = 1
while actual_round <= @game_rounds
puts "#{actual_round}回戦"
play_single_round
actual_round += 1
end
end
def play_single_round
human_hand = @human.choose_hand
cpu_hand = @cpu.choose_hand
puts "あなたの手: #{human_hand}, CPUの手: #{cpu_hand}"
determine_winner(human_hand, cpu_hand)
end
def determine_winner(human_hand, cpu_hand)
result = (HANDS[human_hand] - HANDS[cpu_hand]) % 3
if result == 1
puts "あなたの負けです。"
@cpu_score += 1
elsif result == 2
puts "あなたの勝ちです!"
@human_score += 1
else
puts "あいこでした。もう一度じゃんけんします。"
play_single_round
end
end
def display_result
puts '結果'
puts "#{@human_score}勝#{@cpu_score}敗であなたの#{ @human_score > @cpu_score ? '勝ち' : '負け' }です。"
end
end
神「少し読みやすくなったぞ」
O (Open-Closed) オープン・クローズドの原則
Human
クラスとCpu
クラスは処理は違いますが、choose_hand
という同じメソッド名を使用しています。
今後グーだけを出すプレイヤーを追加したい場合も考えると、元となるPlayer
クラスを用意することで、元となるクラスのメソッドをオーバーライドすることで保守性がより高まります。
class Janken
GAME_COUNT = [1,3,5]
HANDS = { 'g' => 0, 'c' => 1, 'p' => 2 }
def initialize(human, cpu)
@game_rounds = 0
@human = human #変更
@cpu = cpu #変更
@human_score = 0
@cpu_score = 0
end
def start
choose_game_count
play_rounds
display_result
end
private
def choose_game_count
#省略
end
def play_rounds
#省略
end
def play_single_round
#省略
end
def determine_winner(human_hand, cpu_hand)
#省略
end
def display_result
#省略
end
end
class Player
def initialize
end
def choose_hand
"g"
end
end
class Human < Player
def initialize
puts "人間だよーじゃんけん始める準備はできたよ"
end
def choose_hand
puts "g、c、pから選んでください:"
challenge_hand = gets.chomp
if Janken::HANDS.key?(challenge_hand)
challenge_hand
else
puts "入力が不正です。もう一度選んでください。"
choose_hand
end
end
end
class Cpu < Player
def initialize
puts "CPUだよーじゃんけん始める準備はできたよ"
end
def choose_hand
Janken::HANDS.keys.sample
end
end
human = Human.new
cpu = Cpu.new
janken = Janken.new(human, cpu)
janken.start
L (Liskov Substitution) リスコフの置換原則
オブジェクト指向プログラミングにおいて、子クラスのオブジェクトは親クラスのオブジェクトの仕様に従わなければならない、という原則です。
現状はあえて親クラスのPlayer
のchoose_handメソッドにg
を返すように指定しています。
しかし、それらの子クラスであるHuman
、Cpu
では同じメソッドでも中身の処理(振舞い)が異なります。
その為、リスコフの置換原則
では、あるクラス(Player)を継承するとき、継承元(Human、CPU)と継承先のクラス(Player)の振る舞いを同じにしようということです。
親クラスで処理ができることは、子クラスでも同じ処理ができなければならないという原則です。
今回はPlayer
クラスのメソッドを抽象メソッドにすることでこの置換原則に則っています。
class Player
def initialize
end
def choose_hand
raise NotImplementedError, 'Subclasses must implement abstract_method'
end
end
I (Interface Segregation) インターフェイス分離の原則
継承先で使わないメソッドがないようにきちんとメソッドは分けようという原則です。
例としてPlayer
クラスにgreet
メソッドを追加で定義しています。
Human
クラスではgreet
メソッドが実装されていません。
一方、Cpu
クラスではgreet
メソッドが実装されています。
これは、Humanクラスが使わないgreetメソッドの実装も継承できてしまう状況でありインターフェース分離の原則に違反しています。
したがって、greet
メソッドはCpu
クラスにのみ定義することでこの問題を解決することができます。
例として説明しましたが、今回はリスコフの置換原則
に基づいて修正した段階で、こちらの原則は満たされていた為、特に変更はありません。
class Player
def initialize
end
def choose_hand
raise NotImplementedError, 'Subclasses must implement abstract_method'
end
def greet
raise NotImplementedError, 'Subclasses must implement abstract_method'
end
end
class Human < Player
def initialize
puts "人間だよーじゃんけん始める準備はできたよ"
end
def choose_hand
puts "g、c、pから選んでください:"
challenge_hand = gets.chomp
if Janken::HANDS.key?(challenge_hand)
challenge_hand
else
puts "入力が不正です。もう一度選んでください。"
choose_hand
end
end
end
class Cpu < Player
def initialize
puts "CPUだよーじゃんけん始める準備はできたよ"
end
def choose_hand
Janken::HANDS.keys.sample
end
def greet
puts "CPUです"
end
end
D (Dependency Inversion) 依存性逆転の原則
今回のじゃんけんは、プレイヤーに手を選択してもらうためにchoose_hand
メソッドを呼び出しますが、Playerクラス
がchoose_hand
のインターフェースを持っていて、それを各サブクラスが継承して実装しています。
通常の依存関係だと、じゃんけんのロジックは具体的な実装(Human
やCpu
)に依存してしまう。しかし「依存関係逆転」の原則によって、それが逆転していて、ロジックはPlayer
クラスという抽象的なインターフェースに依存し、具体的なサブクラスの実装の詳細は気にせずに実装を行えます。
今回もリスコフの置換原則
に基づいて修正した段階で、こちらの原則は満たされていた為、特に変更はありません。
神「この辺りはさらに勉強が必要じゃ」
じゃんけんの完成
神「ようやく完成じゃ!!!」
class Janken
GAME_COUNT = [1,3,5]
HANDS = { 'g' => 0, 'c' => 1, 'p' => 2 }
def initialize(human, cpu)
@game_rounds = 0
@human = human #変更
@cpu = cpu #変更
@human_score = 0
@cpu_score = 0
end
def start
choose_game_count
play_rounds
display_result
end
private
def choose_game_count
puts "何本勝負?(press #{GAME_COUNT.join(' or ')})"
game_count = gets.to_i
if GAME_COUNT.include?(game_count)
@game_rounds = game_count
puts "#{@game_rounds}本勝負を選びました。"
else
puts "#{GAME_COUNT.join(' か ')}で入力してください"
choose_game_count
end
end
def play_rounds
actual_round = 1
while actual_round <= @game_rounds
puts "#{actual_round}回戦"
play_single_round
actual_round += 1
end
end
def play_single_round
human_hand = @human.choose_hand
cpu_hand = @cpu.choose_hand
puts "あなたの手: #{human_hand}, CPUの手: #{cpu_hand}"
determine_winner(human_hand, cpu_hand)
end
def determine_winner(human_hand, cpu_hand)
result = (HANDS[human_hand] - HANDS[cpu_hand]) % 3
if result == 1
puts "あなたの負けです。"
@cpu_score += 1
elsif result == 2
puts "あなたの勝ちです!"
@human_score += 1
else
puts "あいこでした。もう一度じゃんけんします。"
play_single_round
end
end
def display_result
puts '結果'
puts "#{@human_score}勝#{@cpu_score}敗であなたの#{ @human_score > @cpu_score ? '勝ち' : '負け' }です。"
end
end
class Player
def initialize
end
def choose_hand
raise NotImplementedError, 'Subclasses must implement abstract_method'
end
end
class Human < Player
def initialize
puts "人間だよーじゃんけん始める準備はできたよ"
end
def choose_hand
puts "g、c、pから選んでください:"
challenge_hand = gets.chomp
if Janken::HANDS.key?(challenge_hand)
challenge_hand
else
puts "入力が不正です。もう一度選んでください。"
choose_hand
end
end
end
class Cpu < Player
def initialize
puts "CPUだよーじゃんけん始める準備はできたよ"
end
def choose_hand
Janken::HANDS.keys.sample
end
end
human = Human.new
cpu = Cpu.new
janken = Janken.new(human, cpu)
janken.start
最後に
オブジェクト指向を意識して今後も柔軟なコードをかけるかけるようにしていきたいです。
最後まで読んでいただきありがとうございました!
参考