숫자 야구 게임의 로직 자체는 워낙 간단해서 금방 개발할 수 있었지만 !
합격하게 된다면 우리 선배님들의 코드들을 보고 다시 할 수 밖에 없었다 ㅋㅋㅋ.
수준들이 정말 ....
이전 코드도 올려서 비교했다면 좋았을 것 같긴하지만..
너무 수준 낮고 완성도랄게 없는 것 같다.
반드시 놓치는 부분이 있을 것 이기 때문에
몇가지 사항만큼은 이번 1주차 과제를하며 놓치지 않기 위해 노력했다.
MVC 패턴을 적용 하고자했다.
MVC 중 모델에 해당하는 패키지 이다.
BaseballNumber.java
public class BaseballNumber {
private static final Map<Integer, BaseballNumber> CACHE = new HashMap<>();
private final int baseballNumber;
public BaseballNumber(int baseballNumber) {
this.baseballNumber = baseballNumber;
}
public static BaseballNumber intToBaseballNumber(int baseballNumber) {
return CACHE.computeIfAbsent(baseballNumber, BaseballNumber::new);
}
}
BaseballNumbers.java
public class BaseballNumbers {
private static final int NUMBER_COUNT = 3;
private static final int NUMBER_MINIMUM_RANGE = 1;
private static final int NUMBER_MAXIMUM_RANGE = 9;
private final List<BaseballNumber> baseballNumbers;
private BaseballNumbers(List<BaseballNumber> baseballNumbers) {
this.baseballNumbers = baseballNumbers;
}
public static BaseballNumbers generateRandomNumbers() {
Set<Integer> randomNumbers = new HashSet<>();
while(randomNumbers.size() < NUMBER_COUNT) {
int randomNumber = RandomUtils.nextInt(NUMBER_MINIMUM_RANGE, NUMBER_MAXIMUM_RANGE);
randomNumbers.add(randomNumber);
}
List<BaseballNumber> baseballNumbers = randomNumbers.stream()
.map(BaseballNumber::intToBaseballNumber)
.collect(Collectors.toList());
return new BaseballNumbers(baseballNumbers);
}
public static BaseballNumbers generateInputNumbers(List<Integer> inputNumberList) {
List<BaseballNumber> baseballNumbers =
inputNumberList.stream()
.map(BaseballNumber::intToBaseballNumber)
.collect(Collectors.toList());
return new BaseballNumbers(baseballNumbers);
}
public boolean match(BaseballNumbers targetBaseballNumbers, int index) {
BaseballNumber baseballNumber = this.baseballNumbers.get(index);
BaseballNumber targetBaseballNumber = targetBaseballNumbers.baseballNumbers.get(index);
return baseballNumber.equals(targetBaseballNumber);
}
public boolean contains(BaseballNumbers targetBaseballNumbers, int index) {
BaseballNumber targetBaseballNumber = targetBaseballNumbers.baseballNumbers.get(index);
return this.baseballNumbers.contains(targetBaseballNumber);
}
}
게임의 핵심 자료가 될 3자리 숫자를 저장하기 위한 BaseballNumber와 일급 컬렉션으로 감싼 BaseballNumbers이다.
BaseballNumbers에서 숫자를 만들어 조합한다.
Set이 중복을 막아주기 때문에 중복에 대한 검증은 자연스럽게 해결할 수 있었고,
순서를 보장하지 않지만 만들어진 숫자의 순서는 완성 후에 변하지만 않는다면 중요하지 않았다.
BaseballNumbers 에는 결과를 계산하기 위한 match와 contains 메서드가 있다.
단일 책임 원칙을 위해 계산을 별도의 클래스에서 모두 해결하고 싶었으나,
자료의 불변을 위한 private final 생성자로 인해 외부에서는 데이터에 접근할 수가 없다.
결국 결과 계산을 위한 클래스 ScoreChecker를 분리하긴 했지만, 완전한 분리는 되지 않은 것 같다.
GameController.java
public class GameController {
private final BaseballNumbers targetNumbers;
private final InputView inputView;
public GameController(InputView inputview) {
this.targetNumbers = BaseballNumbers.generateRandomNumbers();
this.inputView = inputview;
}
public boolean playGame() {
//숫자 입력
boolean exitCheck = false;
while (!exitCheck) {
BaseballNumbers userNumbers = BaseballNumbers.generateInputNumbers(inputView.inputNumber());
//점수 계산
int strikeCount = ScoreChecker.strikeCounting(targetNumbers, userNumbers);
int ballCount = ScoreChecker.ballCounting(targetNumbers, userNumbers);
//종료 확인
GameResult gameResult = new GameResult(ballCount, strikeCount);
exitCheck = OutputView.outMessage(gameResult);
}
return inputView.inputRestartOpt();
}
}
게임의 메인 컨트롤러이다.
게임 요구사항 중,
정답이 아니면 입력과 계산을 반복한다.
정답을 맞추고 게임이 종료되면 게임을 재시작할 수 있다.
정답이 아닐 때, exitCheck 코드를 while문으로 돌리고 있다.
게임을 재시작 하는 부분도 같은 처리를 하는데..
이 코드가 가독성이 떨어지는거 같고, boolean exitCheck 변수의 존재가 맘에 들지 않아서 do-while도 고려했지만
do-while이 클린 코드적으로는 더 별로인거 같았다. ㅠ
ScoreCheck.java
public class ScoreChecker {
private static final int NUMBER_INDEX_START = 0;
private static final int NUMBER_INDEX_END = 2;
public static int strikeCounting(BaseballNumbers target, BaseballNumbers user) {
return (int) IntStream.rangeClosed(NUMBER_INDEX_START, NUMBER_INDEX_END)
.filter(index -> isStrike(target, user, index))
.count();
}
private static boolean isStrike(BaseballNumbers target, BaseballNumbers user, int index) {
return target.match(user, index);
}
public static int ballCounting(BaseballNumbers target, BaseballNumbers user) {
return (int) IntStream.rangeClosed(NUMBER_INDEX_START, NUMBER_INDEX_END)
.filter(index -> isBall(target, user, index))
.count();
}
private static boolean isBall(BaseballNumbers target, BaseballNumbers user, int index) {
return !target.match(user, index)
&& target.contains(user, index);
}
}
핵심 계산 로직이다.
어떻게 보면 이렇다할게 없지만 람다에 약해서..ㅠ 정리를 해야할 거같다.
InputView.java
public class InputView {
private static final String INPUT_NUMBER_MESSAGE = "숫자를 입력해주세요. : ";
private static final String DELIM = "";
private static final String RESTART_INFO_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.";
private static final String RE_INPUT_NUMBER_MESSAGE = "숫자를 재 입력 해주세요. : ";
private static final int NUMBER_COUNT = 3;
private static final int RESTART_OPT_LENGTH = 1;
private static final int RESTART_CODE = 1;
private static final int END_CODE = 2;
private static final char NUMBER_MINIMUM_VALUE = '1';
private static final char NUMBER_MAXIMUM_VALUE = '9';
private final Scanner scanner;
public InputView(Scanner scanner) {
this.scanner = scanner;
}
public List<Integer> inputNumber() {
System.out.print(INPUT_NUMBER_MESSAGE);
String inputNumber = scanner.nextLine();
if (!isValidationInputNumber(inputNumber)) {
inputNumber = scanner.nextLine();
}
return Arrays.stream(inputNumber.split(DELIM))
.map(Integer::parseInt)
.collect(Collectors.toList());
}
public boolean inputRestartOpt() {
System.out.println(RESTART_INFO_MESSAGE);
String inputNumber = scanner.nextLine();
if (!isValidationRestartOpt(inputNumber)) {
System.out.println(RESTART_INFO_MESSAGE);
inputNumber = scanner.nextLine();
}
return Integer.parseInt(inputNumber) == RESTART_CODE;
}
private boolean isValidationRestartOpt(String inputNumber) {
try {
validationRestartOpt(inputNumber);
return true;
} catch (IllegalArgumentException e) {
System.out.print(RE_INPUT_NUMBER_MESSAGE);
return false;
}
}
private void validationRestartOpt(String inputNumber) {
boolean isOneLengthInt = inputNumber.chars().count() == RESTART_OPT_LENGTH;
if (!isOneLengthInt) throw new IllegalArgumentException();
int restartOpt = Integer.parseInt(inputNumber);
if (!(restartOpt == RESTART_CODE || restartOpt == END_CODE)) {
throw new IllegalArgumentException();
}
}
private boolean isValidationInputNumber(String inputNumber) {
try {
validationInputNumber(inputNumber);
return true;
} catch (IllegalArgumentException e) {
System.out.print(RE_INPUT_NUMBER_MESSAGE);
return false;
}
}
private void validationInputNumber(String inputNumber) {
boolean isDuplicated = inputNumber.chars()
.filter(number -> NUMBER_MINIMUM_VALUE <= number && number <= NUMBER_MAXIMUM_VALUE)
.distinct()
.count() == NUMBER_COUNT;
if (!isDuplicated) throw new IllegalArgumentException();
}
}
사용자의 입력을 받고 입력에 대한 검증을 한다.
코드를 짜며 검증을 위한 메서드를 검증기로 따로 빼야할 지 고민됬다.
검증을 위한 클래스를 짜는 것이 단일 책임 원칙에 더 알맞은 것 같긴한데..
또 GameController의 메인 play로직의 리턴을 InputView의 검증 메서드를 활용하는데, 이제보니 이건 확실히 리팩토링이 필요한 것 같다.
로직은 굉장히 간단한데 첫 주째부터 몇일이 걸린지 모르겠다.
이래가지군 1차를 붙어도 최종 코테 소화를 못할거 같은데..
어디서 줏어 들은 수준의 개발 지식이 개탄 스럽다.
클린 코드와 OOP 책을 사야것는디?
프리 코스 준비만 후딱 끝내고 찾아보자 ㅠ