美しいプログラムコードとは。

「まずは、次のサイトにアクセスして、プログラムコードから仕様を理解してみて欲しい。」


さて、何がしたいプログラムなのか分かっただろうか? テニスを知ってる人なら分かるかもしれないが、知らない人には、どちらのソースもなかなかやっかいな代物だったのではないだろうか?

"これらはいずれも同じユニットテストに合格している。" とあるのだが、実は両者の振る舞いが違う部分がある。さらに、バグがあったりもするのだ。しかも、お手本の方に…。(^^; まあ、それはともかく、話題は "クリーンなコード" である。

見やすいかどうかが話題になると、コーディングルールが話題になったりするが、実際のところ、コードになる前の段階の話として、対象となるものを単純な問題に変換 (問題の切り分けや、モデル化) して解決しているかどうかが大きい。どれだけ文章を整えようが、練られていない話は面白くないように、問題を整理していないものは、見かけ上のコードを整理したところで、大きな改善効果は期待できないだろう。

んで、件のプログラムコードを書き直してみた。Java はブランクがあってすらすら書けないので、ruby で表現した。オリジナルに近い表現をしたつもりだが、ruby の特徴を使っている部分もあり、知らない人には読み難いかもしれない。それでも、あえてコメントを記入せず、また、ここでの補足説明もすることなく、件の記事と同様に、プログラムコードだけで表現することを試みる。論より証拠となるといいのだが…。

class Player 
  attr_accessor :games
  attr_reader :name
  def initialize(name) 
    @name = name 
    @games = 0 
  end 
  def won
    @games += 1 
  end 
end 

CONDITIONS = [{:COM=>7,:ADV=>1},
              {:COM=>6,:ADV=>2}] 
MSG = {:OVER=>"%s wins the set %s - %s",
       :TIE=>"Set is tied at %s",
       :PLAYING=>"%s leads %s - %s"}

def games_as_string(player1, player2)
  temp   = [player1, player2].sort{|a, b| a.games <=> b.games} 
  other  = temp.shift 
  leader = temp.shift 

  status = :PLAYING 
  status = :TIE if leader.games == other.games 
  CONDITIONS.each {|con| 
    if con[:COM] == leader.games &&
       con[:ADV] <= (leader.games - other.games)
      status = :OVER
    end
  } 

  if status == :TIE 
    options = leader.games
  else 
    options = [leader.name, leader.games, other.games]
 end 

  return MSG[status] % options 
end

# テスト

p1 = Player.new("Player1")
p2 = Player.new("Player2")

(0..7).each {|i|
  (0..7).each {|j|
    p1.games = i
    p2.games = j
    
    puts "[#{i}:#{j}] #{games_as_string(p1, p2)}"
  }
}

もう一つ。こちらは友人のヒントを基にして、別の方法で実現してみたもの。

NAMES = {
  true => "Player1",
  false => "Player2"
}
MSG = { 
  :OVER => "%s wins the set %s - %s", 
  :TIE => "Set is tied at %s", 
  :PLAYING => "%s leads %s - %s"
} 

def get_set_score(games1, games2)
  statuses = 
  8.times {|i| statuses << Array.new(8, :PLAYING)}

  (0..6).each {|i| 
    statuses[6][i] = :OVER if i < 5 
    statuses[i][6] = :OVER if i < 5 
    statuses[7][i] = :OVER if i >= 5 
    statuses[i][7] = :OVER if i >= 5 
    statuses[i][i] = :TIE 
  } 

  status = statuses[games1][games2] 

  options = 
  if status == :TIE 
    options = [games1] 
  else
    options << NAMES[games1>games2]
    options << [games1, games2].sort.reverse
  end

  return MSG[status] % options.flatten
end

# テスト

(0..7).each {|i|
  (0..7).each {|j|
    puts "[#{i}:#{j}] #{get_set_score(i, j)}"
  }
}