じゃんけんごときにクラスなんて要るのか

CodeIQというプログラミング課題に挑戦するサイトに、Rubyでジャンケンクラスを作れという問題があったのでやってみた。すでに問題は読めなくなっているけど、こんな感じのJankenクラスを作れという。

$ irb
> require './janken.rb'
> left = Janken.new
> right = Janken.new
> left.versus(right)
左の人が勝ました。右「チョキ」左「グー」
> left.versus(right)
右の人が勝ちました。右「チョキ」左「パー」
  :
  :

繰り返しirbで実行できるように、とある。

問題を見た瞬間、これは問題自体がおかしいのではないかと思ったけど、やってみた。

# -*- coding: utf-8 -*-
class Janken
  attr_reader :hand

  NAME = {
    goo:   "グー",
    choki: "チョキ",
    pah:   "パー"
  }

  def initialize
    generate_new_hand
  end

  def versus(other)
    generate_new_hand
    other.generate_new_hand
    case match(other)
    when :win
      puts "左の人が勝ちました。右「#{other}」左「#{self}"
    when :lose
      puts "右の人が勝ちました。右「#{other}」左「#{self}"
    when :even
      puts "あいこでした。#{self}"
    end
  end

protected

  def generate_new_hand
    @hand = [:goo, :choki, :pah].shuffle.pop
  end

  def match(other)
    match = [@hand, other.hand]
    case
    when @hand == other.hand; :even
    when match == [:goo, :choki] ||
        match == [:choki, :pah] ||
        match == [:pah, :goo]; :win
    else :lose
    end
  end

  def to_s
    NAME[@hand]
  end
end

勝敗メッセージの出力をJankenクラスでやるのは、いまいち。Jankenクラスは勝ち負けの判定ロジックを持つべきではあっても、その結果に基づくレポート機能まで含むのは抱え込みすぎだと思う。もう1つ、Jankenインスタンス自身は自分が右なのか左なのか分からないはずなので、右や左といった現実世界との結びつけは呼び出し側でやるべきこと。「右」とか「左」といった文字列がJankenクラスに含まれるのはおかしい。

ということを考えると、以下のほうが良い。

# -*- coding: utf-8 -*-
class Janken
  attr_reader :hand

  NAME = {
    goo:   "グー",
    choki: "チョキ",
    pah:   "パー"
  }

  def initialize
    generate_new_hand
  end

  def versus(other)
    generate_new_hand
    other.generate_new_hand
    match(other)
  end

protected

  def generate_new_hand
    @hand = [:goo, :choki, :pah].shuffle.pop
  end

  def match(other)
    match = [@hand, other.hand]
    case
    when @hand == other.hand; :even
    when match == [:goo, :choki] ||
        match == [:choki, :pah] ||
        match == [:pah, :goo]; :win
    else :lose
    end
  end

  def to_s
    NAME[@hand]
  end
end

if __FILE__ == $0
  left = Janken.new
  right = Janken.new

  10.times {
    case left.versus(right)
    when :win
      puts "左の人が勝ちました。右「#{right}」左「#{left}"
    when :lose
      puts "右の人が勝ちました。右「#{right}」左「#{left}"
    when :even
      puts "あいこでした。#{left}"
    end
  }
end

さらに疑問が。

Jankenクラスがversusを呼び出すたびに内部の状態を変えていくというのは違和感を覚える。Jankenクラスは手の状態を持っているべきではあっても、同じインスタンスがどんどん状態を変えていくのが良いAPIとは思えない。ある人がグーを出し、次にチョキを出した時、この2つが同じオブジェクトではマズイ。

むしろ設問にあるJankenクラスというのは、Playerクラスであるべきで、Janken#versusというのは、Player#jankenとでもしたほうがジャンケンの自然な表現。だから、2つのクラスに分けて、

# -*- coding: utf-8 -*-
class Player
  attr_accessor :hand

  def initialize
    @hand = Janken.new
  end

  def janken(other)
    case @hand.match(other.hand)
    when :win
      puts "私の勝ちです。私「#{@hand}」相手「#{other.hand}"
    when :lose
      puts "私の負けです。私「#{@hand}」相手「#{other.hand}"
    when :even
      puts "あいこでした。#{@hand}"
    end
  end
end

class Janken
  attr_reader :hand

  NAME = {
    goo:   "グー",
    choki: "チョキ",
    pah:   "パー"
  }

  def initialize
    @hand = [:goo, :choki, :pah].shuffle.pop
  end

  def match(other)
    match = [@hand, other.hand]
    case
    when @hand == other.hand; :even
    when match == [:goo, :choki] ||
        match == [:choki, :pah] ||
        match == [:pah, :goo]; :win
    else :lose
    end
  end

  def to_s
    NAME[@hand]
  end
end

if __FILE__ == $0
  me = Player.new
  opponent = Player.new

  5.times {
    me.janken opponent
    me.hand = Janken.new
    opponent.hand = Janken.new
  }
end

とでもしたほうがいい。

ただ、2手目以降で外部からJanken.newをPlayerインスタンスに突っ込むのはちょっとヘン。Playerクラスが常にJanken.newをするほうが良い。出す手を考えるのはPlayerだし。考えてみるとジャンケンというのは一連の手のシーケンスなので、以下のようにEnumeratorを使ったほうが、より現実のジャンケンの表現として適切かもしれない。

# -*- coding: utf-8 -*-
class Player
  def hand_seq
    @hand_seq ||= Enumerator.new do |y|
      loop { y << Janken.new }
    end
  end

  def janken(other)
    hand       = hand_seq.peek
    other_hand = other.hand_seq.peek

    case hand.match(other_hand)
    when :win
      puts "私の勝ちです。私「#{hand}」相手「#{other_hand}"
    when :lose
      puts "私の負けです。私「#{hand}」相手「#{other_hand}"
    when :even
      puts "あいこでした。#{hand}"
    end
  end
end

class Janken
  attr_reader :hand

  NAME = {
    goo:   "グー",
    choki: "チョキ",
    pah:   "パー"
  }

  def initialize
    @hand = [:goo, :choki, :pah].shuffle.first
  end

  def match(other)
    match = [@hand, other.hand]
    case
    when @hand == other.hand; :even
    when match == [:goo, :choki] ||
        match == [:choki, :pah] ||
        match == [:pah, :goo]; :win
    else :lose
    end
  end

  def to_s
    NAME[@hand]
  end
end

if __FILE__ == $0
  me = Player.new
  opponent = Player.new

  5.times {
    me.janken opponent

    me.hand_seq.next
    opponent.hand_seq.next
  }
end

Enumeratorを使うのはいいとしても、外部にそのまま見せるのはマズイ。peekって何だよという感じ。こういうときは必要なメソッドにだけ適当な名前を付けてデリゲート。

# -*- coding: utf-8 -*-
require 'forwardable'

class Player
  extend Forwardable

  def initialize
    @hand_seq = Enumerator.new do |y|
      loop { y << Janken.new }
    end
  end

  def_delegator :@hand_seq, :peek, :hand
  def_delegator :@hand_seq, :next, :next!

  def janken(other)
    case hand.match(other.hand)
    when :win
      puts "私の勝ちです。私「#{hand}」相手「#{other.hand}"
    when :lose
      puts "私の負けです。私「#{hand}」相手「#{other.hand}"
    when :even
      puts "あいこでした。#{hand}"
    end
  end
end

class Janken
  attr_reader :hand

  NAME = {
    goo:   "グー",
    choki: "チョキ",
    pah:   "パー"
  }

  def initialize
    @hand = [:goo, :choki, :pah].shuffle.first
  end

  def match(other)
    match = [@hand, other.hand]
    case
    when @hand == other.hand; :even
    when match == [:goo, :choki] ||
        match == [:choki, :pah] ||
        match == [:pah, :goo]; :win
    else :lose
    end
  end

  def to_s
    NAME[@hand]
  end
end

if __FILE__ == $0
  me = Player.new
  opponent = Player.new

  5.times {
    me.janken opponent

    me.next!
    opponent.next!
  }
end

ここまで来ると滅茶苦茶という気がしなくもない。

たかがジャンケンプログラムごときで大げさ。グーチョキパーを [0, 1, 2] で表現して、それを比較するif文をずらっと並べればいい話で、クラスなんて1つも作ることなんてないという気もする。いや、必要なのはハッシュテーブル1つと、2つのジャンケンを受け取って評価する関数の1つだけか。このあいだYouTubeで見たPythonな人のトークで、データ構造で済むものにイチイチ名前を付けたりクラスにしたりするなよという主張に、それはあるかもなと思ったりしている。PythonRubyで文化の違いがありそう。しかし、慣れてみたらクラスは便利で、別にコード行数が増えすぎるとか読みづらくなるというわけでもない。実行効率も多くの場合、全くどうでもいい話で、むしろ大事なのは人間が認識するインターフェースはどうなっているのかというところ。問題の捉え方をモジュールに分けることで読み手に示し、関連するコードをまとめる場としてクラスという容れ物を使うのは、すごく良いことに思えるのだよな。