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

[1주차] 변해빈 #1

Open
wants to merge 63 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
5e5ff7e
docs: 기능 구현 목록 최초 작성
h-beeen Oct 19, 2023
285df98
feat: Couputer 랜덤 숫자 뽑기 기능 구현
h-beeen Oct 19, 2023
87fa078
feat: Player 숫자 입력 및 검증 로직 추가
h-beeen Oct 19, 2023
403e53a
feat: Game 필수 기능 구현 (Player 입력 / Computer 난수 생성)
h-beeen Oct 19, 2023
cd65c41
docs: 구현 기능 목록 수정
h-beeen Oct 19, 2023
29a7d86
feat: Hint 필수 기능 구현 (볼, 스트라이크 카운트)
h-beeen Oct 20, 2023
d5e2f95
feat: 게임 breakPoint 설정
h-beeen Oct 20, 2023
d01607d
refactor: Constant 값 Config 분리 및 축약 문법 수정
h-beeen Oct 20, 2023
45e0473
feat: 힌트 출력 반복 루프 추가
h-beeen Oct 20, 2023
1447d6f
feat: 사용자의 중복 숫자 입력 예외 처리 추가
h-beeen Oct 20, 2023
02ff445
docs: 기능 요구사항 진행사항 갱신
h-beeen Oct 20, 2023
3cd75ee
refactor : MVC 패턴 리팩토링
h-beeen Oct 20, 2023
f65861b
feat: 시스템 설정(Config) 제약조건 추가
h-beeen Oct 20, 2023
641d1d3
refactor: Model Layer / View Layer 분리
h-beeen Oct 20, 2023
dbcdc54
fix: 게임을 새로시작하지 않는 버그 수정
h-beeen Oct 20, 2023
2bd8d97
fix: 숫자 입력 간 개행 오류 수정
h-beeen Oct 20, 2023
8306983
feat: PlayerNumber / ComputerNumber를 Number 객체로 병합
h-beeen Oct 20, 2023
f837542
refactor: Hint 로직 도메인 주도 설계로 개선
h-beeen Oct 20, 2023
daad19e
feat: 생성자 접근제한자 조정
h-beeen Oct 20, 2023
e16b264
refactor: 메소드명 직관적 수정
h-beeen Oct 20, 2023
058853e
refactor: 요구사항 기능 구현 완료 및 MVC 패턴 리팩토링
h-beeen Oct 20, 2023
c672022
fix: 테스트 실패 케이스 핸들링(메소드 변경)
h-beeen Oct 21, 2023
dd8cba7
refactor: 프로그램 종료 플래그 상수 선언
h-beeen Oct 21, 2023
990b7c7
refactor: 상수 변수 Enum으로 리팩토링
h-beeen Oct 21, 2023
f5fa1bc
refactor: validator 로직 개선
h-beeen Oct 21, 2023
3e84307
refactor: 미사용 함수 삭제
h-beeen Oct 21, 2023
6a46e8c
refactor: Enum을 활용한 불변 상수 패키징
h-beeen Oct 21, 2023
04d9229
refactor: 클래스 - 상수 선언부 개행 제거
h-beeen Oct 21, 2023
5e9b6c2
docs: 구현 기능 목록 update
h-beeen Oct 21, 2023
8962557
feat: Result 멤버변수 final 선언
h-beeen Oct 21, 2023
b84f780
style: 코드 포맷팅
h-beeen Oct 21, 2023
7acc36b
chore: 패키지 이름 변경 (model -> domain)
h-beeen Oct 22, 2023
ba6bd2c
feat: Number 일급 컬렉션 적용
h-beeen Oct 22, 2023
b1dd4e1
fix: BallCount / StrikeCount를 제대로 카운트 하지 못하는 이슈 수정
h-beeen Oct 22, 2023
fa90c35
refactor: 전역으로 선언되어있던 Config 설정을 수정
h-beeen Oct 22, 2023
3f7bb2e
feat: Getter 메소드 삭제
h-beeen Oct 22, 2023
7f7d7e8
refactor: 지역변수 재참조 이슈 해결을 위해 do-while문 제거
h-beeen Oct 22, 2023
ac8b5bd
refactor: 메소드명 직관적으로 개선
h-beeen Oct 22, 2023
4a03375
refactor: 게임 종료 플래그 지역변수로 변경
h-beeen Oct 22, 2023
febd594
refactor: 불필요 viewConfig 제거
h-beeen Oct 22, 2023
1999b2c
refactor: 변수, 메소드 명 직관적으로 개선 (request -> ask)
h-beeen Oct 22, 2023
99aa219
refactor: Result 출력 방식 개선을 위한 Enum 추가 및 로직 수정
h-beeen Oct 22, 2023
22f1aef
refactor: 출력 메소드 재사용성 확장 및 출력 메세지 열거형으로 변경
h-beeen Oct 22, 2023
575e4a5
refactor: 공통 정보 출력 메소드 명 변경(printGameInformation -> printInformation)
h-beeen Oct 22, 2023
a1780f0
docs: README.md 프로젝트 개요 작성
h-beeen Oct 22, 2023
26561ff
fix: 빌드 에러, 개행을 정상적으로 출력하지 않는 오류 수정
h-beeen Oct 23, 2023
7ac0f58
chore: 패키지명 변경 (config -> global)
h-beeen Oct 23, 2023
473e3cd
docs: README.md 패키지 변동사항 업데이트
h-beeen Oct 23, 2023
ab42278
refactor: model <-> view 의존 관계 제거
h-beeen Oct 23, 2023
0755554
refactor: 변수명 직관적 개선 (PrintMessage -> StaticNotice)
h-beeen Oct 23, 2023
c56887c
refactor: 정적 팩토리 메소드를 활용한 예외처리 메소드 커스텀
h-beeen Oct 23, 2023
a1307d6
refactor: BaseballException.of 형태 적용 예외처리
h-beeen Oct 23, 2023
9de442b
fix: 변수명 통일 (player, user -> player)
h-beeen Oct 23, 2023
fecb933
refactor: static import를 활용한 코드 간소화
h-beeen Oct 23, 2023
2b505a0
refactor: SRP 준수를 위해서 메소드 추가 분리
h-beeen Oct 23, 2023
f9a297c
feat: 멤버변수가 없는 클래스의 생성자 제한
h-beeen Oct 23, 2023
5fdb512
chore: 패키지 변경 (domain/exception -> global/exception)
h-beeen Oct 23, 2023
3fc9cd2
docs: README.md 업데이트
h-beeen Oct 23, 2023
caa0bd3
feat: 메소드 분리 및 else-if문 구조 변경
h-beeen Oct 23, 2023
1fa1b0b
feat: 변수명 변경 및 종료조건 예외처리 추가(1, 2 외의 값)
h-beeen Oct 23, 2023
0422d7b
feat: 메세지 출력이 GameConfig 전역 설정에 의존하도록 수정
h-beeen Oct 23, 2023
a51764b
docs: README.md 업데이트
h-beeen Oct 23, 2023
5d9eaf2
docs: README.md 업데이트
h-beeen Oct 23, 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
206 changes: 206 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
# ⚾&nbsp;&nbsp;Precourse-Week1 Mission **[숫자 야구]**

## 💌&nbsp;&nbsp;목차

- [📦&nbsp;&nbsp;패키지 구조](#패키지-구조)
- [✨&nbsp;&nbsp;기능 구현 목록](#기능-구현-목록)
- [🎨&nbsp;&nbsp;구현 간 고민했던 내용들](#구현-간-고민했던-내용들)

---

## 📦&nbsp;&nbsp;패키지 구조

<div align="center">
<table>
<tr>
<th align="center">Package</th>
<th align="center">Class</th>
<th align="center">Description</th>
</tr>
<tr>
<td><b>🕹&nbsp;&nbsp;controller</b></td>
<td><b>Game</b></td>
<td>게임 로직을 메인으로 동작하는 컨트롤러 클래스</td>
</tr>
<tr><td colspan="3"></td></tr>
<tr>
<td rowspan="2"><b>💻&nbsp;&nbsp;domain</b></td>
<td><b>Number</b></td>
<td>사용자에게 입력받는 숫자와, 컴퓨터가 생성하는 숫자 클래스
</td>
</tr>
<tr>
<td><b>Result</b></td>
<td>Ball Count와 Strike Count에 대한 결과 클래스</td>
</tr>
<tr>
<td><b>&nbsp;&nbsp;&nbsp;&nbsp;↘️&nbsp;&nbsp;/ constants</b></td>
<td><b>ResultType</b></td>
<td>각 결과에 따라 다른 출력 방법에 대해 정의된 Enum</td>
</tr>
<tr><td colspan="3"></td></tr>
<tr>
<td><b>📃&nbsp;&nbsp;global</b></td>
<td><b>GameConfig</b></td>
<td>전역으로 작용하는 설정과 제약조건에 대한 Enum</td>
</tr>
<tr>
<td rowspan="2"><b>&nbsp;&nbsp;&nbsp;&nbsp;↘️&nbsp;&nbsp;/ exception</b></td>
<td><b>BaseballException</b></td>
<td>전역으로 처리하는 예외상황에 대한 Exception 클래스<br/></td>
</tr>
<tr>
<td><b>ErrorMessage</b></td>
<td>각 예외 상황에서 전역으로 던져질 예외의 메세지 Enum</td>
</tr>
<tr><td colspan="3"></td></tr>
<tr>
<td><b>✅&nbsp;&nbsp;validator</b></td>
<td><b>InputValidator</b></td>
<td>사용자가 입력하는 숫자에 대한 제약조건 클래스</td>
</tr>
<tr><td colspan="3"></td></tr>
<tr>
<td rowspan="2"><b>💬&nbsp;&nbsp;view</b></td>
<td><b>InputView</b></td>
<td>사용자 요청을 처리하는 클래스</td>
</tr>
<tr>
<td><b>OutputView</b></td>
<td>사용자에게 응답을 출력하는 클래스</td>
</tr>
<tr>
<td><b>&nbsp;&nbsp;&nbsp;&nbsp;↘️&nbsp;&nbsp;/ constants</b></td>
<td><b>StaticNotice</b></td>
<td>사용자에게 응답할 정적 메세지를 담은 열거형 클래스</td>
</tr>
<tr><td colspan="3"></td></tr>
<tr>
<td colspan="3" align="center"><b>Package Structure Overview</b></td>
</tr>
<tr>
<td colspan="3"><img src="https://github.com/woowacourse-precourse/java-baseball-6/assets/112257466/f37d479a-d211-4c79-93cf-c0a4be1a7443" width="99%"></td>
</tr>

</table>
</div>

---

## ✨&nbsp;&nbsp;기능 구현 목록

###

- ✅ `a ~ b` 사이의 서로 값이 다른 `n자리`의 정수를 랜덤으로 생성한다.
- Default Setting : `1 ~ 9`사이의 서로 값이 다른 `3자리`의 정수
- ✅ 게임 시작 문구 출력
- ✅ 사용자에게 `a ~ b 사이의 서로 값이 다른 n자리의 정수`를 입력 받는다.
- 입력받은 input이 비어있을 경우 예외처리
- 입력받은 input이 숫자가 아닌 문자가 포함될 경우 예외처리
- 입력받은 input에 중복된 숫자가 있을 경우 예외처리
- ✅ 사용자 input 숫자와 랜덤 생성 정수의 자리수를 비교해 출력할 힌트를 계산한다.
- 숫자의 값은 같지만 자리수가 다른 경우의 수 n개 : `n볼`
- 숫자의 값과 자리수가 모두 같은 경우의 수 m개 : `n스트라이크`
- ✅계산된 힌트를 아래 양식으로 출력한다
- 볼 n개, 스트라이크 0개가 존재할 때 : `n볼`
- 볼 0개, 스트라이크 n개가 존재할 때 : `n스트라이크`
- 볼 n개, 스트라이크 m개가 존재할 때 : `n볼 m스트라이크`
- 볼 0개, 스트라이크 0개가 존재할 때 : `낫싱`

- ✅ 게임 클리어 여부 판단
- `n스트라이크가 아니라면`, 다시 사용자에게 입력을 숫자를 받고, 힌트를 출력한다.
- `n스트라이크를 맞추었다면`, 아래 메세지를 출력하고 사용자에게 플래그를 입력받는다.
- `n개의 숫자를 모두 맞히셨습니다! 게임 종료`
- `게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.`
- 입력받은 input이 1과 2가 아닌 숫자일 경우 예외처리
- 입력값에 따라 게임을 재시작하거나 종료한다.

--------------------------------------------------------

## 🎨&nbsp;&nbsp;구현 간 고민했던 내용들

### 1️⃣&nbsp;&nbsp;&nbsp;확장에는 열려있고, 변경에는 닫혀있는 OCP 설계

- input 숫자의 범위가 변하더라도(1~9), 자리수가 변하더라도 대응이 손쉽게 가능해야 한다.</br>
`GameConfig` 파일에서 `NUMBER_LENGTH` 변수의 value를 변경해 손쉽게 변경이 가능하다.<br/>
개발 요구사항에서 자릿수 요청까지 처리하는 문제였다면, 더욱 OCP를 준수하는 코드 작성이 가능했을 것 같다.

<div align="center">

<table>
<tr>
<th align="center">숫자 3자리</th>
<th align="center">숫자 4자리</th>
<th align="center">숫자 5자리</th>
</tr>
<tr>
<td align="center"><img src="https://github.com/woowacourse-precourse/java-baseball-6/assets/112257466/0b5b10f6-357f-4274-9b42-ea68d18edf85" height="50%"/></td>
<td align="center"><img src="https://github.com/woowacourse-precourse/java-baseball-6/assets/112257466/33d19627-775a-4d56-b2c1-88c186a95336" height="50%"/></td>
<td align="center"><img src="https://github.com/woowacourse-precourse/java-baseball-6/assets/112257466/e41b3fc0-2d6f-4f6c-abe4-f765abcb7aad" height="50%"/></td>
</tr>
</table>


--------------------------------------------------------

### 2️⃣ 4번의 대규모 리팩토링, 그리고 얻어낸 값진 `Number`

- 영감을 얻게 해줬던 한 마디
```bash
객체는 '자율적인 존재'라는 점을 명심하라.
< 중략 >
객체는 스스로의 행동에 의해서만 상태가 변경되는 것을 보장함으로써 객체의 자율성을 보장한다.

- 객체지향의 사실과 오해 中
```

- First-class collection + Static Factory Method 활용
```java
public class Number {
private final List<Integer> numbers;

// Player Input Number Constructor
private Number(String input) {
validateEmpty(input);
validateNumberLength(input);
validateContainOnlyNumber(input);
validateContainDuplicatedNumber(input);

this.numbers = convertInputNumber(input);
}

// Computer Generated Number Constructor
private Number(List<Integer> computerNumber) {
this.numbers = computerNumber;
}
}
```

- 일급 컬렉션과 생성자 검증을 사용해 `numbers`에 유효하게 검증이 끝난 숫자만 들어오도록 설계
- playerNumber와, computerNumber의 생성자는 서로 다른 파라미터를 지니기 때문에, 개발자가 사용 간 혼동 가능<br/>
해당 문제를 해결하기 위해, 생성자를 `private`으로 제한하고, 의미있는 메소드로만 생성자를 호출하도록 설계

```java
// Computer Generated Number Constructor
public static Number generateRandomNumbers() {
List<Integer> randomNumbers = new ArrayList<>();
while (randomNumbers.size() < NUMBER_LENGTH.getValue()) {
int number = pickNumberInRange(RANDOM_NUMBER_MINIMUM.getValue(), RANDOM_NUMBER_MAXIMUM.getValue());
if (!hasDuplicatedNumber(randomNumbers, number)) {
randomNumbers.add(number);
}
}
return new Number(randomNumbers);
}

// Player Number Static Factory Method
public static Number inputPlayerNumbers() {
String playerNumbers = InputView.askPlayerNumbers();
return new Number(playerNumbers);
}
```
- 정적 팩토리 메소드 명에 의미를 부여하고, 개발자가 직관적으로 해석할 수 있도록 했고,<br/>
일급 컬렉션을 활용해 검증이 끝난 유효한 값만 리스트에 담을 수 있게 되었다!

-

5 changes: 4 additions & 1 deletion src/main/java/baseball/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package baseball;

import baseball.controller.Game;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
Game game = new Game();
game.start();
}
}
36 changes: 36 additions & 0 deletions src/main/java/baseball/controller/Game.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package baseball.controller;

import baseball.domain.Number;
import baseball.domain.Result;
import baseball.validator.InputValidator;
import baseball.view.InputView;
import baseball.view.OutputView;

import static baseball.view.OutputView.printStaticNotice;
import static baseball.view.constants.StaticNotice.GAME_START;

public class Game {
public void start() {
printStaticNotice(GAME_START);
do {
Number computerNumber = Number.generateRandomNumbers();
play(computerNumber);
} while (!askRestartOrExit());
}

private void play(Number computerNumber) {
while (true) {
Number playerNumber = Number.inputPlayerNumbers();
Result result = Result.create(playerNumber, computerNumber);
OutputView.printMessage(result.createResultMessage());
if (result.checkGameOver()) {
break;
}
}
}

private boolean askRestartOrExit() {
String input = InputView.askRestartOrExit();
return InputValidator.isValidRestartFlag(input);
}
}
80 changes: 80 additions & 0 deletions src/main/java/baseball/domain/Number.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package baseball.domain;

import baseball.view.InputView;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

import static baseball.global.GameConfig.*;
import static baseball.validator.InputValidator.*;
import static camp.nextstep.edu.missionutils.Randoms.pickNumberInRange;

public class Number {
private final List<Integer> numbers;

// Player Number Constructor
private Number(String input) {
validateEmpty(input);
validateNumberLength(input);
validateContainOnlyNumber(input);
validateContainDuplicatedNumber(input);

this.numbers = convertInputNumber(input);
}
Comment on lines +17 to +24

Choose a reason for hiding this comment

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

여기서 검증 메소드를 다 사용 안 한 이유가 있나요? 분명 7개 만든 것 같은데 4개 밖에 없어서ㅋㅋㅋ

Copy link
Member Author

@h-beeen h-beeen Oct 23, 2023

Choose a reason for hiding this comment

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

    public static void validateContainOnlyNumber(final String number) {
        if (!isValidNumber(number)) {
            throw new IllegalArgumentException("number cannot contain any letters");
        }
    }

isValid 메소드는 실제 검증을 수행한 결과를 boolean으로 리턴합니다.
그리고 이 메소드 호출 결과에 따른 포스트 컨디션으로
validate 메소드를 활용해는 검증 -> 예외를 던지는 역할로 분리했습니다.

따라서 상기 4개의 메소드를 호출하면
7개의 메소드를 전부 활용한 검증이 가능한거죠!


// Computer Number Constructor
private Number(List<Integer> computerNumber) {
this.numbers = computerNumber;
}

// Player Number Static Factory Method
public static Number inputPlayerNumbers() {
String playerNumbers = InputView.askPlayerNumbers();
return new Number(playerNumbers);
}

// Computer Number Static Factory Method
public static Number generateRandomNumbers() {
List<Integer> randomNumbers = new ArrayList<>();
while (randomNumbers.size() < NUMBER_LENGTH.getValue()) {
int number = pickNumberInRange(RANDOM_NUMBER_MINIMUM.getValue(), RANDOM_NUMBER_MAXIMUM.getValue());
if (!hasDuplicatedNumber(randomNumbers, number)) {
randomNumbers.add(number);
}
}
return new Number(randomNumbers);
}

private static boolean hasDuplicatedNumber(List<Integer> randomNumbers, int number) {
return randomNumbers.contains(number);
}

private List<Integer> convertInputNumber(String input) {
return input.chars()
.mapToObj(Character::getNumericValue)
.toList();
}
Comment on lines +53 to +57

Choose a reason for hiding this comment

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

이런 식으로 문자열을 리스트로 바꾸는 거는 생각을 못 했는데 간결하고 좋은 것 같습니다👍


public int countBallCount(final Number comparableNumber) {
return (int) IntStream.range(0, numbers.size())
.filter(i -> comparableNumber.isBall(numbers.get(i), i))
.count();
}

public int countStrikeCount(final Number comparableNumber) {
return (int) IntStream.range(0, numbers.size())
.filter(i -> comparableNumber.isStrike(numbers.get(i), i))
.count();
}

// Ball : Contain their number at other position
private boolean isBall(int number, int digit) {
return !isStrike(number, digit) && numbers.contains(number);
}

// Strike : Contain their number at same digit
private boolean isStrike(int number, int digit) {
return number == numbers.get(digit);
}
}
Loading