로또 - 객체지향
교내 개발 동아리의 첫 과제였다.
로또 프로그램을 객체지향적으로 짜기..
머리박고 짜봤는데 객체지향적으로는 도저히 못 짜겠어서 일단은 전부 박았다;
그 후에 분리를 시켜서 객체로 생성하여 분리시키는데 정말 효울이 쓰레기로 나오는 것 같았다.
시간도 너무 오래 걸리고 코드도 더러운 걸 보니 화가 치밀어 올라왔다.
https://github.com/urinaner/java-lotto
내가 짠 코드;;
전혀 객체지향적으로도 안되어있고 예외처리, 캡슐화, 상속 등 java의 장점을 살리지 못하였다.
만들때 객체지향 생활 체조법칙을 지키면서 짜는걸 추천하셨는데 알지만 실천이 안되었다.
https://jamie95.tistory.com/99
그래서 학교 동아리 멘토님이 짠 코드를 보면서 놀라면서 공부중이다..
https://github.com/kokodak/java-lotto/tree/kokodak
일단 3개의 폴더로 나눠 가독성을 높였다.
controller
domain
view
domain 이 핵심이고 나머지는 끼워 맞추기 식이다.
domain을 보면 총 Lotto, LottoGenerator, Lottos, WinningLotto, WinningRank, WinningStatistics 클래스로 구분되는데
Lotto부터 확인해보면
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
validate(numbers);
this.numbers = numbers;
}
private void validate(List<Integer> numbers) {
if (!isSizeSix(numbers) || isDuplicate(numbers)) {
throw new IllegalArgumentException();
}
}
private boolean isSizeSix(List<Integer> numbers) {
return numbers.size() == 6;
}
private boolean isDuplicate(List<Integer> numbers) {
Set<Integer> duplicateChecker = new HashSet<>(numbers);
return duplicateChecker.size() != 6;
}
public List<Integer> getNumbers() {
return numbers;
}
}
로또 클래스에서 로또List를 받아서 개수가 6개인지 or 중복되는지 유효성 검증을 하여 Lotto 배열에 저장하는 코드였고,
LottoGenerator
public class LottoGenerator {
private static final int LOTTO_PRICE = 1_000;
private static final int LOTTO_NUMBER_LOWER_LIMIT = 1;
private static final int LOTTO_NUMBER_UPPER_LIMIT = 45;
private static final int LOTTO_NUMBER_QUANTITY = 6;
private static final int ZERO = 0;
private static final String MONEY_SHOULD_BE_DIVIDED_BY_ONE_THOUSAND = "[ERROR] 구입 금액은 1,000원 단위로만 받을 수 있습니다.";
private final List<Lotto> lottos = new ArrayList<>();
private final int lottoQuantity;
public LottoGenerator(int money) {
validateMoney(money);
lottoQuantity = money / LOTTO_PRICE;
}
private void validateMoney(int money) {
if (isZeroOrNegativeNumber(money) || !isDividedByOneThousand(money)) {
throw new IllegalArgumentException(MONEY_SHOULD_BE_DIVIDED_BY_ONE_THOUSAND);
}
}
private boolean isZeroOrNegativeNumber(int money) {
return money <= ZERO;
}
private boolean isDividedByOneThousand(int money) {
return money % LOTTO_PRICE == ZERO;
}
public List<Lotto> generateLottos() {
for (int i = 0; i < lottoQuantity; i++) {
Lotto lotto = generateLotto();
lottos.add(lotto);
}
return lottos;
}
private Lotto generateLotto() {
List<Integer> randomNumbers = new ArrayList<>(
Randoms.pickUniqueNumbersInRange(LOTTO_NUMBER_LOWER_LIMIT, LOTTO_NUMBER_UPPER_LIMIT,
LOTTO_NUMBER_QUANTITY));
randomNumbers.sort(Comparator.naturalOrder());
return new Lotto(randomNumbers);
}
public int getLottoQuantity() {
return lottoQuantity;
}
}
money에 값이 들어오면 -> 유효성검사 -> 로또 개수 저장 으로 진행되고
validateMoney -> money가 0이거나 1000으로 나눠떨어지지 않는다면 error 발생
generateLotto -> 랜덤으로 로또를 생성시키고 오름차순 정렬.
generateLottos -> 2차원 리스트에 lottoQuantity만큼 저장
Lottos
public class Lottos {
private List<Lotto> lottos;
public Lottos(List<Lotto> lottos) {
this.lottos = lottos;
}
public List<Lotto> getLottos() {
return lottos;
}
}
2차원 리스트 (로또 저장소) 선언 및 저장
WinningLotto
public class WinningLotto {
private static final String WINNING_NUMBERS_ARE_BETWEEN_ONE_AND_FORTY_FIVE = "[ERROR] 당첨 번호는 1부터 45 사이의 숫자여야 합니다.";
private static final String WINNING_NUMBERS_MUST_BE_SIX_DIFFERENT_NUMBERS = "[ERROR] 당첨 번호는 서로 다른 6개의 수여야 합니다.";
private static final String BONUS_NUMBER_IS_BETWEEN_ONE_AND_FORTY_FIVE = "[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.";
private static final String WINNING_NUMBERS_CONTAIN_BONUS_NUMBER = "[ERROR] 당첨 번호와 보너스 번호가 중복됩니다.";
private static final int LOTTO_NUMBER_LOWER_LIMIT = 1;
private static final int LOTTO_NUMBER_UPPER_LIMIT = 45;
private static final int LOTTO_NUMBERS_SIZE = 6;
private final List<Integer> winningNumbers;
private final int bonusNumber;
public WinningLotto(List<Integer> winningNumbers, int bonusNumber) {
validateWinningNumbers(winningNumbers);
validateBonusNumber(bonusNumber);
validateDuplicate(winningNumbers, bonusNumber);
this.winningNumbers = winningNumbers;
this.bonusNumber = bonusNumber;
}
private void validateWinningNumbers(List<Integer> winningNumbers) {
if (!isSixDifferentNumbers(winningNumbers)) {
throw new IllegalArgumentException(WINNING_NUMBERS_MUST_BE_SIX_DIFFERENT_NUMBERS);
}
if (!isBetweenOneAndFortyFive(winningNumbers)) {
throw new IllegalArgumentException(WINNING_NUMBERS_ARE_BETWEEN_ONE_AND_FORTY_FIVE);
}
}
private boolean isBetweenOneAndFortyFive(List<Integer> winningNumbers) {
for (int winningNumber : winningNumbers) {
if (winningNumber < LOTTO_NUMBER_LOWER_LIMIT || winningNumber > LOTTO_NUMBER_UPPER_LIMIT) {
return false;
}
}
return true;
}
private boolean isSixDifferentNumbers(List<Integer> winningNumbers) {
Set<Integer> duplicateChecker = new HashSet<>(winningNumbers);
return duplicateChecker.size() == LOTTO_NUMBERS_SIZE;
}
private void validateBonusNumber(int bonusNumber) {
if (!isBetweenOneAndFortyFive(bonusNumber)) {
throw new IllegalArgumentException(BONUS_NUMBER_IS_BETWEEN_ONE_AND_FORTY_FIVE);
}
}
private boolean isBetweenOneAndFortyFive(int bonusNumber) {
return bonusNumber >= LOTTO_NUMBER_LOWER_LIMIT && bonusNumber <= LOTTO_NUMBER_UPPER_LIMIT;
}
private void validateDuplicate(List<Integer> winningNumbers, int bonusNumber) {
if (winningNumbers.contains(bonusNumber)) {
throw new IllegalArgumentException(WINNING_NUMBERS_CONTAIN_BONUS_NUMBER);
}
}
public List<Integer> getWinningNumbers() {
return winningNumbers;
}
public int getBonusNumber() {
return bonusNumber;
}
}
당첨번호 리스트, 보너스숫자 들어오면 -> 유효성검사-> 저장
6개가 되는지, 유효범위 숫자인지에 대한 리스트, 보너스 숫자 검사 클래스
WinningRank
public enum WinningRank {
LAST_PLACE(0, false, 0),
FIFTH_PLACE(3, false, 5_000),
FOURTH_PLACE(4, false, 50_000),
THIRD_PLACE(5, false, 1_500_000),
SECOND_PLACE(5, true, 30_000_000),
FIRST_PLACE(6, false, 2_000_000_000);
private final int matchingCount;
private final boolean containsBonusNumber;
private final int winningPrice;
WinningRank(int matchingCount, boolean containsBonusNumber, int winningPrice) {
this.matchingCount = matchingCount;
this.containsBonusNumber = containsBonusNumber;
this.winningPrice = winningPrice;
}
public static WinningRank findWinningRank(int matchingCount, boolean containsBonusNumber) {
return Arrays.stream(values())
.filter(winningRank -> winningRank.matchingCount == matchingCount)
.filter(winningRank -> winningRank.containsBonusNumber == containsBonusNumber)
.findFirst()
.orElse(WinningRank.LAST_PLACE);
}
public int getMatchingCount() {
return matchingCount;
}
public int getWinningPrice() {
return winningPrice;
}
}
enum을 사용하여 코드를 줄였으며, stream .filter로 찾아서 enum에 맞는 타입 반환
맞은 개수와 보너스넘버 있는 경우가 넘어오면 그에 맞는 enum 타입 반환
WinningStatistics
public class WinningStatistics {
private static final int AT_LEAST_THIRD_PLACE = 5;
private static final int PERCENTAGE = 100;
private static final int INITIAL_VALUE = 0;
public static Map<WinningRank, Integer> getWinningDetails(Lottos lottos, WinningLotto winningLotto) {
Map<WinningRank, Integer> winningDetails = generateWinningDetails();
for (Lotto lotto : lottos.getLottos()) {
int matchingCount = compareNumbersWithWinningNumbers(lotto, winningLotto);
boolean containsBonusNumber = compareNumbersWithBonusNumber(lotto, winningLotto, matchingCount);
WinningRank winningRank = WinningRank.findWinningRank(matchingCount, containsBonusNumber);
winningDetails.replace(winningRank, winningDetails.get(winningRank) + 1);
}
return winningDetails;
}
public static Map<WinningRank, Integer> generateWinningDetails() {
Map<WinningRank, Integer> winningDetails = new EnumMap<>(WinningRank.class);
Arrays.stream(WinningRank.values()).forEach(winningRank -> winningDetails.put(winningRank, INITIAL_VALUE));
return winningDetails;
}
private static int compareNumbersWithWinningNumbers(Lotto lotto, WinningLotto winningLotto) {
List<Integer> numbers = lotto.getNumbers();
List<Integer> winningNumbers = winningLotto.getWinningNumbers();
return (int) numbers.stream()
.filter(winningNumbers::contains)
.count();
}
private static boolean compareNumbersWithBonusNumber(Lotto lotto, WinningLotto winningLotto, int matchingCount) {
if (matchingCount != AT_LEAST_THIRD_PLACE) {
return false;
}
List<Integer> numbers = lotto.getNumbers();
int bonusNumber = winningLotto.getBonusNumber();
return numbers.contains(bonusNumber);
}
public static long getWinningAmount(Map<WinningRank, Integer> winningDetails) {
return winningDetails.entrySet().stream()
.mapToLong(entry -> (long) entry.getKey().getWinningPrice() * entry.getValue())
.sum();
}
public static double getLottoYield(long winningAmount, int money) {
double lottoYield = PERCENTAGE + (double) (winningAmount - money) / money * PERCENTAGE;
return Math.round(lottoYield * 10) / 10.0;
}
}
Lotto와 WinningLotto를 비교해서 통계를 내는 클래스
자바 stream 사용법이 익숙하지 않아 공부를 좀 해야될듯하다..