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단계 - 로또(수동)] 연로그(권시연) 미션 제출합니다. #454

Merged
merged 27 commits into from
Mar 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6d80ecb
docs: 리드미 갱신 - 요구사항 추가
Mar 2, 2022
820a506
refactor: LottoNumbers input 값의 최소한의 검증 View의 책임으로 변경
Mar 2, 2022
f7db213
style: 변수명 변경 number -> value
Mar 2, 2022
cf4410f
refactor: 인스턴스 캐싱 방식 변경
Mar 2, 2022
137b7c2
refactor: 하드코딩 제거, 형식 통일화
Mar 2, 2022
3b9694f
style: 우승 상금이 없는 경우 DEFULAT -> NONE 이름 변경
Mar 2, 2022
829c37d
test: given, when, then 구분
Mar 2, 2022
c71c797
refactor: LottoNumber의 접근제어자 private -> protected 변경
Mar 2, 2022
b7a65d2
feat: 수동 로또 개수 클래스 생성
Mar 2, 2022
ac8befc
feat: 수동 로또 입력 기능 구현
Mar 2, 2022
9f08b0c
feat: 수동 로또 개수 입력 기능 구현
Mar 2, 2022
964e57a
refactor: 하드코딩 제거
Mar 2, 2022
e45fd9e
style: 오류 문구 수정
Mar 2, 2022
7796450
refactor: 수동 구매 시 남은 횟수도 출력되도록 수정
Mar 2, 2022
dcd4bcd
docs: 리드미 갱신 - 구현 완료 기능 체크
Mar 2, 2022
de191ad
refactor: 티켓에 수동 로또 더하는 경우 LottoTicket 형태로 받도록 개선
Mar 4, 2022
3d94fe2
refactor: 정적 팩토리 메소드 도입
Mar 4, 2022
2701bcc
test: 테스트 케이스 수정
Mar 4, 2022
abb7362
test: given, when, then 분리
Mar 4, 2022
ecaba3c
test: findRanking 메소드 테스트 코드 추가
Mar 4, 2022
cb8cfb6
style: 1000단위 구분자 표시
Mar 4, 2022
4646a00
style: 메소드명 변경
Mar 4, 2022
affa474
refactor: equals(), hashCode() 재정의
Mar 5, 2022
293fe25
fix: 2등이 아닌데 보너스볼이 포함되는 경우 NONE으로 반환되는 현상 수정
Mar 5, 2022
581f834
refactor: findRanking() 메소드 분리
Mar 5, 2022
64660e7
style: 사용하지 않는 import 제거
Mar 5, 2022
6951fa8
fix: 3위가 반환되지 않는 현상 수정
Mar 5, 2022
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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- 로또 1장의 가격은 1000원이다.

## step1 요구사항
## 요구사항
> ⭐: step2에서 추가된 기능
- [x] 로또 구입 금액
- (e) 1000원 단위 외의 금액 불가
- (e) 문자열 입력
- 구매 로또 개수 구하기
- [x] 수동 로또 ⭐
- (e) 구입한 금액보다 많이 구매했는지 확인
- 수동으로 구매할 로또 개수 입력
- 수동 로또 번호 입력
- [x] 구매한 로또 개수 출력
- [x] 로또 목록
- 로또 생성
Expand All @@ -35,4 +40,4 @@
- 숫자랑 위치 일치하는지 확인
- 보너스볼 확인
- [x] 추첨 결과 출력
- 수익률 출력 (수익률 = 당첨 금액 /구입 금액)
- 수익률 출력 (수익률 = 당첨 금액 /구입 금액)
63 changes: 49 additions & 14 deletions src/main/java/lotto/controller/LottoController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package lotto.controller;

import java.util.ArrayList;
import java.util.List;
import lotto.domain.LottoAmount;
import lotto.domain.LottoNumber;
import lotto.domain.LottoNumbers;
import lotto.domain.LottoTicket;
import lotto.domain.ManualLottoCount;
import lotto.domain.WinningNumbers;
import lotto.domain.WinningResult;
import lotto.view.InputView;
Expand All @@ -13,7 +16,8 @@ public class LottoController {
public void start() {
LottoAmount amount = inputAmount();

LottoTicket lottoTicket = buyTickets(amount);
ManualLottoCount manualLottoCount = inputManualLottoCount(amount);
Copy link
Author

Choose a reason for hiding this comment

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

수동 로또 개수를 검증하는 로직을 분리하고 싶어서 ManualLottoCount라는 vo를 생성했습니다
다만 ManualLottoCount에 저장한 값이 계속 필요해서 사용할때마다 getValue()를 호출하게 되네요
이게 적절한 VO의 생성이 맞나?라는 의문이 계속 들어요😂

Copy link

Choose a reason for hiding this comment

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

값을 사용해야 하는 시점에서는 어쩔 수 없죠 ㅎㅎ
그게 아니라면 public 메서드를 열어서 내부에서 처리하게끔 해야하는데, 아래 count 계산 로직의 경우 책임 분리의 관점에서 애매한 부분이 있을수도 있겠네요 :)

LottoTicket lottoTicket = buyTicket(amount, manualLottoCount);

WinningNumbers winningNumbers = createWinningNumbers();

Expand All @@ -31,37 +35,68 @@ private LottoAmount inputAmount() {
}
}

private LottoTicket buyTickets(LottoAmount amount) {
int ticketCount = amount.calculateLottoCount();
OutputView.printTicketCount(ticketCount);
private ManualLottoCount inputManualLottoCount(LottoAmount amount) {
try {
return new ManualLottoCount(InputView.inputTryCount(), amount.calculateLottoCount());
} catch (IllegalArgumentException e) {
OutputView.printException(e);
return inputManualLottoCount(amount);
}
}

LottoTicket lottoTicket = new LottoTicket(ticketCount);
OutputView.printTicket(lottoTicket);
private LottoTicket buyTicket(LottoAmount amount, ManualLottoCount manualLottoCount) {
int manualTicketCount = manualLottoCount.getValue();
int autoTicketCount = amount.calculateLottoCount() - manualTicketCount;

LottoTicket lottoTicket = LottoTicket.createAutoLottoTicket(autoTicketCount);

if (manualTicketCount != 0) {
lottoTicket.addLottoTicket(buyManualTicket(manualLottoCount));
Copy link

Choose a reason for hiding this comment

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

이름 짓기

이름 짓는게 정말 어렵네요😂
혹시 닉은 이름 짓는 팁 같은거 있으실까요?👀
클래스 내부의 private 필드 가져올 때 getXXX()을 쓰듯이 일반적으로 사용하는 메소드명 같은게 또 따로 있을지 궁금하네요!

getter/setter 같은 네이밍은 관용어구에 가깝지만, 다른 네이밍은 좋은 코드를 많이 보다보면 감이 느는 것 같아요 ㅎㅎ
(감이 는다고 했지 익숙해진다고는 안했습니다,,)
연차 무관하게 어려움을 느끼는 지점이라고 생각해요 :)

}

printTickets(manualTicketCount, autoTicketCount, lottoTicket);
return lottoTicket;
}

private LottoTicket buyManualTicket(ManualLottoCount manualLottoCount) {
List<LottoNumbers> manualTickets = new ArrayList<>();
int tryCount = manualLottoCount.getValue();

for (int i = tryCount; i > 0; i--) {
OutputView.printInputManualTicketSentence(i);
manualTickets.add(inputLottoNumbers());
}
return LottoTicket.createManualLottoTicket(manualTickets);
}

private void printTickets(int manualTryCount, int autoTryCount, LottoTicket lottoTicket) {
OutputView.printTicketCount(manualTryCount, autoTryCount);
OutputView.printTicket(lottoTicket);
}

private WinningNumbers createWinningNumbers() {
LottoNumbers inputLottoNumbers = getInputLottoNumbers();
LottoNumber bonusNumber = getBonusNumber();
OutputView.printInputWinningTicketSentence();
LottoNumbers inputLottoNumbers = inputLottoNumbers();
LottoNumber bonusNumber = inputBonusNumber();

return getWinningNumbers(inputLottoNumbers, bonusNumber);
}

private LottoNumbers getInputLottoNumbers() {
private LottoNumbers inputLottoNumbers() {
try {
return new LottoNumbers(InputView.inputWinningNumbers());
return new LottoNumbers(InputView.inputLottoNumbers());
} catch (IllegalArgumentException e) {
OutputView.printException(e);
return getInputLottoNumbers();
return inputLottoNumbers();
}
}

private LottoNumber getBonusNumber() {
private LottoNumber inputBonusNumber() {
try {
return LottoNumber.of(InputView.inputBonusBall());
} catch (IllegalArgumentException e) {
OutputView.printException(e);
return getBonusNumber();
return inputBonusNumber();
}
}

Expand All @@ -70,7 +105,7 @@ private WinningNumbers getWinningNumbers(LottoNumbers lottoNumbers, LottoNumber
return new WinningNumbers(lottoNumbers, bonusNumber);
} catch (IllegalArgumentException e) {
OutputView.printException(e);
return getWinningNumbers(lottoNumbers, getBonusNumber());
return getWinningNumbers(lottoNumbers, inputBonusNumber());
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/lotto/domain/LottoAmount.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ public class LottoAmount {
private static final int MIN_COST = 1000;
private static final double DECIMAL = 1.0;
private static final String NOT_NUMBER_ERROR = "금액은 숫자만 등록 가능합니다.";
private static final String NOT_NATURAL_NUMBER_ERROR = "금액은 " + MIN_COST + " 이상이어야 합니다.";
private static final String NOT_DIVISIBLE_NUMBER_ERROR = "금액은 " + MIN_COST + "단위여야 합니다.";
private static final String NOT_NATURAL_NUMBER_ERROR = String.format("금액은 %d 이상이어야 합니다.", MIN_COST);
private static final String NOT_DIVISIBLE_NUMBER_ERROR = String.format("금액은 %d 단위여야 합니다.", MIN_COST);

private final int amount;

Expand Down
58 changes: 41 additions & 17 deletions src/main/java/lotto/domain/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
package lotto.domain;

import java.util.stream.IntStream;
import java.util.HashMap;
import java.util.Map;

public class LottoNumber {
private static final int MIN = 1;
private static final int MAX = 45;
protected static final int MIN = 1;
Copy link
Author

Choose a reason for hiding this comment

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

LottoNumbers에서도 해당 상수가 필요해 접근 제어자 변경

➕ default와 protected 중 고민했으나 나중에 LottoNumber을 상속하는 클래스가 생기면 해당 클래스의 패키지에서도 사용할 일이 생기지 않을까? 라고 예상했습니다.
하지만 LottoNumber을 상속하는 경우가 생기지 않을 것 같기도 하고...
뭘 사용해야 더 적절한지에 대한 고민이 생겼습니다.
닉은 default와 protected를 사용하는 기준 같은게 따로 있으실까요?

Copy link

Choose a reason for hiding this comment

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

일단 저는 상수 같은 경우는 public으로 열긴 하는데요. (불변 상수기 때문에 접근제어를 fit하게 해봤자 큰 의미 없다고 생각해서)
저도 해당 시점에 판단해서 상속이 필요할 것 같으면 protected, 패키지 내에서만 사용할 것 같으면 default로 둡니다.
사실 자바에서 패키지 기반으로 접근 제어를 한다는 사실이 맘에 들진 않아요 ㅋㅋ (코틀린을 쓰면 그런 고민할 필요가 없습니다 갓틀린 짱짱..)

답은 없으니 각 제어자 목적에 맞게 고민하고 판단하셔서 결정하시면 됩니다 :)

protected static final int MAX = 45;

private static final String NUMBER_RANGE_ERROR = "로또 숫자는 " + MIN + " 이상 " + MAX + " 이하의 숫자만 가능합니다.";
private static final String NUMBER_RANGE_ERROR = String.format("로또 숫자는 %d 이상 %d 이하의 숫자만 가능합니다.", MIN, MAX);

private static final LottoNumber[] cacheLottoNumber = new LottoNumber[MAX + 1];
private static final Map<Integer, LottoNumber> cacheLottoNumber = new HashMap<>();
Copy link
Author

Choose a reason for hiding this comment

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

캐싱된 인스턴스 Map에 저장
👉 현재는 저장되는 값이 숫자지만 특정 문자열 등이 캐싱되어야 하는 경우 Array나 List 등을 통해 값을 꺼내오기에는 의미를 분명히 하는데 한계가 있을 수 있겠다고 생각


private final int number;
private final int value;

static {
IntStream.range(MIN, MAX + 1)
.forEach(i -> cacheLottoNumber[i] = new LottoNumber(i));
}

private LottoNumber(int number) {
this.number = number;
private LottoNumber(int value) {
this.value = value;
}

public static LottoNumber of(int number) {
validateNumber(number);
return cacheLottoNumber[number];
return generateLottoNumber(number);
}

private static void validateNumber(int number) {
Expand All @@ -32,14 +28,42 @@ private static void validateNumber(int number) {
}
}

public int toInt() {
return number;
private static LottoNumber generateLottoNumber(int number) {
LottoNumber lottoNumber = cacheLottoNumber.get(number);
if (lottoNumber == null) {
lottoNumber = new LottoNumber(number);
cacheLottoNumber.put(number, lottoNumber);
}
return lottoNumber;
}

public int getValue() {
return value;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof LottoNumber)) {
return false;
}

LottoNumber that = (LottoNumber) o;

return value == that.value;
}

@Override
public int hashCode() {
return value;
}

@Override
public String toString() {
return "LottoNumber{" +
"number=" + number +
"number=" + value +
'}';
}
}
41 changes: 18 additions & 23 deletions src/main/java/lotto/domain/LottoNumbers.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,19 @@
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class LottoNumbers {
private static final int LOTTO_COUNT = 6;
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 45;

private static final String DELIMITER = ",";
private static final String DUPLICATE_ERROR = "로또 개수는 중복이 불가능합니다.";
private static final String COUNT_ERROR = "로또 개수는 " + LOTTO_COUNT + "개로 제한됩니다.";
private static final String COUNT_ERROR = String.format("로또 개수는 %d개로 제한됩니다.", LOTTO_COUNT);

private static final List<LottoNumber> candidateLottoNumbers = new ArrayList<>();

static {
for (int i = MIN_NUMBER; i <= MAX_NUMBER; i++) {
for (int i = LottoNumber.MIN; i <= LottoNumber.MAX; i++) {
candidateLottoNumbers.add(LottoNumber.of(i));
}
}
Expand All @@ -31,45 +27,44 @@ public LottoNumbers() {
this.lottoNumbers = generateRandomLottoNumbers();
}

public LottoNumbers(String input) {
String[] stringArr = input.split(DELIMITER);
validateLottoNumbers(stringArr);
this.lottoNumbers = convertStringArrToIntegerList(stringArr);
public LottoNumbers(List<Integer> numbers) {
Copy link
Author

Choose a reason for hiding this comment

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

최소한의 검증을 거치고 생성자에 파라미터 보내줄 수 있도록 View와 Domain의 로직 분리

validateLottoNumbers(numbers);
this.lottoNumbers = convertToLottoNumberList(numbers);
}

private List<LottoNumber> generateRandomLottoNumbers() {
Collections.shuffle(candidateLottoNumbers);
return List.copyOf(candidateLottoNumbers.subList(0, LOTTO_COUNT));
}

private void validateLottoNumbers(String[] stringArr) {
validateLottoCount(stringArr);
validateDuplicateCount(stringArr);
private void validateLottoNumbers(List<Integer> numbers) {
validateLottoCount(numbers);
validateDuplicateCount(numbers);
}

private void validateLottoCount(String[] array) {
if (array.length != LOTTO_COUNT) {
private void validateLottoCount(List<Integer> numbers) {
if (numbers.size() != LOTTO_COUNT) {
throw new IllegalArgumentException(COUNT_ERROR);
}
}

private void validateDuplicateCount(String[] arr) {
int distinctCount = calDistinctCountFromArray(arr);
private void validateDuplicateCount(List<Integer> numbers) {
int distinctCount = calDistinctCountFromArray(numbers);

if (arr.length != distinctCount) {
if (numbers.size() != distinctCount) {
throw new IllegalArgumentException(DUPLICATE_ERROR);
}
}

private int calDistinctCountFromArray(String[] arr) {
return (int) Arrays.stream(arr)
private int calDistinctCountFromArray(List<Integer> numbers) {
return (int) numbers.stream()
.distinct()
.count();
}

private List<LottoNumber> convertStringArrToIntegerList(String[] array) {
return Arrays.stream(array)
.map(string -> LottoNumber.of(Integer.parseInt(string)))
private List<LottoNumber> convertToLottoNumberList(List<Integer> numbers) {
return numbers.stream()
.map(LottoNumber::of)
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
}

Expand Down
30 changes: 22 additions & 8 deletions src/main/java/lotto/domain/LottoTicket.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,41 @@
import java.util.Objects;

public class LottoTicket {
private final List<LottoNumbers> lottoTickets = new ArrayList<>();
private final List<LottoNumbers> lottoTicket;

public LottoTicket(int count) {
generateTickets(count);
private LottoTicket(final List<LottoNumbers> lottoTicket) {
this.lottoTicket = lottoTicket;
}

private void generateTickets(int count) {
public static LottoTicket createAutoLottoTicket(int count) {
return new LottoTicket(generateTickets(count));
}

private static List<LottoNumbers> generateTickets(int count) {
List<LottoNumbers> lottoTicket = new ArrayList<>();
for (int i = 0; i < count; i++) {
lottoTickets.add(new LottoNumbers());
lottoTicket.add(new LottoNumbers());
}
return lottoTicket;
}

public static LottoTicket createManualLottoTicket(List<LottoNumbers> lottoTicket) {
return new LottoTicket(lottoTicket);
}

public List<LottoNumbers> getLottoTickets() {
return Collections.unmodifiableList(lottoTickets);
public List<LottoNumbers> getLottoTicket() {
return Collections.unmodifiableList(lottoTicket);
}

public WinningResult calculateWinningStatistic(WinningNumbers winningNumbers) {
List<Ranking> rankings = lottoTickets.stream()
List<Ranking> rankings = lottoTicket.stream()
.map(winningNumbers::calculateRanking)
.filter(Objects::nonNull)
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
return new WinningResult(rankings);
}

public void addLottoTicket(LottoTicket otherLottoTicket) {
lottoTicket.addAll(otherLottoTicket.getLottoTicket());
}
}
Loading