본문 바로가기

TDD, CleanCode

클린코더스 강의 리뷰 5. TDD 예제 (Bowling Game)

 

    @Test
    public void perfectGame() {
        rollMany(10,10);
        game.roll(10);
        game.roll(10);
        assertThat(game.getScore(),is(300));
    }

깃 주소 : https://github.com/jiwon3346/bowling-game

Bowling Game

규칙은 다음과 같다.

< 볼링 게임 규칙 >

규칙

 

1. 볼링 게임은 10개의 프레임으로 구성된다.

2. 각 프레임은 대게 2roll 을 가진다.(1개의 프레임에서 10개의 핀을 쓰러 뜨리기 위해 2번의 기회를 갖는다.)

3. Spare : 10 + next first roll에서 쓰러 뜨린 핀수.

4 . Strike : 10 + new two rolls에서 쓰러 뜨린 핀수.

5. 10번째 프레임은 spare 처리시에 3번의 roll이 부여됨.(마지막에 한 번 더 던질 수 있다.)

 

기본 설계 

 

 

< 자세한 설계는 git의 md를 확인하자. >

이 때 공감가는 말씀을 하셨는데,

계획대로 설계대로 흘러가진 않아도, 꼭 설계를 작성하고 꼭 이대로 가기 보다는 청사진, 밑그림 정도로만 생각하고 따라가자. 계획대로 안된다고 계획을 짜지 않으면, 계획을 짤 때보다 더 계획대로 안된다.

 

TDD

1. Game 클래스의 생성

    @Test
    public void canMakeClass() {
        Game game = new Game();
    }

2. roll 메소드의 생성

    @Test
    public void roll () {
        Game game = new Game();
        game.roll(0);
    }

 

1과 2를 통해서 game 클래스 생성의 중복이 발생했다.

fieldValue로 올려 중복을 제거한다.

    private Game game;

    @Before
    public void setUp() throws Exception {
        game = new Game();
    }

    @Test
    public void roll () {
        Game game = new Game();
        game.roll(0);
    }

이 때 할당은 setUp에서 한다.

 

3. getScore를 생성 . (전부 거터일 경우)

    @Test
    public void gutterGame() {
        for(int i = 0; i < 20; i++)
            game.roll(0);
        assertThat(game.getScore(), is(0));
    }
    
    //Game클래스
    public int getScore() {
        return 0;
    }

getScore를 생성해야 한다. 이 때 가장 쉬운 경우부터 순서대로 생성한다.

 

3-1 전부 1점일 경우 생성

    @Test
    public void allOnes() {
        for(int i = 0; i < 20; i++)
            game.roll(1);
        assertThat(game.getScore(), is(20));
    }

  public class Game {
      private int score = 0;

      public void roll(int pins) {
          this.score += pins;
      }

      public int getScore() {
          return this.score;
      }
  }

이제는 0을 반환할 수 만은 없다. 다음과 같이 메소드가 변경된다.

 

3-2 프레임 중복 제거

    private void rollMany(int frame, int pins) {
        for (int i = 0; i < frame; i++) {
            game.roll(pins);
        }
    }
    @Test
    public void gutterGame() {
        rollMany(20, 0);
        assertThat(game.getScore(), is(0));
    }
    

    @Test
    public void allOnes() {
        rollMany(20, 1);
        assertThat(game.getScore(), is(20));
    }

20프레임동안 핀을 넘어뜨리는 부분이 중복되어 있다.

extract 메소드한다. 필요하다면 inline한다.

 

4. spare 처리

    @Test
    public void oneSpare() {
        game.roll(5);
        game.roll(5);
        game.roll(3);
        rollMany(17,0);
        assertThat(game.getScore(),is(16));
    }

테스트 코드는 다음과 같다.

첫번째 프레임에서 스페어 처리를 했기 때문에 (5 + 5) + 다음 첫 roll 의 점수(3) = 13점

두번째 프레임 3 + 0, 나머지 0 점

해서 총 16점이다.

 

그런데, 생각보다 더 복잡하다.

만약 첫번째 두번째 롤이 10점이면 스페어가 되고

만약 전이 스페어라면 다음 roll의 점수를 저장해야 하고...

 

문제는 보통 디자인 원칙이 위배 된 경우라고 한다.

이를 해결하고자 두가지 방법을 취하는데,

 

1. 다시만든다.

2. 해당 테스트를 @Ignore 한 후에, 전체적인 디자인을 현재 코드가 돌아가는 선 내에서 재구성한다.

 

2번을 이용하여 재구성한다.

4-1

public class Game {
    private int[] rolls = new int[21];
    private int currentRoll = 0;

    public void roll(int pins) {
        rolls[currentRoll++] = pins;
    }

    public int getScore() {
        int score = 0;
        for (int frame = 0; frame < 10; frame++) {
            score += rolls[frame * 2] + rolls[frame * 2 + 1];
        }
        return score;
    }
}

사실상 스코어를 계산하는 것은 roll의 역할은 아니다.

roll은 핀의 갯수를 기억만 하고

getScore에서 계산하게 코드를 수정했다. 이 때 반복문의 루브변수가 frame이라는 점도 인상깊다.

 

4-2 그래서 Spare 처리하기

    public int getScore() {
        int score = 0;
        for (int frame = 0; frame < 10; frame++) {
            if(rolls[frame * 2] + rolls[frame * 2 + 1] == 10){
                score += 10 + rolls[(frame+1) * 2];
            } else {
                score += rolls[frame * 2] + rolls[frame * 2 + 1];
            }
        }
        return score;
    }

만약 어떤 프레임의 합이 10인 경우,

score는 10점 만점 + 다음 프레임의 첫번째 roll이다.

 

4-3 Rename

    public int getScore() {
        int score = 0;
        for (int frame = 0; frame < 10; frame++) {
            int firstRollInFrame = frame * 2;
            int secondRollInFrame = frame * 2 + 1;
            if(rolls[firstRollInFrame] + rolls[secondRollInFrame] == 10){
                score += 10 + rolls[(frame+1) * 2];
            } else {
                score += rolls[firstRollInFrame] + rolls[secondRollInFrame];
            }
        }
        return score;
    }

프레임의 첫번째 롤과 두번째 롤을 변수로 만들었다.

 

4-4 아예 가정문을 추출하기

    public int getScore() {
        int score = 0;
        for (int frame = 0; frame < 10; frame++) {
            if(isSpare(frame)){
                score += 10 + rolls[(frame+1) * 2];
            } else {
                score += rolls[frame * 2] + rolls[frame * 2 + 1];
            }
        }
        return score;
    }

    private boolean isSpare(int frame) {
        return rolls[frame * 2] + rolls[frame * 2 + 1] == 10;
    }

 

5. Strike 처리

strike처리시에는 frame을 바로 건너 뛴다.

이것을 처리 하기 쉽지 않기 때문에 다음과 같이 재구성한다.

    public int getScore() {
        int score = 0;
        int fristRollInFrame = 0;
        for (int frame = 0; frame < 10; frame++) {
            if(isSpare(fristRollInFrame)){
                score += 10 + rolls[fristRollInFrame + 2];
                fristRollInFrame += 2;
            }else if(rolls[fristRollInFrame] == 10){
                score += 10 + rolls[fristRollInFrame + 1] + rolls[fristRollInFrame +2] ;
                fristRollInFrame += 1;
            } else {
                score += rolls[fristRollInFrame] + rolls[fristRollInFrame+1];
                fristRollInFrame += 2;
            }
        }
        return score;
    }

roll를 fristRollInFrame으로 따로 조절하기로 햇다.

strike일땐 하나를 아닐때는 두개씩 넘어간다.

하나의 프레임에서는 첫번째 롤이 fristRollInFrame 두번째 롤이 fristRollInFrame +1 이 된다.

 

6. Perfect 게임

    @Test
    public void perfectGame() {
        rollMany(11,10);
        game.roll(10);
        assertThat(game.getScore(),is(300));
    }

10게임을 마무리 하고, 11번째 프레임까지 마무리 한 후에

10점을 추가받는다.

 

규칙을 잘 적용해씩 때문에 다음 perfectGame에서는 수정할 것이 없이 잘 동작하는 것을 볼 수 있다.