Antefatto
Poco prima di Natale (2004) Ron Jeffries pubblica sul suo sito una serie di articoli in cui implementa per l’ennesima volta un sistema che calcola lo score di una partita di bowling. Il tutto prende le mosse dagli XP Immersion di zio Bob, ovvero anima e mente di ObjectMentor ==> check http://www.objectmentor.com/resources/articles/xpepisode.htm
Questa volta Ron Jeffries vuole ritornare a usare smalltalk, inizia a scrivere e contemporaneamente riceve feedback sulla mailing list XP. Per cui la serie di articoli si allunga:
- Discovering Better Code: Bowling For Smalltalk
- DBC: Bowling For Smalltalk II
- DBC: I Was Framed!
- DBC: Keep on Rollin’
- DBC: With a Little Help From My Friends
- DBC: Another Frame
- DBC: An Example Refactored
- DBC: StreamBowlingGame
E ora ruby!
Mi sono subito detto: perche’ non seguire i suoi passi, uno alla volta, pero’ in ruby anziche’ smalltalk? Ebbene, ci sono riuscito, nonostante non avessi un refactoring browser (e naturalmente niente RubyLint, e niente sorgente di un metodo disponibile programmaticamente, ma quantomeno quest’ultimo e’ un aspetto secondario). Per scrivere il codice e i test e rifattorizzare ho usato un po’ Eclipse (con RDT) e un po’ Arachno Ruby. Quest’ultimo ha il pregio di essere piu’ leggero di Eclipse e per certi versi piu’ veloce, ma non sono mai riuscito a far funzionare il debugger; Eclipse invece ha un ottimo code completion e una buona integrazione con il debugger – e’ solo un po’ troppo fat.
Mi manca molto il refactoring browser, pero’ vabbe’. Il search & replace di Arachno e’ molto carino, aiuta. Un po’ manca anche l’assenza dei reference a un metodo.
Alla fin fine ho rifattorizzato anche i test in un’unica classe, con istanziazione parametrica della classe che implementa il calcolo vero e proprio (se segui gli articoli ne ottieni 3 versioni distinte). Comunque sia, leggetevi gli articoli, altrimenti quanto scrivo non ha molto senso.
- test_bowling.rb
class TestBowling < Test::Unit::TestCase def setup @game = GAME.new end def test_all_gutters 20.times { @game.roll 0 } assert_equal(0, @game.score) end def test_all_open 10.times { @game.roll 5; @game.roll 4 } assert_equal(90, @game.score) end def test_spare @game.roll 4 @game.roll 6 @game.roll 5 @game.roll 4 8.times { @game.roll 0; @game.roll 0 } assert_equal(24, @game.score) end def test_strike @game.roll 10 @game.roll 5 @game.roll 4 @game.roll 3 @game.roll 0 7.times { @game.roll 0; @game.roll 0 } assert_equal(31, @game.score) end def test_perfect 12.times { @game.roll 10 } assert_equal(300, @game.score) end def test_alternating 5.times { @game.roll 10; @game.roll 6; @game.roll 4 } @game.roll 10 assert_equal(200, @game.score) end def test_complex_open rolls = [1, 1, 1, 2, 2, 1, 2, 3, 3, 2, 1, 4, 4, 1, 1, 5, 5, 1, 1, 6] rollSum = rolls.inject(0) {|sum, each| sum + each} rolls.each {|i| @game.roll i} assert_equal(rollSum, @game.score) end end
- bowling.rb
require 'test/unit' require 'test_bowling' class BowlingGame def initialize @rolls = [] end def roll(pins) @rolls.push pins end def score frames.total_score end def frames firstFrame = RackFrame.ten_frame_list @rolls.each { |pins| firstFrame.roll pins} firstFrame end end class RackFrame def initialize @state = :first_roll @next = nil end def RackFrame.ten_frame_list frame = RackFrame.new 9.times { frame = RackFrame.another(frame) } frame end attr_reader :score attr_accessor :next def roll(pins) send(@state, pins) end def RackFrame.another(aFrame) frame = RackFrame.new frame.next = aFrame frame end def total_score score + (@next.nil? ? 0 : @next.total_score) end private def first_roll(pins) @score = pins if @score == 10 @state = :first_strike_bonus else @state = :second_roll end end def first_strike_bonus(pins) @score += pins next_roll pins @state = :second_strike_bonus end def second_roll(pins) @score += pins if @score == 10 @state = :spare_bonus else @state = :satisfied end end def second_strike_bonus(pins) @score += pins next_roll pins @state = :satisfied end def spare_bonus(pins) @score += pins next_roll pins @state = :satisfied end def satisfied(pins) next_roll pins end def next_roll pins @next.roll pins unless @next.nil? end end class TestFrame < Test::Unit::TestCase def setup @frame = RackFrame.ten_frame_list end def test_open @frame.roll 4 @frame.roll 5 @frame.roll 1 @frame.roll 2 assert_equal(9, @frame.score) end def test_spare @frame.roll 9 @frame.roll 1 @frame.roll 5 @frame.roll 4 assert_equal(15, @frame.score) end def test_spare_with_zero @frame.roll 9 @frame.roll 1 @frame.roll 0 @frame.roll 4 assert_equal(10, @frame.score) end def test_strike @frame.roll 10 @frame.roll 10 @frame.roll 10 @frame.roll 5 assert_equal(30, @frame.score) end def test_frame_string count = 0 currentFrame = @frame until currentFrame.nil? count += 1 currentFrame = currentFrame.next end assert_equal(10, count) end def test_ten_open_frames 10.times { @frame.roll 4; @frame.roll 3 } assert_equal(70, @frame.total_score) end def test_second_frame @frame.roll 5 @frame.roll 4 @frame.roll 3 @frame.roll 2 assert_equal(9, @frame.score) assert_equal(5, @frame.next.score) end def test_perfect 12.times { @frame.roll 10 } assert_equal(300, @frame.total_score) end end TestBowling.const_set :GAME, BowlingGame - bowling2.rb
require 'test/unit' require 'test_bowling' class BowlingGame2 def initialize @rolls = [] @score = 0 @frameStart = 0 end def roll(pins) @rolls.push pins end def score number_of_frames.times { @score += frame_score update_frame_start } @score end private def number_of_frames 10 end def all_pins_down 10 end def frame_score frame_score = roll1 + roll2 frame_score += roll3 if bonus_frame? frame_score end def update_frame_start @frameStart += strike? ? 1 : 2 end def bonus_frame? spare? || strike? end def roll1 @rolls[@frameStart] end def roll2 @rolls[@frameStart+1] end def roll3 @rolls[@frameStart+2] end def strike? roll1 == all_pins_down end def spare? roll1 < all_pins_down && roll1+roll2 == all_pins_down end end TestBowling.const_set :GAME, BowlingGame2 - bowling_stream.rb
require 'test/unit' require 'test_bowling' class StreamBowlingGame def initialize @rolls = [] end def roll(pins) @rolls.push pins end def score @index = -1 total = 0 10.times { total += score_frame } total end def next @index += 1 @rolls[@index] end private def score_frame if spare? self.next + self.next + peek elsif strike? self.next + peek + peek_second else self.next + self.next end end def spare? peek + peek_second == 10 end def strike? peek == 10 end def peek @rolls[@index+1] end def peek_second @rolls[@index+2] end end TestBowling.const_set :GAME, StreamBowlingGame