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을 각각 생성할 수 있도록 만들었다. 인터페이스를 만들어 각각 상속받아 따로 만드는 방법이 더 괜찮았을 것 같기도 하고... 계속 공부해가며 알아가봐야겠다.
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 컬렉션 자체를 외부에 공개하기보다 필요한 기능만 외부로 공개해서 불변성을 유지하려 노력했다.
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전용 객체를 빼면서 문자열만 리턴할 수 있도록 했다.
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를 리턴해준다.
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에 함수가 쌓일텐데 이렇게 재귀함수 호출이 자주 일어난다면 반복문으로 돌리는 것이 맞는 선택일까 궁금하다.