じゃんけんごときにクラスなんて要るのか
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な人のトークで、データ構造で済むものにイチイチ名前を付けたりクラスにしたりするなよという主張に、それはあるかもなと思ったりしている。PythonとRubyで文化の違いがありそう。しかし、慣れてみたらクラスは便利で、別にコード行数が増えすぎるとか読みづらくなるというわけでもない。実行効率も多くの場合、全くどうでもいい話で、むしろ大事なのは人間が認識するインターフェースはどうなっているのかというところ。問題の捉え方をモジュールに分けることで読み手に示し、関連するコードをまとめる場としてクラスという容れ物を使うのは、すごく良いことに思えるのだよな。