Clean Craftmanship やってみた
QueueをTDDで実装。
Queue程度ならTDDで実装する必要無い?と思ったりしたが、Exception 処理などに対してstep by stepで気付ける、挙動を掴みながら実装を積んでいけるのは良い点。
またインタフェースを変えずにリファクタリングする練習になってくる。
# queue.rb
require_relative './QueueUnderflowError'
class Queue
def initialize
@elements = []
end
def isEmpty?
false
end
def push(element)
@elements << element
end
def pop
raise QueueUnderflowError if @elements.length == 0
@elements.shift
end
def getSize
@elements.length
end
end
# test_queue_spec.rb
require_relative '../src/Craftman/Queue'
require_relative '../src/Craftman/QueueUnderflowError'
RSpec.describe 'queue' do
before do
@queue = Queue.new
end
it 'can one push ' do
expect(@queue.isEmpty?).to eq false
end
it 'can pop one' do
@queue.push(0)
expect(@queue.pop).to eq 0
end
it 'can getSize' do
queue = Queue.new
queue.push(0)
expect(queue.getSize).to eq 1
end
it 'can getSize if twice pushed' do
queue = Queue.new
queue.push(0)
queue.push(0)
expect(queue.getSize).to eq 2
end
it 'can getSize if one push and one pop' do
queue = Queue.new
queue.push(0)
queue.pop
expect(queue.getSize).to eq 0
end
it 'can throw error if empty pop' do
queue = Queue.new
expect{queue.pop}.to raise_error(QueueUnderflowError)
end
it 'can two push and pop is ordered' do
queue = Queue.new
queue.push(0)
queue.push(1)
expect(queue.pop).to eq 0
expect(queue.pop).to eq 1
end
end
# QueueUnderflowError.rb
class QueueUnderflowError < RuntimeError
end
Queueを練習問題として...Stackを同じ手順で実装
実際的にはほぼ変わらないのだが、やはり似たような設計になる。
また大して変わらない。
# Stack.rb
class Stack
def initialize
@empty = true # boolean
@elements = []
@size = 0 #int
end
def isEmpty?
@size == 0
end
def push(element)
@size += 1
@elements << element
end
def pop()
raise StackUnderflow if @size == 0
@size -= 1
@elements.pop
end
def getSize
@size
end
end
# # test_stack_spec.rb
require_relative '../src/Craftman/Stack'
require_relative '../src/Craftman/StackUnderflow'
RSpec.describe 'stack' do
it 'canCreateStack' do
stack = Stack.new
expect(stack.isEmpty?).to eq true
end
it 'afterOnePush_isNotEmptyStack' do
stack = Stack.new
stack.push(0);
expect(stack.isEmpty?).to eq false
end
it 'afterTwoPushAndPop_isEmpty' do
stack = Stack.new
stack.push(0);
stack.push(0);
stack.pop()
expect(stack.isEmpty?).to eq false
end
it 'afterTwoPushes_sizeIsTwo' do
stack = Stack.new
stack.push(0)
stack.push(0)
expect(stack.getSize).to eq 2
end
it 'afterOnePushAndOnePop_isEmpty' do
stack = Stack.new
stack.push(0)
stack.pop
expect(stack.isEmpty?).to eq true
expect(stack.getSize).to eq 0
end
it 'afterOnePushAndPop_isEmpty' do
stack = Stack.new
stack.push(0)
expect(stack.isEmpty?).to eq false
expect(stack.getSize).to eq 1
end
it 'poppingEmptyStack_throwsUnderflow' do
stack = Stack.new
expect{stack.pop}.to raise_error(StackUnderflow)
end
it 'afterPushingX_willPopX' do
stack = Stack.new
stack.push(99)
expect(stack.pop).to eq 99
stack.push(88)
expect(stack.pop).to eq 88
end
it 'afterPushingXandY_willPopYandX' do
stack = Stack.new
stack.push(99)
stack.push(88)
expect(stack.pop).to eq 88
expect(stack.pop).to eq 99
end
end
StackとQueueを取りまとめて扱う場合はどうするか。
例えば OrderList Obstractクラスか OrderList interfaceを作る。
それを継承して取りまとめてインタフェースを限定し、実装クラスとしてQueueとStackを実装する。
とかにすると扱いやすそうだなー。StackとQueueを入れ替えて使いたいケースとかさほど無いけど挙動が読みやすくなるとかはありそう。rubyだとあんまりきっちり絞れない気がするけど。
Bowlingを実装。
ボーリングのルール知らなすぎたw
ざっくりまとめると
- ストライクの場合は
- 10点 + ストライク後の1投目、2投目の合計点数
- スペアの場合は
- 10点 + スペア後1投目の合計点数
- それ以外は
- 1投目、2投目の合計点数
実装を見るに「呼び出し元は”ゲームに対して投げること(@g.roll)”しか知らない」っぽい。
確かにプレイヤーは一切の知識がない状態でレーンに玉を投げる。
で、ゲーム終了後にスコアが出てくる、という振る舞いが見える。
FrameIndexがイマイチ分かりづらいんだよな。。。
# Bowling.rb
require_relative '../src/Craftman/Bowling/Game'
RSpec.describe 'BowlingTest' do
before do
@g = Game.new
end
def rollMany(n, pins) # int, int
(1..n).each do |i|
@g.roll(pins);
end
end
def rollSpare
rollMany(2, 5)
end
def rollStrike
@g.roll(10)
end
it 'gutterGame' do
rollMany(20, 0)
expect(@g.score).to eq 0
end
it 'allOnes' do
rollMany(20, 1)
expect(@g.score).to eq 20
end
it 'oneSpare' do
rollSpare();
@g.roll(7)
rollMany(17, 0)
expect(@g.score).to eq 24
end
it 'oneStrike' do
rollStrike();
@g.roll(2)
@g.roll(3)
rollMany(16, 0)
expect(@g.score).to eq 20
end
it 'perfectGame' do
rollMany(12, 10)
expect(@g.score).to eq 300
end
end
# test_bowling_spec.rb
require_relative '../src/Craftman/Bowling/Game'
RSpec.describe 'BowlingTest' do
before do
@g = Game.new
end
def rollMany(n, pins) # int, int
(1..n).each do |i|
@g.roll(pins);
end
end
def rollSpare
rollMany(2, 5)
end
def rollStrike
@g.roll(10)
end
it 'gutterGame' do
rollMany(20, 0)
expect(@g.score).to eq 0
end
it 'allOnes' do
rollMany(20, 1)
expect(@g.score).to eq 20
end
it 'oneSpare' do
rollSpare();
@g.roll(7)
rollMany(17, 0)
expect(@g.score).to eq 24
end
it 'oneStrike' do
rollStrike();
@g.roll(2)
@g.roll(3)
rollMany(16, 0)
expect(@g.score).to eq 20
end
it 'perfectGame' do
rollMany(12, 10)
expect(@g.score).to eq 300
end
end
上記のルールに対して、以下の概念を追加するとロジックに複雑性が出る。
- ボーリング特有の「フレーム」という概念
- ゲーム途中での計算
この辺は実装してみると面白いかも。
Frameという概念を追加してみているが難しい。
Frameを追加する。
フレームは自分の番号とそれぞれの投球結果を持っている。
# Frame.rb
class Frame
attr_reader :rolls, :index
@@last_index = 9
@rolls # int[]
@index
def initialize(index)
@rolls = []
@index = index
@score = 0
end
def roll(pins)
if @rolls.length > 3
throw Error('1つのFrameで扱えるロール数を超えました')
end
@rolls.push(pins)
end
def self.last_index
@@last_index
end
def isStrike
return false if @rolls.empty?
@rolls.first == 10
end
def isSpare
return false unless isTwiceRoll
@rolls[0] + @rolls[1] == 10
end
def isTwiceRoll
@rolls.length == 2
end
def isLast
@index == @@last_index
end
def firstRoll
@rolls[0]
end
def secondRoll
@rolls[1]
end
def thirdRoll
@rolls[2]
end
end
次にFrameの振る舞いを扱うFrameManagerを作った。
なんとFrameを追加するだけでこんなにも長くなるのである。
require_relative './Frame'
class FrameManager
@@last_frame_index = 9
def initialize
@frames = [] # Frame[]
@currentFrame = genFrame()
end
def roll(pins)
@currentFrame = checkOrGenFrame()
@currentFrame.roll(pins)
end
def getFrames
@frames + [@currentFrame]
end
def getFrameScoreOneToEight(frame)
score = 0
frames = getFrames()
f1 = frame
f2 = frames[frame.index+1]
f3 = frames[frame.index+2]
if f1.isStrike
score += 10
if f2.isStrike
score += 10 + f3.firstRoll
return score
end
score += f2.firstRoll + f2.secondRoll
return score
elsif f1.isSpare
score += 10 + f2.firstRoll
return score
end
return score += f1.firstRoll + f1.secondRoll
end
def getFrameScoreNine(frame)
score = 0
frames = getFrames()
f1 = frame
f2 = frames[frame.index+1]
if f1.isStrike
score += 10 + f2.firstRoll + f2.secondRoll
return score
elsif f1.isSpare
score += 10 + f2.firstRoll
return score
end
return score += f1.firstRoll + f1.secondRoll
end
def getFrameScoreTen(frame)
score = 0
frames = getFrames()
f1 = frame
if f1.isStrike
score += 10 + f1.secondRoll + f1.thirdRoll
return score
elsif f1.isSpare
score += 10 + f1.thirdRoll
return score
end
return score += f1.firstRoll + f1.secondRoll
end
def getFrameScore(frame)
case frame.index
when 0..7
return getFrameScoreOneToEight(frame)
when 8
return getFrameScoreNine(frame)
when 9
return getFrameScoreTen(frame)
end
end
def genFrame
if @frames.length == @@last_frame_index
return Frame.new(Frame.last_index)
end
Frame.new(@frames.length)
end
def checkOrGenFrame # Frame
if @currentFrame.isLast
return @currentFrame
end
if @currentFrame.isStrike
@frames.push(@currentFrame)
return genFrame()
end
if @currentFrame.isSpare
@frames.push(@currentFrame)
return genFrame()
end
if @currentFrame.isTwiceRoll
@frames.push(@currentFrame)
return genFrame()
end
@currentFrame
end
end