16
8

【SOLID原則】じゃんけんアプリをオブジェクト指向で作り変えてみた

Last updated at Posted at 2023-12-17

はじめに

この記事は「🎄RUNTEQ Advent Calendar 2023🎅」の18日目に参加用の記事です。

テーマは【初めて学んだ技術】ということで、改めてデザインパターンである【オブジェクト指向とSOLID原則】について書いていきます。

以前作ったじゃんけんアプリをもとにオブジェクト指向について考えながら、作り変えていきたいと思います。

古のジャンケンアプリ

学習初期にRubyの学習に合わせてジャンケンアプリを作っていました。
勝負回数を選択でき、CPUと対戦できる簡単なアプリです。

Image from Gyazo

コードはこちらです。
パッと見でも???と思うようなコードが散見されています。
もはや愛くるしささえ感じます。

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)に見立てた「オブジェクト」と呼ばれる小さな構成単位の組み合わせとして捉える。

なるほどなるほど。

現実世界の物理的なモノに見立ててプログラムを組むということでしょうか?

一方で、モノとして考えてコードを書いていけば良いことはわかったものの、具体的な書き方がわかりません。。。。。

そして神となる

とりあえず神になります。
余計なことは考えず、神となってじゃんけんアプリを作ってみましょう。

神「神じゃ。」

internet_god.png

神はこの様に世界を作られる

神はいくらでも自由に世界をお作りになられることができます。

一方で、神が世界を作られる上で前提のルールがあります。

それがクラスインスタンスです。

例えば、神がある世界(プログラム)人間(オブジェクト/モノ)を誕生させたい時には、いきなり作ることは出来ません。

人間(オブジェクト/モノ)を誕生させたい場合は、人間って何?という必ず設計図を書く必要があります。

この設計図をクラスと呼びます。

んvjdんjf.png

#クラス(設計図)を作る
class Human
end

そしてこのクラス(設計図)をもとに実際に誕生した人間=オブジェクト/モノインスタンスと呼びます。

人間.png

#1.クラス(設計図)を作る
class Human
end

#2.クラス(設計図)をもとに人間(インスタンス)を誕生させる
Human.new

神は全てを操れる

神は設計図であるHumanクラスを作り、Human.newでこの世にオブジェクトを誕生させました。

一方で、クラスには現状何の情報も無いため、誕生させても人間の形をした空っぽのオブジェクトになってしまいます。

figure_standcd.png

神「つまらん。」

そこで神は、この空っぽ人間を誕生させる時に、名前性別という情報を与えることにしました。

class Human
  #Human.newの際に実行される
  def initialize(name, gender)
    @name = name
    @gender = gender
  end
end

takahashi = Human.new("高橋", "男")
satoh = Human.new("佐藤", "女")

こうして無事に、名前と性別という情報を持ったHumanクラスのインスタンスを誕生させることが出来ました。

Rubyでは、この情報のことをインスタンス変数と呼んでいます。

count_boy.png
count_girl.png

さっそく神は作った人間を操作することにしました。

神「高橋、佐藤と喋ってくれ〜」

高橋「......」

count_boy.png

神「佐藤、何か食べてくれ〜」

佐藤「......」

count_girl.png

神「ミスった。」

そう、神はお間違いになられました。

今の人間インスタンスは情報を持っただけで、食べたり、会話したりする反応をすることが出来ません。

取り急ぎ、神は設計図を書き直すことにしました。

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("佐藤")

高橋「今から佐藤と話すよー」

count_boy.png

神「佐藤、焼肉を食べてくれ〜 satoh.eat("焼肉")

佐藤「今から焼肉を食べるよー」

count_girl.png

無事に神は人間たちを思い通りに操ることが出来ました。

この様に誕生させたインスタンスに反応してもらうためには、メソッドを定義する必要があります。

def メソッド名
  #反応してもらう内容
end

このように、プログラム上でクラス(設計図)を作り、そのクラス(設計図)を基にインスタンス(オブジェクト)を誕生させ、インスタンス変数(情報)メソッド(反応)を追加し操作することでプログラム(世界)を完成させていきます。

このような流れでプログラムを行うことをオブジェクト指向プログラミングと呼びます。

神「オブジェクト指向で色んな世界を作るんじゃ」

internet_god.png

これまでのまとめ

  1. 世界=プログラムを作るには、設計図=クラスと設計図から作られたモノ=インスタンスを準備する必要がある

  2. 設計図から作られたモノ=インスタンスには、情報と、反応してもらうためにメソッドを定義することができる

  3. オブジェクト指向プログラミングとは、プログラム上でクラス(設計図)を作り、そのクラス(設計図)を基にインスタンス(オブジェクト)を誕生させ、インスタンス変数(情報)メソッド(反応)を追加し操作することでプログラム(世界)を完成させていくこと

神は暇なのでジャンケンができる世界を作ってみた

神「人間がジャンケンできる世界を作るんじゃ!」

神は早速作ることにしました。

ここで先ほどのルールから、ジャンケンに必要な設計図は何かを考えることにしました。

janken_boys.png

神「じゃんけんというゲームとジャンケンができる人間を用意すれば出来そうじゃ」

そして、神はこのジャンケンを楽しくするためにいくつかのルールを考えました。

ルール

  • じゃんけんの勝負回数を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

人間 「チョキ」

otoko.png

ルール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

神「完成じゃ!!!!!」

Image from Gyazo

internet_god.png

神気付く

喜びも束の間、神は何かの違和感を感じました。

internet_god_kamieshi.png

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原則じゃ、、!!」

slide_3.jpeg
引用先: 「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を返すように指定しています。

しかし、それらの子クラスであるHumanCpuでは同じメソッドでも中身の処理(振舞い)が異なります。

その為、リスコフの置換原則では、あるクラス(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のインターフェースを持っていて、それを各サブクラスが継承して実装しています。

通常の依存関係だと、じゃんけんのロジックは具体的な実装(HumanCpu)に依存してしまう。しかし「依存関係逆転」の原則によって、それが逆転していて、ロジックは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

最後に

オブジェクト指向を意識して今後も柔軟なコードをかけるかけるようにしていきたいです。

最後まで読んでいただきありがとうございました!

参考

16
8
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
16
8