백엔드

로또 - 객체지향

육빔 2024. 3. 18. 21:03
728x90
반응형

교내 개발 동아리의 첫 과제였다.

로또 프로그램을 객체지향적으로 짜기..

 

머리박고 짜봤는데 객체지향적으로는 도저히 못 짜겠어서 일단은 전부 박았다;

그 후에 분리를 시켜서 객체로 생성하여 분리시키는데 정말 효울이 쓰레기로 나오는 것 같았다.

시간도 너무 오래 걸리고 코드도 더러운 걸 보니 화가 치밀어 올라왔다.

https://github.com/urinaner/java-lotto

 

GitHub - urinaner/java-lotto

Contribute to urinaner/java-lotto development by creating an account on GitHub.

github.com

내가 짠 코드;;

 

전혀 객체지향적으로도 안되어있고 예외처리, 캡슐화, 상속 등 java의 장점을 살리지 못하였다.

 

 

만들때 객체지향 생활 체조법칙을 지키면서 짜는걸 추천하셨는데 알지만 실천이 안되었다.

 

https://jamie95.tistory.com/99

 

[Java] 객체지향 생활 체조 원칙 9가지 (from 소트웍스 앤솔러지)

1. 한 메서드에 오직 한 단계의 들여쓰기만 한다. 한 메서드에 들여쓰기가 여러 개 존재한다면, 해당 메서드는 여러가지 일을 하고 있다고 봐도 무관하다. 메서드는 맡은 일이 적을수록(잘게 쪼

jamie95.tistory.com

 

 

그래서 학교 동아리 멘토님이 짠 코드를 보면서 놀라면서 공부중이다..

 

https://github.com/kokodak/java-lotto/tree/kokodak

 

GitHub - kokodak/java-lotto: 로또 미션을 진행하는 저장소

로또 미션을 진행하는 저장소. Contribute to kokodak/java-lotto development by creating an account on GitHub.

github.com

일단 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 사용법이 익숙하지 않아 공부를 좀 해야될듯하다..

728x90
반응형