Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[홍섭] 숫자 야구 #2

Open
wants to merge 29 commits into
base: hongxeob
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
37ece42
docs : 기능 목록 작성
hongxeob Dec 4, 2024
96af6a0
docs : 기능 목록 작성
hongxeob Dec 5, 2024
c69ccea
feat: 콘솔로 사용자의 입력을 받는 Reader 예외 구현
hongxeob Dec 5, 2024
c0d3112
feat: 랜덤 숫자를 생성하는 생성기 구현 및 테스트
hongxeob Dec 5, 2024
68f6db3
feat: 룰에 관한 값을 관리 하기 위한 상수 클래스 구현
hongxeob Dec 5, 2024
486d780
refactor: Validator 추상화 해제 및 검증 로직 오버로딩
hongxeob Dec 5, 2024
60aafdc
docs : update README.md
hongxeob Dec 5, 2024
105c9f5
feat: 숫자를 비교하는 심판 테스트 코드 및 구현
hongxeob Dec 6, 2024
2cc2624
feat: 검증없이 입력 읽는 메서드 추가
hongxeob Dec 6, 2024
cb78f5d
feat: 숫자 야구를 실행하는 Game 클래스 테스트 및 구현
hongxeob Dec 6, 2024
1b16ff7
refactor: 검증용 클래스 static으로 사용
hongxeob Dec 6, 2024
0b305cf
style: 패키지 설정
hongxeob Dec 6, 2024
e8419f9
feat: 출력의 역할을 하는 Printer 구현 및 테스트
hongxeob Dec 10, 2024
bf10789
refactor : Game 클래스 수정
hongxeob Dec 10, 2024
7b80461
update README.md
hongxeob Dec 10, 2024
91a3763
test: 패키지 이동
hongxeob Dec 11, 2024
ba5ecd3
refactor: 검증 순서 변경
hongxeob Dec 19, 2024
5251d14
refactor: Game -> BaseballGameRule로 변경
hongxeob Dec 19, 2024
6556810
test: BaseballGameRule 테스트 코드 작성
hongxeob Dec 19, 2024
fb70817
test: BaseballGame/ BaseballGameManager로 리팩토링
hongxeob Dec 19, 2024
0c97e38
test : 테스트코드 작성
hongxeob Dec 19, 2024
8e103ec
refactor: 메인에 반영
hongxeob Dec 19, 2024
1093f77
리뷰 반영 : number List -> 객체로 변경
hongxeob Dec 26, 2024
34dec54
리뷰 반영 Test : number List -> 객체로 변경
hongxeob Dec 26, 2024
2b554b8
리뷰 반영 : Random 생성자 초기화
hongxeob Dec 26, 2024
a43688a
리뷰 반영 : Converter 검증 추가
hongxeob Dec 28, 2024
28a43b3
리뷰 반영 : read 네이밍 변경
hongxeob Dec 28, 2024
fe5ac84
add : 회고
hongxeob Jan 2, 2025
200e04f
add : 회고
hongxeob Jan 2, 2025
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
35 changes: 35 additions & 0 deletions java-playground/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ⚾️ 숫자 야구 기능 목록 Check List

- [x] 1부터 9까지 서로 다른 랜덤 숫자 3개를 생성한다.
- [x] 사용자로부터 숫자를 입력 받는다.
- [x] [예외] 받은 값이 3자리가 아니다.
- [x] [예외] 받은 값이 숫자가 아니다.
- [x] [예외] 받은 값에 중복이 있다.
- [x] 컴퓨터의 수 & 플레이어수를 비교한다.
- [x] 몇 개의 숫자가 같은지 확인한다.
- [x] 특정 자리에 해당 숫자가 있는지 확인한다.
- [x] 같은 숫자만 있으면 `ball`
- [x] 같은 숫자 + 같은 위치면 `strike`
- [x] 같은 숫자가 하나도 없으면 `낫싱`
- [x] 숫자 모두 같은 위치에 있으면 게임 종료.
- [x] 사용자에게 결과를 알려준다.
- [x] 재시작 여부를 묻는다.
-[x] [예외] 1,2 이외의 값이 포함 되어있다.

# 회고

## Keep
- `테스트를 먼저 작성한다.`를 지키기 위해 힘들어도 의식적으로 최대한 테스트 코드를 먼저 작성 하려고했다. -> 컴포트 존에서 벗어나려는 노력
- SOLID 원칙중 SRP를 최대한 지키려고 노력했다.
- 스터디 시간 외에도 더 관심있게 보고 작업하고 이어가려고 노력했다.
- 비슷한 주제로 부트캠프에서 빅테크 선배들한테 코드리뷰를 받은 적 있는데, 기억을 살려 최대한 비슷하게 팀원들에게 리뷰해주려고 노력했다.
- 팀원들이 까먹지 않게 최대한 리마인드를 했다.

## Problem
- 코드리뷰 후 막바지 수정에 100% 고민하고 에너지를 쏟아서 리팩토링하지 못한 것 같다.
- TDD 진행시 엣지 케이스, 실패 케이스에 대한 테스트를 더 많이 작성해야한다.

## Try
- 테스트 코드 작성시 해피 케이스도 좋지만, 실패/엣지 케이스에 대해 더 고민하고 작성해야겠다.
- 비즈니스적인 feature 코드를 잘 작성하는 것에 더불어 공통적인 코드(ex. `common`,`util`, `general` 및 컨버터, 정규식 등등) 조각들을 어떻게 더 효율적으로 관리할지 고민해봐야겠다.
- 초반에 잘 불타던 것이 뒷심이 딸리지 않게 에너지/시간 분배를 잘 해야겠다.
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
package org.gonza.javaplayground;

import org.gonza.javaplayground.core.BaseballGameManager;
import org.gonza.javaplayground.core.BaseballGameRule;
import org.gonza.javaplayground.core.Judgement;
import org.gonza.javaplayground.core.NumberGenerator;
import org.gonza.javaplayground.view.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.Scanner;

@SpringBootApplication
public class JavaPlaygroundApplication {

public static void main(String[] args) {
SpringApplication.run(JavaPlaygroundApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(JavaPlaygroundApplication.class, args);

Scanner scanner = new Scanner(System.in);
Reader reader = new ConsoleReader(scanner);
Printer printer = new ConsolePrinter();

// 도메인
NumberGenerator numberGenerator = new NumberGenerator();
Judgement judgement = new Judgement();
BaseballGameRule baseballGameRule = new BaseballGameRule(numberGenerator, judgement);

// UI View
GameView gameView = new GameView(reader, printer);

// 게임 매니저 생성 및 실행
BaseballGameManager baseballGameManager = new BaseballGameManager(baseballGameRule, gameView);
baseballGameManager.run();

scanner.close();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.gonza.javaplayground.core;

public class BaseballGame {
private final BaseballGameRule rule;
private Numbers computerNumbers;

public BaseballGame(BaseballGameRule rule) {
this.rule = rule;
this.computerNumbers = rule.generateNumbers();
}

public GameResult guess(Numbers playerNumbers) {
return rule.guess(computerNumbers, playerNumbers);
}

public void restart() {
this.computerNumbers = rule.generateNumbers();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.gonza.javaplayground.core;

import org.gonza.javaplayground.view.GameView;

public class BaseballGameManager {
private final BaseballGame game;
private final GameView view;

public BaseballGameManager(BaseballGameRule rule, GameView view) {
this.game = new BaseballGame(rule);
this.view = view;
}

public void run() {
view.displayGameStart();
startGameLoop();
}

private void startGameLoop() {
while (true) {
playRoundWithErrorHandling();
}
}

private void playRoundWithErrorHandling() {
try {
playOneRound();
} catch (IllegalArgumentException e) {
view.displayError(e.getMessage());
}
}

private void playOneRound() {
Numbers playerNumberList = view.getPlayerInput();
GameResult gameResult = game.guess(playerNumberList);
view.displayResult(gameResult.result());

if (!gameResult.isGameWon()) return;
handleWin();
}

private void handleWin() {
view.displayWinMessage();
if (!shouldPlayAgain()) {
exitGame();
}
startNewGame();
}

private void startNewGame() {
game.restart();
view.displayNewGameMessage();
}

private boolean shouldPlayAgain() {
try {
GameCommand command = getGameCommand();
return command.isRestart();
} catch (IllegalArgumentException e) {
view.displayInvalidChoiceError();
return shouldPlayAgain();
}
}

private GameCommand getGameCommand() {
int choice = view.getRetryChoice();
return GameCommand.from(choice);
}

private void exitGame() {
System.exit(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.gonza.javaplayground.core;

public class BaseballGameRule {
private final NumberGenerator numberGenerator;
private final Judgement judgement;

public BaseballGameRule(NumberGenerator numberGenerator, Judgement judgement) {
this.numberGenerator = numberGenerator;
this.judgement = judgement;
}

public GameResult guess(Numbers computerNumbers, Numbers playerNumbers) {
String result = judgement.compareNumber(computerNumbers, playerNumbers);
boolean isGameWon = judgement.isGameWon(result);

return new GameResult(result, isGameWon);
}

public Numbers generateNumbers() {
return numberGenerator.generateRandomNumber(RuleConstants.REQUIRED_LENGTH);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.gonza.javaplayground.core;

import java.util.Arrays;

public enum GameCommand {
RESTART(1),
EXIT(2);

private final int command;

GameCommand(int command) {
this.command = command;
}

public static GameCommand from(int input) {
return Arrays.stream(values())
.filter(command -> command.command == input)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("1 또는 2만 입력 가능합니다."));
}

public boolean isRestart() {
return this == RESTART;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.gonza.javaplayground.core;

public record GameResult(String result, boolean isGameWon) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.gonza.javaplayground.core;

import java.util.List;

public class Judgement {

public String compareNumber(Numbers computerNumbers, Numbers playerNumbers) {
int correctCount = getPlaceHitCount(computerNumbers, playerNumbers);
int strike = getStrikeCount(computerNumbers, playerNumbers);
int ball = getBallCount(correctCount, strike);

String result = getResult(correctCount, strike, ball);

return result;
}

public boolean isGameWon(String result) {
return result.equals(RuleConstants.REQUIRED_LENGTH + "스트라이크");
}

private String getResult(int correctCount, int strike, int ball) {
if (correctCount == 0) {
return "아웃";
}

if (strike == 0) {
return ball + "볼";
}

if (ball == 0) {
return strike + "스트라이크";
}

return ball + "볼 " + strike + "스트라이크";
}

private int getStrikeCount(Numbers computerNumberList, Numbers playerNumberList) {
int strike = 0;

for (int placeIndex = 0; placeIndex < playerNumberList.values().size(); placeIndex++) {
if (hasNumberInPlace(computerNumberList.values(), placeIndex, playerNumberList.values().get(placeIndex))) {
strike++;
}
}
return strike;
}

private int getBallCount(int correctCount, int strike) {
return correctCount - strike;
}

private int getPlaceHitCount(Numbers computerNumberList, Numbers playerNumberList) {
int count = 0;
for (int player : playerNumberList.values()) {
if (computerNumberList.values().contains(player)) {
count++;
}
}
return count;
}

private boolean hasNumberInPlace(List<Integer> computers, int placeIndex, int number) {
return computers.get(placeIndex) == number;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.gonza.javaplayground.core;

import org.gonza.javaplayground.util.Validator;

import java.util.*;

public class NumberGenerator {
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 9;

private final Random random;

public NumberGenerator() {
this.random = new Random();
}

public Numbers generateRandomNumber(int size) {
Validator.validateListSize(size);
return new Numbers(generateUniqueNumbers(size));
}

private List<Integer> generateUniqueNumbers(int size) {
Set<Integer> numberSet = new HashSet<>();

while (numberSet.size() < size) {
numberSet.add(random.nextInt(MAX_NUMBER) + MIN_NUMBER);
}

return new ArrayList<>(numberSet);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.gonza.javaplayground.core;

import org.gonza.javaplayground.util.Validator;

import java.util.List;

public record Numbers(List<Integer> values) {
public Numbers {
Validator.validateListSize(values.size());
values = List.copyOf(values);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.gonza.javaplayground.core;

public final class RuleConstants {
public static final int MIN_NUMBER = 1;
public static final int MAX_NUMBER = 9;
public static final int REQUIRED_LENGTH = 3;

private RuleConstants() {
throw new AssertionError("StringUtil 클래스는 인스턴스화 할 수 없습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.gonza.javaplayground.util;

import java.util.List;
import java.util.stream.Collectors;

public class Converter {

public static List<Integer> convertStringToNumberList(String input) {
Copy link
Member

Choose a reason for hiding this comment

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

숫자로 이루어진 문자열이 아닌 경우가 발생할 수 있지 않을까요?

작성자는 Validate에서 검증한다 인지할 수 있지만, 해당 함수만 봤을 때에는 오류의 가능성이 보여서요!

Copy link
Member Author

@hongxeob hongxeob Dec 28, 2024

Choose a reason for hiding this comment

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

해당 부분은 단순하게 숫자로 이루어진 문자열을 변환만 해주는 역할로 썼습니다!
그래서 호출하는 쪽 (사용자에게 입력 받는 부분)에서 이미 검증을 한 상태입니다. 그래서 말씀주신 숫자로된 문자열 형태가 아닐시 그 전에 예외가 터집니다!
근데 현진님 말씀대로 이 안에서 한 번 더 검증하는 것도 맞는거 같습니다.

if (input == null || input.isEmpty()) {
throw new IllegalArgumentException("입력값이 비어있습니다.");
}

Validator.validateNumeric(input);

return input.chars()
.map(Character::getNumericValue)
.boxed()
.collect(Collectors.toList());
}
}
Loading