Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c0abb19
docs : add functional list
ray-yhc Mar 25, 2023
f449f83
test : add UserOutput test code
ray-yhc Mar 25, 2023
714d713
test : add UserInput test code
ray-yhc Mar 25, 2023
3ac7ecc
test : add SingleGame test code
ray-yhc Mar 25, 2023
292c25c
test : edit UserOutput test code
ray-yhc Mar 25, 2023
e4f003c
test : edit UserInput test code
ray-yhc Mar 25, 2023
6e8eec3
test : edit SingleGame test code
ray-yhc Mar 25, 2023
6b2fc50
feat : implement SingleGame
ray-yhc Mar 25, 2023
dcc759f
feat : implement UserInput
ray-yhc Mar 25, 2023
483d073
feat : implement UserOutput
ray-yhc Mar 25, 2023
6a580a1
feat : implement Application
ray-yhc Mar 25, 2023
9a837a4
feat : add regex to check user input
ray-yhc Mar 25, 2023
d6fb92a
feat : add code for checking exceptions in UserInput
ray-yhc Mar 25, 2023
756a633
test : add GameBall test
ray-yhc Mar 27, 2023
951f6b7
test : delete UserInputTest.java
ray-yhc Mar 27, 2023
2169607
chore : edit method name
ray-yhc Mar 27, 2023
3198cf6
refactor : edit output type
ray-yhc Mar 27, 2023
fec8802
refactor : edit UserInput return type
ray-yhc Mar 27, 2023
9336696
refactor : refactor game code
ray-yhc Mar 27, 2023
d5d7ade
test : add GameCommand test
ray-yhc Mar 27, 2023
4472181
feat : add GameCommand
ray-yhc Mar 27, 2023
cc19a60
feat : add GameBall
ray-yhc Mar 27, 2023
314e127
chore : edit method order of GameStatus
ray-yhc Mar 27, 2023
1b3f09b
refactor : refactor main method to GameService
ray-yhc Mar 27, 2023
7e9f562
feat : delete Regex code
ray-yhc Mar 27, 2023
6472f4a
feat : edit UserInput, UserOutput into static method
ray-yhc Mar 27, 2023
dc3d36f
chore : edit appearance
ray-yhc Mar 27, 2023
9d64f96
test : edit UserOutputTest
ray-yhc Mar 27, 2023
af4d49d
chore : edit method
ray-yhc Mar 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 기능 목록

입출력
- [ ] 사용자 입력
- [ ] 숫자 입력
- [ ] 예외처리
- [ ] 게임 진행(1 or 2) 입력
- [ ] 예외처리
- [ ] 메시지 출력
- [ ] 시작 메시지
- [ ] (b, s) -> 볼/스트라이크 출력
- [ ] 종료 메시지

게임 진행
- [ ] main
- [ ] 게임 시작
- [ ] 게임 종료, 결과 출력
- [ ] singleGame
- [ ] init : 난수 생성
- [ ] singleTurn (xxx) -> 판정 후 결과 리턴
39 changes: 38 additions & 1 deletion src/main/java/baseball/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,44 @@
package baseball;

import baseball.game.GameStatus;
import baseball.game.SingleGame;
import baseball.inout.UserInput;
import baseball.inout.UserOutput;

import java.util.List;
Comment on lines +3 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안쓰는 임포트는 지워주세요!
인텔리제이를 사용하신다면, 맥 기준 ctrl+option+o(영어 오) / 윈도우 기준 ctrl+alt+o 임포트 정리 단축키를 사용해서 간편하게 정리할 수 있습니다ㅎㅎ

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오오 꿀팁이네요


public class Application {
private static UserInput userInput;
private static UserOutput userOutput;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전역변수로 선언하는 것보다 파라미터로 넘겨주는게 더 좋지 않을까요?


public static void main(String[] args) {
// TODO: 프로그램 구현
boolean isContinue = true;

startGame();
while (isContinue)
isContinue = playSingleGame();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

playSingleGame()이 boolean을 반환하는게 좀 어색하다고 느껴집니다!

}

private static void startGame() {
userInput = new UserInput();
userOutput = new UserOutput();

userOutput.initMessage();
}

private static boolean playSingleGame() {
SingleGame singleGame = new SingleGame();

boolean isCorrect = false;

while (!isCorrect) {
List<Integer> playNum = userInput.getNum();
GameStatus gameStatus = singleGame.singleTurn(playNum);
userOutput.statusMessage(gameStatus);
isCorrect = gameStatus.isCorrect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

게임상태가 올바르면 반복한다는게 어떤 의미일까요?

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반복문을 실행하기 위해 boolean 변수를 선언한 후 반복문에서 상태를 바꾸게 되면 처음에는 게임 상태가 올바르지 않아서(종료가 아니어서) 시작하게 되는데, 이런 동작이 어색하다고 느껴집니다.
게임을 실행하면 해당 과정을 반복하는데 게임 상태가 올바르면(종료이면) 반복을 종료한다. 이런 로직이 좀 더 자연스럽지 않을까요??


userOutput.endMessage();
return userInput.isContinue();
}
}
30 changes: 30 additions & 0 deletions src/main/java/baseball/game/GameStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package baseball.game;

import java.util.Objects;

public class GameStatus {
public int ball;
public int strike;

public GameStatus(int ball, int strike) {
this.ball = ball;
this.strike = strike;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GameStatus that = (GameStatus) o;
return ball == that.ball && strike == that.strike;
}

@Override
public int hashCode() {
return Objects.hash(ball, strike);
}

public boolean isCorrect() {
return (strike == 3 && ball == 0);
}
}
32 changes: 32 additions & 0 deletions src/main/java/baseball/game/SingleGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package baseball.game;

import camp.nextstep.edu.missionutils.Randoms;

import java.util.ArrayList;
import java.util.List;

public class SingleGame {
List<Integer> computer = new ArrayList<>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변수 캡슐화해주새요!


public SingleGame() {
while(computer.size() < 3) {
int randomNumber = Randoms.pickNumberInRange(1, 9);
if (!computer.contains(randomNumber))
computer.add(randomNumber);
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 방법으로 초기 숫자를 생성할 예정이라면
LinkedHashSet 자료구조를 썻어도 좋았을 것 같네요.
순서를 보장하면서 중복이 없도록 만들어줘서 좋습니다 :)


public void setComputer (List<Integer> list) {
computer = list;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트를 위한 메소드 작성은 좋지 않은 것 같습니다.
그리고 setter 사용보다는 생성자를 이용해서 생성해주면 좋을 것 같아요!


public GameStatus singleTurn(List<Integer> player){
int strike = 0, ball = 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 줄에 두 개의 변수를 선언하면,, 가독성이 떨어지는 것 같습니다!

for (int i = 0; i < player.size(); i++) {
if (computer.get(i) == player.get(i)) strike ++;
else if (computer.contains(player.get(i))) ball ++;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단항연산자 앞에 공백은 삭제해주시면 가독성이 더 좋아질 것 같습니다!

Suggested change
if (computer.get(i) == player.get(i)) strike ++;
else if (computer.contains(player.get(i))) ball ++;
if (computer.get(i) == player.get(i)) strike++;
else if (computer.contains(player.get(i))) ball++;

}

return new GameStatus(ball, strike);
}
}
18 changes: 18 additions & 0 deletions src/main/java/baseball/inout/CheckRegex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package baseball.inout;

public class CheckRegex {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스는 유틸성 클래스같은데, 이런 경우 기본 생성자를 private로 막아주어 외부에서 객체 생성을 하지 못하도록 막아주어야 합니다!


public static boolean isThreeDigit (String test) {
String pattern = "\\d{3}";
return test.matches(pattern);
}
public static boolean isBaseball (String test) {
String pattern = "^(?!.*(.).*\\1)\\d{3}$";
return test.matches(pattern);
}

public static boolean isCommand (String test) {
String pattern = "[12]";
return test.matches(pattern);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체적으로 String.matches를 사용하고 계신데요!
matches 내부로 들어가면 Pattern 을 사용하는 것을 알 수 있습니다.

입력이 들어올때마다 Pattern 객체를 생성하고 버리는 과정이 진행되는데요
이 부분을 캐싱하면 어떨까요?

자세한 내용은 이펙티브 자바 아이템 6에서 확인하실 수 있습니다~

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 만약에 야구 게임의 숫자가 4자리로 늘어난다면?
이 클래스 뿐만 아니라 다른 클래스도 변경해야하는데,
이런식으로 변경해야할 부분이 여러 군데라면 변경하기 굉장히 힘들겠죠..?

}
43 changes: 43 additions & 0 deletions src/main/java/baseball/inout/UserInput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package baseball.inout;


import java.util.ArrayList;
import java.util.List;

import static camp.nextstep.edu.missionutils.Console.readLine;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이렇게 import하는 것보다 직접 호출하는 방법이 어디서 해당 메소드를 호출하는지가 더 명확한 것 같습니다ㅎㅎ
ex. readLine(); -> Console.readLine();


public class UserInput {

/**
* 숫자 입력
*/
public List<Integer> getNum() {
System.out.print("숫자를 입력해주세요 : ");
String input = readLine().trim();

if (!CheckRegex.isThreeDigit(input))
throw new IllegalArgumentException("3자리의 숫자를 입력해야 합니다.");
if (!CheckRegex.isBaseball(input))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검증 로직이 모두 '어떤 조건을 만족한가'와 '그렇지 않은가' 즉, !(not)을 사용해서 이루어지는데, 저는 이런 방식보다는 조건문에 '어떤 조건을 만족하지 않은가'를 넣는게 코드가 의미하는 바를 더 명확하게 나타낼 것 같은데 어떻게 생각하시나용

throw new IllegalArgumentException("중복이 없는 3자리의 숫자를 입력해야 합니다.");

List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 3; i++)
numbers.add(input.charAt(i) - '0');

return numbers;
}

/**
* 게임 진행여부 입력
*/
public boolean isContinue() {
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
String input = readLine().trim();
// regex
if (!CheckRegex.isCommand(input))
throw new IllegalArgumentException("1 또는 2를 입력해야 합니다.");

return input.equals("1");
}

}
27 changes: 27 additions & 0 deletions src/main/java/baseball/inout/UserOutput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package baseball.inout;

import baseball.game.GameStatus;

public class UserOutput {

public void initMessage() {
System.out.println("숫자 야구 게임을 시작합니다.");
}

public void statusMessage(GameStatus status) {
if (status.ball == 0 && status.strike == 0)
System.out.println("낫싱");
else if (status.ball == 0)
System.out.println(status.strike + "스트라이크");
else if (status.strike == 0)
System.out.println(status.ball + "볼");
else
System.out.println(status.ball + "볼 " + status.strike + "스트라이크");
Comment on lines +15 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view에서 출력 외 로직이 동작하는 것이 어색합니다!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇군요..!! GameStatus로 옮기는 것을 고려해봐야겠네요
view에서는 단순히 문자열을 받아서 출력하는 것이 좋은가요?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 입출력 클래스는 메인 서비스 로직의 영향을 받지 않아야된다고 생각합니다.
그런데 지금처럼 출력이 아닌 로직을 포함할 경우, 볼과 스트라이크를 구하는 방식이 변경된다면 '게임 결과'가 변경되는 것인데 '게임 결과'가 아닌 '출력'의 로직을 수정해야 합니다. 이는 굉장히 어색한 책임 같습니다.
'출력'을 위한 객체가 출력이 아닌 책임까지 갖게 되면 책임이 모호해져서, 입출력에는 최대한 로직을 넣지 않는 것을 선호하는 편입니다!


}

public void endMessage() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메소드 네이밍이 어색한 것 같습니다.
초기 메시지를 출력하다., 상태 메시지를 출력하다., 종료 메시지를 출력하다.가 메소드의 행위를 더 명확하게 나타낼 수 있을 것 같습니다!

System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
}

}
51 changes: 51 additions & 0 deletions src/test/java/baseball/game/SingleGameTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package baseball.game;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SingleGameTest {
SingleGame singleGame;

@BeforeAll
void setSingleGame() {
singleGame = new SingleGame();
singleGame.setComputer(List.of(1, 2, 3));
}

List<List<Integer>> inputs = new ArrayList<>(List.of(
List.of(4, 5, 6), // n
List.of(4, 5, 1), // 1b
List.of(1, 5, 6), // 1s
List.of(1, 5, 2), // 1b1s
List.of(1, 2, 3) // 3s
));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자가 0 을 입력해도 정상적으로 돌아가네요
예를 들어 012 같은거 ?

List<GameStatus> expects = new ArrayList<>(List.of(
new GameStatus(0, 0),
new GameStatus(1, 0),
new GameStatus(0, 1),
new GameStatus(1, 1),
new GameStatus(0, 3)
));

@ParameterizedTest
@DisplayName("singleTurn 동작 테스트")
@ValueSource(ints = {0, 1, 2, 3, 4})
void singleTurn(int i) {
// when
GameStatus status = singleGame.singleTurn(inputs.get(i));

// then
assertThat(status).isEqualTo(expects.get(i));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스 마지막에 빈 줄 삽입은 컨벤션입니다!

102 changes: 102 additions & 0 deletions src/test/java/baseball/inout/UserInputTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package baseball.inout;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class UserInputTest {
private static ByteArrayOutputStream outputMessage;
private static ByteArrayInputStream inputMessage;
private static UserInput userInput;

@BeforeEach
void setUpStreams() {
outputMessage = new ByteArrayOutputStream(); // OutputStream 생성
System.setOut(new PrintStream(outputMessage)); // 생성한 OutputStream 으로 설정

userInput = new UserInput();
}

@AfterEach
void restoresStreams() {
System.setOut(System.out); // 원상복귀
}

@ParameterizedTest
@DisplayName("숫자 입력 동작 테스트")
@ValueSource(strings = {"123", "145", "671"})
void getNum(String input) {
// given
enterInput(input);
List<Integer> compare = new ArrayList<>();
for (int i = 0; i < 3; i++)
compare.add( input.charAt(i) - '0');

// when
List<Integer> list = userInput.getNum();

// then
assertThat(list).isEqualTo(compare);
}


@ParameterizedTest
@DisplayName("숫자 입력 예외 테스트")
@ValueSource(strings = {"111", "122", "1325", "a11"})
void getNumErrors(String input) {
// given
enterInput(input);

// then
assertThatThrownBy( () -> {
userInput.getNum();
}).isInstanceOf(IllegalArgumentException.class);
}


@ParameterizedTest
@DisplayName("게임진행 입력 동작 테스트")
@ValueSource(strings = {"1", "2"})
void isContinue(String input) {
// given
enterInput(input);

// when
boolean cmd = userInput.isContinue();

// then
assertThat(cmd).isEqualTo( input.equals("1") );
}


@ParameterizedTest
@DisplayName("게임진행 입력 예외 테스트")
@ValueSource(strings = {"111", "3", "0", "a"})
void isContinueError(String input) {
// given
enterInput(input);

// then
assertThatThrownBy( () -> {
userInput.isContinue();
}).isInstanceOf(IllegalArgumentException.class);
}

void enterInput(String input){
inputMessage = new ByteArrayInputStream(input.getBytes());
System.setIn(inputMessage);
}
}
Loading