Open7

Clean Craftmanship やってみた

ShuzoNShuzoN

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
ShuzoNShuzoN

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
ShuzoNShuzoN

StackとQueueを取りまとめて扱う場合はどうするか。

例えば OrderList Obstractクラスか OrderList interfaceを作る。
それを継承して取りまとめてインタフェースを限定し、実装クラスとしてQueueとStackを実装する。

とかにすると扱いやすそうだなー。StackとQueueを入れ替えて使いたいケースとかさほど無いけど挙動が読みやすくなるとかはありそう。rubyだとあんまりきっちり絞れない気がするけど。

ShuzoNShuzoN

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

上記のルールに対して、以下の概念を追加するとロジックに複雑性が出る。

  • ボーリング特有の「フレーム」という概念
  • ゲーム途中での計算

この辺は実装してみると面白いかも。

ShuzoNShuzoN

Frameという概念を追加してみているが難しい。

ShuzoNShuzoN

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
ShuzoNShuzoN

次に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