BaseBall

package domain;

import utils.RandomUtils;

import java.util.ArrayList;
import java.util.List;

public class BaseBall {
    private static final int MIN_DIGIT = 1;
    private static final int MAX_DIGIT = 9;
    private static final int BASEBALL_LENGTH = 3;

    private List<Integer> baseballNumber;

    private BaseBall(List<Integer> baseballNumber) {
        this.baseballNumber = baseballNumber;
    }

    public static BaseBall createRandomBaseBall() {
        List<Integer> randomDigit = getRandomDigit();
        return new BaseBall(randomDigit);
    }

    public static BaseBall createBaseBall(int baseBallNumber) throws IllegalStateException {
        validateNumber(baseBallNumber);
        List<Integer> ballNumber = mapToList(baseBallNumber);
        return new BaseBall(ballNumber);
    }

    private static List<Integer> getRandomDigit() {
        List<Integer> randomNumber = new ArrayList<>();
        while (randomNumber.size() < BASEBALL_LENGTH) {
            int randomDigit = RandomUtils.nextInt(MIN_DIGIT, MAX_DIGIT);
            if (!randomNumber.contains(randomDigit)) {
                randomNumber.add(randomDigit);
            }
        }
        return randomNumber;
    }

    private static void validateNumber(int baseBallNumber) throws IllegalStateException {
        String number = String.valueOf(baseBallNumber);
        if (number.length() != BASEBALL_LENGTH) {
            throw new IllegalStateException(BASEBALL_LENGTH + "자리 숫자를 입력해주세요.");
        }
    }

    private static List<Integer> mapToList(int baseBallNumber) {
        List<Integer> ballNumber = new ArrayList<>();
        for (int digit = 100; digit > 0; digit /= 10) {
            ballNumber.add(baseBallNumber / digit);
            baseBallNumber %= digit;
        }
        return ballNumber;
    }

    public boolean containsNumber(int number) {
        return baseballNumber.contains(number);
    }

    public boolean hasNumber(int index, int number) {
        return baseballNumber.get(index) == number;
    }

    public int getNumber(int index) {
        return baseballNumber.get(index);
    }

    public int size() {
        return baseballNumber.size();
    }
}

Baseball 도메인 모델이다. 도메인 모델이라고 표현하는 게 맞는지는 잘 모르겠다... 일단 Baseball 게임의 세자리 숫자를 갖고 있는 객체이다. 외부에서 List를 바꾸지 못하게 필요한 기능들만 외부에 전달한다. randomBaseball과 입력받는 baseball을 각각 생성할 수 있도록 만들었다. 인터페이스를 만들어 각각 상속받아 따로 만드는 방법이 더 괜찮았을 것 같기도 하고... 계속 공부해가며 알아가봐야겠다.


BaseballResult

package baseball;

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class BaseballResult {
    private static final int ANSWER_COUNT = 3;
    private static final int ZERO = 0;
    private static final String NOTHING = "낫싱";
    private Map<BallType, Integer> result;

    public BaseballResult() {
        initResult();
    }

    private void initResult() {
        Map<BallType, Integer> result = new HashMap<>();
        this.result = result;
    }

    public void accumulateBallType(BallType ballType) {
        result.put(ballType, result.getOrDefault(ballType, 0) + 1);
    }

    public String getResult() {
        if (isNothing()) {
            return NOTHING;
        }
        return ballAndStrike();
    }

    private boolean isNothing() {
        return result.isEmpty();
    }

    private String ballAndStrike() {
        Set<BallType> ballTypes = result.keySet();
        return ballTypes.stream()
                .map(ballType -> ballType.getNameWith(countFor(ballType)))
                .sorted(Comparator.reverseOrder())  //볼이 먼저 나오게 끔 sorting
                .collect(Collectors.joining(" "));
    }

    private int countFor(BallType ballType) {
        if (!result.containsKey(ballType)) {
            return ZERO;
        }
        return result.get(ballType);
    }

    public boolean isAnswer() {
        return result.containsKey(BallType.STRIKE) && result.get(BallType.STRIKE) == ANSWER_COUNT;
    }

}

게임 결과를 담는 결과 객체이다. 처음에는 getResult()함수가 print까지 처리하다가 Print전용 객체를 따로 빼고 분리하였다. 최대한 Map 컬렉션 자체를 외부에 공개하기보다 필요한 기능만 외부로 공개해서 불변성을 유지하려 노력했다.

BallType.enum

package baseball;

public enum BallType {
    BALL("볼"),
    STRIKE("스트라이크");

    private String name;

    BallType(String name) {
        this.name = name;
    }

    public String getNameWith(int count) {
        return count + name;
    }
}

예전에 우아한 형제들 기술 블로그에서 enum사용법을 읽고 자주 사용하는 기법이다. 개인적으로 enum을 사용하고 코드 전체가 보기에 편해진 기분이다. getNameWith함수도 프린트 역할도 같이 했지만 Print전용 객체를 빼면서 문자열만 리턴할 수 있도록 했다.

BallTypeChecker

package baseball;

import domain.BaseBall;

public class BallTypeChecker {
    private BaseBall randomBaseball;

    private BallTypeChecker(BaseBall randomBaseball) {
        this.randomBaseball = randomBaseball;
    }

    public static BallTypeChecker ballTypeCheckWith(BaseBall randomBaseball) {
        return new BallTypeChecker(randomBaseball);
    }

    public BaseballResult startChecking(BaseBall inputBaseball) {
        BaseballResult baseballResult = new BaseballResult();
        compare(inputBaseball, baseballResult);
        return baseballResult;
    }

    private void compare(BaseBall inputBaseball, BaseballResult baseballResult) {
        for (int index = 0; index < inputBaseball.size(); index++) {
            if (isStrike(index, inputBaseball)) {
                baseballResult.accumulateBallType(BallType.STRIKE);
                continue;
            }
            if (isBall(index, inputBaseball)) {
                baseballResult.accumulateBallType(BallType.BALL);
                continue;
            }
        }
    }

    private boolean isStrike(int index, BaseBall inputBaseball) {
        return randomBaseball.hasNumber(index, inputBaseball.getNumber(index));
    }

    private boolean isBall(int index, BaseBall inputBaseball) {
        return randomBaseball.containsNumber(inputBaseball.getNumber(index));
    }

}

결과를 체크해주는 클래스이다. 즉, BaseballResult를 리턴해준다.

BaseballGameExecutor

package baseball;

import domain.BaseBall;

import java.util.InputMismatchException;
import java.util.Scanner;

public class BaseballGameExecutor {
    private static Scanner scanner;
    private static final int RESTART_NUM = 1;
    private static final int END_NUM = 2;

    public static void startGame(Scanner inputScanner) {
        scanner = inputScanner;
        BallTypeChecker ballTypeChecker = readyForGame();
        guessNumber(ballTypeChecker);
    }

    private static BallTypeChecker readyForGame() {
        BaseBall randomBaseBall = BaseBall.createRandomBaseBall();
        return BallTypeChecker.ballTypeCheckWith(randomBaseBall);
    }

    private static void guessNumber(BallTypeChecker ballTypeChecker) {
        BaseBall inputBaseball = inputBaseballNumber();
        BaseballResult baseballResult = ballTypeChecker.startChecking(inputBaseball);
        Printer.result(baseballResult);
        if (baseballResult.isAnswer()) {
            Printer.isCorrect();
            askToRestart();
        } else {
            guessNumber(ballTypeChecker);
        }
    }

    private static BaseBall inputBaseballNumber() {
        try {
            int inputNumber = inputNumber();
            return BaseBall.createBaseBall(inputNumber);
        } catch (IllegalStateException e) {
            Printer.printMessage(e.getMessage());
            return inputBaseballNumber();
        }
    }

    private static int inputNumber() {
        try {
            Printer.inputNumber();
            int inputNumber = scanner.nextInt();
            return inputNumber;
        } catch (InputMismatchException e) {
            scanner.nextLine(); //개행문자 버그를 잡기 위해
            Printer.onlyNumber();
            return inputNumber();
        }
    }

    private static void askToRestart() {
        Printer.askRestart();
        try {
            int reStart = scanner.nextInt();
            if (reStart == RESTART_NUM) {
                startGame(scanner);
            } else if (reStart != END_NUM) {
                askToRestart();
            }
        } catch (InputMismatchException e) {
            scanner.nextLine(); //개행문자 버그를 잡기 위해
            Printer.onlyNumber();
            askToRestart();
        }
    }

}

BaseballGame을 실행해주는 클래스이다. 여기서 프린트 역할도 맡게 했었는데 아무래도 너무 역할이 많은 것 같아 프린트 역할은 따로 분리했다. 대부분 예외를 잡아 다시 실행해주고 흐름을 조정해주는 역할을 맡고 있다. 궁금한 건 재귀함수를 통해 흐름을 조정해주는데 계속해서 재귀함수 호출이 일어난다면 Stack에 함수가 쌓일텐데 이렇게 재귀함수 호출이 자주 일어난다면 반복문으로 돌리는 것이 맞는 선택일까 궁금하다.