@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이 부여됨.(마지막에 한 번 더 던질 수 있다.)
기본 설계
이 때 공감가는 말씀을 하셨는데,
계획대로 설계대로 흘러가진 않아도, 꼭 설계를 작성하고 꼭 이대로 가기 보다는 청사진, 밑그림 정도로만 생각하고 따라가자. 계획대로 안된다고 계획을 짜지 않으면, 계획을 짤 때보다 더 계획대로 안된다.
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에서는 수정할 것이 없이 잘 동작하는 것을 볼 수 있다.
'TDD, CleanCode' 카테고리의 다른 글
클린코더스 강의 리뷰 5. TDD 예제 (wordWrap) (0) | 2019.05.21 |
---|---|
클린코더스 강의 리뷰 5. TDD 예제 (Prime factors) (0) | 2019.05.21 |
클린코더스 강의 리뷰 5. TDD의 기본 예시 (0) | 2019.05.13 |
클린코더스 강의 리뷰 4. Forms (0) | 2019.05.11 |
클린코더스 강의 리뷰 3. Function Structure(2) (0) | 2019.05.10 |