일단 돌아가는 쓰레기 만들기...
이번 미션의 개인적인 목표은 '코드를 어떻게 계속해서 객체지향적으로 리펙토링할 것인가?'이다. 솔직히 숫자 야구 게임을 돌아가게 만드는 것은 누구나 할 수 있다. 하지만 확장성, 재사용성 등을 챙기며 다른 사람이 봐도 이 코드가 어떤 역할을 하는지, 그리고 한눈에 봐도 깔끔한 코드를 짠다는 것은 너무 어려운 일이다.
따라서 이번 미션 뿐만이 아니라 남은 24년도에는 객체지향적으로 클린한 코드를 짜는 것, 즉 처음부터 클린 코드를 짜는 것은 어렵기에 이전에 작성한 코드를 내가 더 나은 코드로 변경할 수 있도록 훈련하는 것이 목표이다. 하지만 역시 쉽지 않았다...
먼저 무작정 분리만 한다고 해서 깔끔해 보이는 것도 아닐테니 일단은 제일 간단하면서 중복되는 코드들로 해당 로직을 짜봤다. 진짜...진짜 자바가 부족하다보니 이상하게 짰지만 일부러 코드들을 수정하기 위해 뇌빼고 작성했으니 초반 코드는 이해하려하지 말고 그냥 물 흐르듯 넘어가주시면 감사하겠다. 내용 위주로........
Application.java -- 실행 클래스
import game.GameLogic;
import game.RandomNum;
import view.InputView;
public class Application {
public static void main(String[] args) {
// 시작 - 게임 시작을 알리는 문구, 랜덤 숫자 생성
RandomNum randomNums = new RandomNum();
String randomNum = randomNums.randomCreation();
while (true) {
String num = InputView.inputNumber();
// 해당 입력 숫자의 볼, 스트라이크, 아웃에 대한 정보 출력, 만약 세자리 수 모두 맞으면 정답임을 출력
GameLogic gameLogic = new GameLogi(num, randomNum);
System.out.println("스트라이크" + gameLogic.getStrike() + "볼" + gameLogic.getBall() + "아웃" + computer.getOut());
if (gameLogic.getStrike() == 3) {
System.out.println(Guide.SUCCESS_MSG);
break;
}
}
}
}
GameLogic.java -- 볼, 스트라이크, 아웃 클래스
public class GameLogic { // 볼, 스트라이크, 아웃 메서드
private static int ball;
private static int strike;
private static int out;
private static int[] result;
public GameLogic(String num, String randomNum){
ball = 0;
strike = 0;
out = 0;
ball(num, randomNum);
strike(num, randomNum);
out(num, randomNum);
}
public static int getBall() {
return ball;
}
public static int getStrike() {
return strike;
}
public static int getOut() {
return out;
}
public static void strike(String num, String randomNum){ //123
String[] numbers = num.split("");
String[] randomNumbers = randomNum.split("");
for (int i = 0; i < numbers.length; i++) {
if (numbers[i].equals(randomNumbers[i])) {
strike++;
}
}
}
public static void ball(String num, String randomNum){
String[] numbers = num.split("");
String[] randomNumbers = randomNum.split("");
for (int i = 0; i < numbers.length; i++) {
if (randomNum.contains(numbers[i]) && !numbers[i].equals(randomNumbers[i])) {
ball++;
}
}
}
public static void out(String num, String randomNum){
String[] numbers = num.split("");
String[] randomNumbers = randomNum.split("");
for (int i = 0; i < numbers.length; i++) {
if (!randomNum.contains(numbers[i]) && !numbers[i].equals(randomNumbers[i])) {
out++;
}
}
}
}
그러면 일단 이 로직에서 걸리는 것들이 있다. 멤버 변수가 모두 static으로 선언되어 있다는 것. 이 부분은 인스턴스 변수로써 활용될 수 있도록 해야 할 것이고, 또 ball, strike, out이 굳이 다른 메서드로 만들 필요없이 하나의 메서드로 조건문을 통해서 해결할 수 있다는 점이다. 따라서 관련해서 로직을 수정했다.
멤버 변수에서 static을 제거하고 아예 인스턴스 변수로써 작동하도록 했고, strike, ball, out의 Count를 초기화하는 메서드를 만들어 gameResult 메서드가 실행될 때 resetCounts() 메서드를 통해서 맨 처음에 초기화 되도록 작성했다.
public class GameLogic {
private int ball;
private int strike;
private int out;
public GameLogic(){
this.ball = 0;
this.strike = 0;
this.out = 0;
}
public int getBall() {
return ball;
}
public int getStrike() {
return strike;
}
public int getOut() {
return out;
}
public void gameResult(String num, String randomNum){
resetCounts();
String[] numbers = num.split("");
String[] randomNumbers = randomNum.split("");
for (int i = 0; i < randomNumbers.length; i++) {
if (numbers[i].equals(randomNumbers[i])) {
strike++;
} else if (randomNum.contains(numbers[i])) {
ball++;
} else {
out++;
}
}
}
private void resetCounts(){
this.strike = 0;
this.ball = 0;
this.out = 0;
}
}
근데 사실 이 로직도 그닥 마음에 들지는 않는다. 게임 결과를 계산하기 위한 로직이 너무 메서드 안에서 결합이 강하게 되어있으면 추후 규칙이 변경이 된다거나 할 때 수정이 쉽지 않을 수 있다. 일단 보류하고 실행 로직쪽을 변경하겠다.
이전에는 아예 Application 클래스에서 입력받고 출력하고 다했을 테지만 우리는 조금 성장했을 테니 분리해준다.
우선 main메서드에서는 단순히 실행만 시키도록 한다.
Applcation.java
public class Application {
public static void main(String[] args) {
BaseballGame baseballGame = new BaseballGame();
baseballGame.start();
}
}
그리고 Application에 구현해둔 게임 실행 로직을 BaseballGame 클래스로 옮겼다. 참고로 보이는 InputView나 OutputView같은 경우는 입출력을 따로 분리해서 관리하기 위해 클래스를 별도로 생성했었다. 이 부분은 추후 완성 후 다룰 예정!!
BaseballGame 클래스를 생성하면서 사용자가 입력하는 문자에 대한 예외처리도 철저하게 진행했다.
먼저 예외상황이 일어날 수 있는 코드들을 try-catch문으로 감쌌다. 그리고 사용자가 숫자를 입력할 때 해당 숫자를 검증하는 클래스로 이동하게 만들었다.
BaseballGame.java
package game;
import exception.PlayerNumException;
import game.constant.GameRule;
import view.InputView;
import view.OutputView;
import view.constant.Guide;
import static exception.ErrorMsg.NOT_CORRECT_INPUT;
import static view.constant.Guide.GAME_START;
public class BaseballGame {
public void start(){
OutputView.print(GAME_START);
RandomNum randomNums = new RandomNum();
GameLogic gameLogic = new GameLogic();
String computerNum = randomNums.randomCreation();
while (true) {
try {
String playerNum = InputView.inputNumber();
PlayerNumException.validatePlayerNum(playerNum); // 입력 검증
gameLogic.gameResult(playerNum, computerNum);
System.out.println("스트라이크" + gameLogic.getStrike() + " 볼" + gameLogic.getBall() + " 아웃" + gameLogic.getOut());
if (gameLogic.getStrike() == GameRule.NUM_SIZE) {
System.out.println(Guide.SUCCESS_MSG);
break;
}
} catch (IllegalArgumentException e) {
System.out.println(NOT_CORRECT_INPUT);
System.out.println(e.getMessage());
}
}
}
}
PlayerNumException 클래스에서 예외처리 한 것들
1. 숫자만을 입력했는가?
2. 세자리 숫자인가?
3. 중복되는 숫자가 없는가?
4. 1부터 9까지의 숫자만 작성했는가?
해당 메서드들을 각각 스트림과 람다를 이용해서 로직을 짰고, validatePlayerNum메서드를 public static으로 선언한 후, 사용자가 입력한 숫자를 매개변수로 받아 각각 순서대로 검증할 수 있도록 했다. 입력메세지의 경우는 따로 클래스를 만들어서 상수로 관리하였다.
PlayerNumException.java
import java.util.Arrays;
import static exception.ErrorMsg.*;
import static game.constant.GameRule.*;
public class PlayerNumException {
public static void validatePlayerNum(String playerNum){
isDigitNumber(playerNum);
checkThreeDigitInput(playerNum);
isUnique(playerNum);
isInRange1To9(playerNum);
}
private static void isDigitNumber(String playerNum){
boolean isNotNumber = playerNum.chars()
.anyMatch(c -> !Character.isDigit(c));
if (isNotNumber) {
throw new IllegalArgumentException(NOT_NUM);
}
}
private static void checkThreeDigitInput(String playerNum){
if (playerNum.length() != NUM_SIZE) {
throw new IllegalArgumentException(NOT_THREE_NUM);
}
}
private static void isUnique(String playerNum){
long uniqueCount = Arrays.stream(playerNum.split(""))
.distinct()
.count();
if (uniqueCount != NUM_SIZE) {
throw new IllegalArgumentException(DUPLICATE_NUM);
}
}
private static void isInRange1To9(String playerNum){
boolean outOfRange = playerNum.chars()
.anyMatch(a -> a < '1' || a > '9');
if (outOfRange) {
throw new IllegalArgumentException(OUT_OF_RANGE);
}
}
}
그러면 아래와 같이 콘솔에 메세지가 뜬다! 여기까지가 Lv2 작업!