[프리코스 1주차] 미션 수행 기록
[10/21 13:25] 착석 후 시작
남의 코드로 내 생각을 지우지 말자
쓰레기 코드를 쓰더라도 내 생각을 녹여 만들자
그래야 최종코테에 가서 내 생각을 피력할 수 있다
[10/21 13:28] 클론 완료 후 기능 요구사항 읽기
도메인 MyNumbers
일급컬렉션
그럼 컴퓨터가 만든 무작위 수도 일급컬렉션로 만들어야겠다
exception 클래스 빼고 validation 걸리면 해당 예외 throw 후순위로 미룰까
5시간 신경쓰지말고 일단 해보자
도메인 ComputerNumbers
일급컬렉션
한번 객체 만들고 불변하도록 final 필드
equals, hashcode 재정의 필요?
equals, hashcode 오버라이딩은 왜 하는거지?
MyNumbers 와 ComputerNumbers 를 비교하는 기능은 MyNumbers 에 구현해보자
어떤 식으로 비교할거지?
로직은 이런식으로
- ComputerNumbers 가 게임 시작하면 불변객체 생성
- 사용자로부터 입력값 받을 때마다 MyNumbers 객체 생성
- MyNumbers 객체와 ComputerNumbers 객체의 컬렉션 멤버변수 비교하여 그 결과값으로 Info 객체 생성
- Info 객체에는 뭐가 같은 자리에 위치하는지, 뭐가 다른 자리에 위치 하는지에 대한 정보 들어있음
- Info 로 적절히 메시지 출력
그럼 InputView 호출은 언제?
InputView 는 사용자가 입력할 때마다
어떻게 넘길까, 어떤 형태로 넘길까, 어떻게 넘겨야 OutputView 에서 잘 쓸까
Info VO 만들어서 int Strike, Ball, boolean Nothing 이런 식으로 넘겨보자
VO가 정확히 뭔데?
DTO와 무슨 차이인데?
Info 객체는 적절히 출력값 만들기
경우 1 : 동일한 숫자가 같은 위치에 존재 -> “?스트라이크”
경우 2 : 동일한 숫자가 다른 위치에 존재 -> “?볼”
경우 3 : 경우1 + 경우2 -> “?볼 ?스트라이크”
경우 4 : 동일한 숫자가 존재하지 않음 -> “낫싱”
컨트롤러는 무엇을 해야할까
ComputerNumbers 무작위 수 생성
while true
InputView 사용자 입력 메서드
InputView 사용자 입력값과 무작위 수 비교 -> Info 객체 반환
만약 Info 객체의 strike 가 3 이라면 break
아니라면 OutputView(Info) 넣어서 출력값
while true 쓰긴 싫은데 일단 해보고 리팩해보자
도메인은 MyNumbers, ComputerNumbers, Info
뷰는 InputView, OutputView
컨트롤러 GameController
[10/21 14:10] 생각 종료, 미션 시작!
[10/21 17:41] 3시간 정도 개발 후 생각
로직 하나하나 자세히 들어가보니 해야할게 많다
Enum 써보고 싶다
SIZE 를 3으로 제한하는 걸 공통으로 할 수 없나?
모든 클래스에서 상수를 정의하는건 너무 냄새난다
아 인터페이스 세워서 상수 정해놓고 그대로 상속받아서 쓰게 해보자
된다!
근데 문제가 있는데
Numbers 인터페이스를 ClientNumbers, ComputerNumbers 가 상속받는건 말이 되지만
Game 이 상속받는건 좀 이상한데...
하나의 인터페이스가 너무 범용적으로 쓰이고 있어 ISP 위반?
상수 하나 정도인데... 괜찮지 않을까
의문점1
// if, else if 를 쓰면
// 3개의 숫자를 모두 맞히셨습니다! 게임 종료
// 위 문장이 출력이 안되고 while 무한반복
public void proceedGame() {
while(true) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
ClientNumbers clientNumbers = InputView.makeClientNumbers();
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
int ball = info.ball;
int strike = info.strike;
boolean ongoing = info.ongoing;
System.out.println(ball + " " + strike + " " + ongoing);
if(ball > 0 && strike > 0) {
OutputView.printBallAndStrike(ball, strike); // ?볼 ?스트라이크
} else if (ball > 0) {
OutputView.printBall(ball); // ?볼
} else if (strike > 0) {
OutputView.printStrike(strike); // ?스트라이크
} else if (ball == 0 && strike == 0) {
OutputView.printNothing(); // 낫싱
} else if (strike == 3) {
OutputView.printStrike(strike); // 3스트라이크
OutputView.printSuccess(); // 3개의 숫자를 모두 맞히셨습니다! 게임 종료
break;
}
}
}
// if 를 쓰면 정상작동함
public void proceedGame() {
boolean ongoing = true;
while(ongoing) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
ClientNumbers clientNumbers = InputView.makeClientNumbers();
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
int ball = info.ball;
int strike = info.strike;
boolean ongoing2 = info.ongoing;
System.out.println(ball + " " + strike + " " + ongoing2);
if(ball > 0 && strike > 0) {
OutputView.printBallAndStrike(ball, strike); // ?볼 ?스트라이크
}
if (ball > 0) {
OutputView.printBall(ball); // ?볼
}
if (strike > 0) {
OutputView.printStrike(strike); // ?스트라이크
}
if (ball == 0 && strike == 0) {
OutputView.printNothing(); // 낫싱
}
if (strike == 3) {
ongoing = info.ongoing;
OutputView.printStrike(strike); // 3스트라이크
OutputView.printSuccess(); // 3개의 숫자를 모두 맞히셨습니다! 게임 종료
}
}
}
왜지????????
ball, strike, ongoing 지역변수는 모두 잘 초기화되는데 왜 while 무한반복이지?
아 else if (strike > 0) {...} 에서 이미 걸려서 while 문으로 돌아가기 때문에 else if (strike == 3)까지 도달을 못하네!!!!!!!!!!!!!!!
[10/21 21:30] 1일차 마무리
너무 정신없이 코딩하다보니 README 업데이트도 안하고
로직 고치는 데에만 빠져버렸다... 정신차리자
내일은 테케 하나 실패한 거 고치는거 해보자!
DAY2
[10/22 10:30] 2일차 시작! 테케가 왜 틀렸을까
@Test
void 게임종료_후_재시작() {
assertRandomNumberInRangeTest(
() -> {
run("246", "135", "1", "597", "589", "2");
assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
},
1, 3, 5, 5, 8, 9
);
}
Index: 0, Size: 0
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.base/java.util.LinkedList.checkElementIndex(LinkedList.java:559)
at java.base/java.util.LinkedList.get(LinkedList.java:480)
at baseball.domain.ClientNumbers.compareWithComputerNumbers(ClientNumbers.java:26)
at baseball.domain.Game.proceedGame(Game.java:21)
at baseball.controller.GameController.start(GameController.java:10)
at baseball.Application.main(Application.java:8)
at baseball.ApplicationTest.runMain(ApplicationTest.java:33)
at camp.nextstep.edu.missionutils.test.NsTest.run(NsTest.java:37)
at baseball.ApplicationTest.lambda$게임종료_후_재시작$0(ApplicationTest.java:16)
at camp.nextstep.edu.missionutils.test.Assertions.lambda$assertRandomTest$4(Assertions.java:89)
at org.junit.jupiter.api.AssertTimeout.lambda$assertTimeoutPreemptively$2(AssertTimeout.java:102)
at org.junit.jupiter.api.AssertTimeout.lambda$assertTimeoutPreemptively$4(AssertTimeout.java:138)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
public Info compareWithComputerNumbers(ComputerNumbers computerNumbers) {
Info info = new Info();
for(int i=0; i<SIZE; i++) {
for(int j=0; j<SIZE; j++) {
int cl = numbers.get(i);
int co = computerNumbers.getNumbers().get(j); // line26
if(cl == co && i != j) {
info.ball++;
}
if(cl == co && i == j) {
info.strike++;
}
}
}
if(info.strike == 3) {
info.ongoing = false;
}
return info;
}
아 프린트 다 찍어보니 틀린거 찾았다!
public static ClientNumbers makeClientNumbers() {
String inputStr = Console.readLine();
validateInputNotNumber(inputStr);
validateInputNumberNotUnique(inputStr);
validateInputSizeNot3(inputStr);
int input = Integer.parseInt(inputStr);
ClientNumbers clientNumbers = new ClientNumbers();
for(int i=CLIENT_NUMBERS_SIZE-1; i>=0; i--) {
clientNumbers.getNumbers().add(input % 10);
input /= 10;
}
System.out.println("clientNumbers : " + clientNumbers.getNumbers());
return clientNumbers;
}
add() 는 리스트 앞에서부터 순차적으로 요소를 채우는거였다
input % 10 으로 일의 자리, 십의 자리, 백의 자리를 얻는 방식으로 인덱싱해서 뒤에서부터 넣으려했는데
애초에 add() 는 앞에서부터 요소를 집어넣는거였다!
어떻게 이런 실수를...
public static ClientNumbers makeClientNumbers() {
String inputStr = Console.readLine();
validateInputNotNumber(inputStr);
validateInputNumberNotUnique(inputStr);
validateInputSizeNot3(inputStr);
char[] charArray = inputStr.toCharArray();
ClientNumbers clientNumbers = new ClientNumbers();
for(char c : charArray) {
clientNumbers.getNumbers().add(Integer.parseInt(String.valueOf(c)));
}
return clientNumbers;
}
이렇게 바꿔야 함!
아니 그래도 똑같이 테스트 실패네???
또 로그를 보자
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test FAILED
numbers in ComputerNumbers() : []
숫자 야구 게임을 시작합니다.
numbers in proceedGame() : []
숫자를 입력해주세요 : size in compareWithComputerNumbers() : 0
Index: 0, Size: 0
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.base/java.util.LinkedList.checkElementIndex(LinkedList.java:559)
at java.base/java.util.LinkedList.get(LinkedList.java:480)
at baseball.domain.ClientNumbers.compareWithComputerNumbers(ClientNumbers.java:29)
at baseball.domain.Game.proceedGame(Game.java:28)
at baseball.controller.GameController.start(GameController.java:9)
at baseball.Application.main(Application.java:8)
at baseball.ApplicationTest.runMain(ApplicationTest.java:33)
at camp.nextstep.edu.missionutils.test.NsTest.run(NsTest.java:37)
at baseball.ApplicationTest.lambda$게임종료_후_재시작$0(ApplicationTest.java:16)
at camp.nextstep.edu.missionutils.test.Assertions.lambda$assertRandomTest$4(Assertions.java:89)
at org.junit.jupiter.api.AssertTimeout.lambda$assertTimeoutPreemptively$2(AssertTimeout.java:102)
at org.junit.jupiter.api.AssertTimeout.lambda$assertTimeoutPreemptively$4(AssertTimeout.java:138)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
ComputerNumbers 가 왜 초기화가 안되지?
분명 생성자에 랜덤한 숫자 3개로 리스트 만들게 해놧는데?
지금 호출 순서가
게임종료_후_재시작()
run()
runMain()
main()
start()
proceedGame()
compareWithComputerNumbers()
get()
checkElementIndex()
분명 proceedGame() 로직에는 ComputerNumbers 인스턴스 먼저 만들도록 되어있는데?
main() 실행해서 게임은 잘 돌아가는데 왜 테스트에서 탈락하지?
설마 main 메서드 실행 직후에 ComputerNumbers 인스턴스 만들어야 하나?
[10/22 12:20] 찾았다... 기능 요구사항을 잘 읽자...
기능 요구사항에서 Randoms.pickNumberInRange() 사용하라고 했는데
왜 맘대로 Randoms.pickUniqueNumbersInRange() 쓰는거야!!!
아마 pickNumberInRange() 메서드를 기준으로 테케가 만들어진 것 같다
그래서 size, index 모두 초기화되지 않은 걸로 인식되지 않았을까? (개인적인 생각일뿐 오피셜은 아님)
public class ComputerNumbers implements Numbers {
private final List<Integer> numbers;
ComputerNumbers() {
numbers = Randoms.pickUniqueNumbersInRange(1, 9, SIZE);
}
...
}
를 다음과 같이 수정
public class ComputerNumbers implements Numbers {
private final List<Integer> numbers;
public ComputerNumbers() {
numbers = new ArrayList<>(SIZE);
while(numbers.size() < 3) {
int randomNumber = Randoms.pickNumberInRange(1, 9);
if(!numbers.contains(randomNumber)) {
numbers.add(randomNumber);
}
}
}
...
}
[10/22 12:30] 모든 테케 통과 후 생각 정리
어떤 것을 클래스로 만들어야 하는지
클래스를 만들더라도 인스턴스를 만들기 위한 클래스인지, 단지 유틸 메서드를 제공하기 위한 클래스인지를 고민해야 했다.
private, static, final, getter, 생성자의 사용이 어색했다.
validation 은 inputView 에서 해야할까? ClientNumbers 에서 해야할까? 아니면 두 곳 모두에서 해야할까?
일급컬렉션는 제대로 알고 쓰는걸까? 잠깐 공부하고 오자!
근데 일급객체는 뭐지? 뭔 차이지?
final 은 불변이라 하지만
정확히 말하자면 재할당 금지이다
즉, add() 등의 메서드로 컬렉션 내부의 요소를 변경가능하다는 것이다
일급컬렉션은
컬렉션 타입의 멤버변수만을 가지고 있으며
생성자에서 그 유일한 컬렉션 타입 멤버변수를 초기화하는 클래스이다.
만약 해당 클래스에서
멤버변수가 private 이고
생성자가 public 이고
getter, setter 가 구현되지 않았다면 해당 컬렉션 타입의 멤버변수는 초기화된 이후에는 절대 불변할 수 있다.
만약에 생성되는 시점에만 일급컬렉션를 건들고 그 이후에는 절대 접근할 수 없게 하려면
생성자 내부에서 객체 생성, validation 등의 로직을 다 담아야 한다
일급컬렉션의 장점은 무엇이 있을까?
1. 일급컬렉션 절대불변 보장할 수 있음
2. 일급컬렉션를 생성자에 로직을 담는다면 서비스 로직에서 로직을 제거할 수 있다.
3. 일급컬렉션와 관련된 로직들을 해당 클래스에서 관리할 수 있다. (Separation of Concern 또는 SRP)
4. 일급컬렉션의 멤버변수에 이름을 붙여서 직관적으로 변수를 인지할 수 있다
일급컬렉션를 쓰기 전에 생각할 것은 무엇일까?
1. 생성자에서 일급컬렉션의 컬렉션 멤버변수를 add() 등의 메서드로 초기화한 이후에 절대 건들지 않을 것을 확신할 수 있는지
[10/21 13:30] 리팩 시작
InputView 에서 ClientNumbers 일급컬렉션를 만드는 것을 구현하는 것은 좋지 않다
일급컬렉션 만드는 것은 ClientNumbers 생성자에 구현하는 게 SRP 를 준수할 수 있기 때문이다?
일단 해보자
오 코드가 깔끔해졌다!!!
일단 InputView 코드가 SRP 를 지킬 수 있게 된 것 같다!
package baseball.view;
import baseball.domain.ClientNumbers;
import baseball.exception.InputNeitherRestartNorExit;
import baseball.exception.InputNotNumber;
import baseball.exception.InputNumberNotUnique;
import baseball.exception.InputSizeNot3;
import camp.nextstep.edu.missionutils.Console;
public class InputView {
private static final String INPUT_MSG = "숫자를 입력해주세요 : ";
private static final int CLIENT_NUMBERS_SIZE = 3;
public static void printInputMsg() {
System.out.print(INPUT_MSG);
}
public static ClientNumbers makeClientNumbers() {
String inputStr = Console.readLine();
validateInputNotNumber(inputStr);
validateInputNumberNotUnique(inputStr);
validateInputSizeNot3(inputStr);
char[] charArray = inputStr.toCharArray();
ClientNumbers clientNumbers = new ClientNumbers();
for(char c : charArray) {
clientNumbers.getNumbers().add(Integer.parseInt(String.valueOf(c)));
}
return clientNumbers;
}
// 사용자가 모든 숫자를 맞춘 이후에 검증해야 함
public static void validateInputNeitherRestartNorExit(String inputStr) {
if (!inputStr.equals("1") || !inputStr.equals("2")) {
throw new InputNeitherRestartNorExit();
}
}
public static void validateInputNotNumber(String inputStr) {
char[] charArray = inputStr.toCharArray();
for(char c : charArray) {
if(!Character.isDigit(c)) {
throw new InputNotNumber();
}
}
}
public static void validateInputNumberNotUnique(String inputStr) {
char[] charArray = inputStr.toCharArray();
for(int i=1; i<CLIENT_NUMBERS_SIZE; i++) {
char firstChar = charArray[0];
if(firstChar == charArray[i]) {
throw new InputNumberNotUnique();
}
}
}
public static void validateInputSizeNot3(String inputStr) {
if (inputStr.length() != CLIENT_NUMBERS_SIZE) {
throw new InputSizeNot3();
}
}
}
위 코드에서 아래 코드로 리팩토링!
package baseball.view;
import baseball.domain.ClientNumbers;
import camp.nextstep.edu.missionutils.Console;
public class InputView {
private static final String INPUT_MSG = "숫자를 입력해주세요 : ";
public static void printInputMsg() {
System.out.print(INPUT_MSG);
}
public static ClientNumbers makeClientNumbers() {
String inputStr = Console.readLine();
ClientNumbers clientNumbers = new ClientNumbers(inputStr);
return clientNumbers;
}
}
package baseball.domain;
import baseball.exception.InputNotNumber;
import baseball.exception.InputNumberNotUnique;
import baseball.exception.InputSizeNot3;
import java.util.ArrayList;
import java.util.List;
public class ClientNumbers implements Numbers {
private final List<Integer> numbers = new ArrayList<>(SIZE);
public ClientNumbers(String inputStr) {
validateInputNotNumber(inputStr);
validateInputNumberNotUnique(inputStr);
validateInputSizeNot3(inputStr);
char[] charArray = inputStr.toCharArray();
for(char c : charArray) {
numbers.add(Integer.parseInt(String.valueOf(c)));
}
}
... 생략
}
물론 ClientNumbers 클래스로 많은 로직들이 넘어왔지만
validation 은 결국 ClientNumbers 일급컬렉션를 만들 때 거쳐야 하는 로직이기 때문에 ClientNumbers 로 넘어오는게 맞는 것 같다
validation 은 따로 패키지로 빼면 더 깔끔해질 수도 있다!
근데 ClientNumbers() 생성자에 문자열을 넣어야 ClientNumbers 일급컬렉션을 만들 수 있다는 것은 직관적으로 알기 어렵지 않나?
정적팩토리 메서드를 써야 하나? 나중에 리팩해보자!
[10/21 14:14] Info 도메인 리팩
Info 클래스는 일급컬렉션는 아니지만 은닉되지 않는다
이것도 리팩토링해보자!
멤버변수를 private 으로 선언하고 getter, setter 를 구현 -> 은닉
package baseball.domain;
public class Info {
public int ball = 0;
public int strike = 0;
public boolean ongoing = true;
}
위 코드에서 아래 코드로 리팩토링!
getter, setter 를 구현하는 것이 조금 번거롭긴 하지만
조금이라도 캡슐화, 은닉성을 향상시킬 수 있으니 해야한다!
package baseball.domain;
public class Info {
private int ball = 0;
private int strike = 0;
private boolean ongoing = true;
public int getBall() {
return ball;
}
public int getStrike() {
return strike;
}
public boolean getOngoing() {
return ongoing;
}
public void setBall(int ball) {
this.ball = ball;
}
public void setStrike(int strike) {
this.strike = strike;
}
public void setOngoing(boolean ongoing) {
this.ongoing = ongoing;
}
}
[10/21 14:20] Game 도메인 리팩
Game 도메인의 proceedGame() 내부에는 ClientNumbers 와 ComputerNumbers 를 비교하여 Info 인스턴스를 만들고
그 Info 인스턴스로 경우에 따른 메시지를 출력하는 로직이 구현되어 있다
Info 인스턴스로 메시지를 출력하는 건 Info 도메인으로 옮기는게 맞을 것 같다
public void proceedGame() {
ComputerNumbers computerNumbers = new ComputerNumbers();
boolean ongoing = true;
while(ongoing) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
ClientNumbers clientNumbers = InputView.makeClientNumbers();
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers); // error
int ball = info.ball;
int strike = info.strike;
if (ball > 0 && strike > 0) {
OutputView.printBallAndStrikeMsg(ball, strike); // ?볼 ?스트라이크
} else if (ball > 0) {
OutputView.printBallMsg(ball); // ?볼
} else if (strike > 0) {
OutputView.printStrikeMsg(strike); // ?스트라이크
} else if (ball == 0 && strike == 0) {
OutputView.printNothingMsg(); // 낫싱
}
if (strike == 3) {
ongoing = info.ongoing;
OutputView.printSuccessMsg(); // 3개의 숫자를 모두 맞히셨습니다! 게임 종료
}
}
}
위 코드에서 아래 코드로 리팩토링!
public void proceedGame() {
ComputerNumbers computerNumbers = new ComputerNumbers();
boolean ongoing = true;
while(ongoing) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
ClientNumbers clientNumbers = InputView.makeClientNumbers();
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers); // error
ongoing = info.printMsgByUsingInfo();
}
}
Info 도메인 내부에서 로직을 구현하니깐 getBall(), getStrike(), getOngoing() 을 구현하지 않아도 된다!
package baseball.domain;
import baseball.view.OutputView;
public class Info {
private int ball = 0;
private int strike = 0;
private boolean ongoing = true;
public void setBall(int ball) {
this.ball = ball;
}
public void setStrike(int strike) {
this.strike = strike;
}
public void setOngoing(boolean ongoing) {
this.ongoing = ongoing;
}
public boolean printMsgByUsingInfo() {
if (ball > 0 && strike > 0) {
OutputView.printBallAndStrikeMsg(ball, strike); // ?볼 ?스트라이크
} else if (ball > 0) {
OutputView.printBallMsg(ball); // ?볼
} else if (strike > 0) {
OutputView.printStrikeMsg(strike); // ?스트라이크
} else if (ball == 0 && strike == 0) {
OutputView.printNothingMsg(); // 낫싱
}
if (strike == 3) {
ongoing = false;
OutputView.printSuccessMsg(); // 3개의 숫자를 모두 맞히셨습니다! 게임 종료
return ongoing;
}
return true;
}
}
DAY 3
[10/23 11:52] 스캐너 테스트코드는 어떻게 자동화하지?
테스트코드는 자동화해야 하는데
InputView 클래스의 makeClientNumbers() 정적 메서드는 내부에서 사용자로부터 입력값을 받는 로직이 존재하기 때문에
테스트코드 자동화가 불가능해
Console.readLine() 을 생성자 외부로 빼고
문자열을 인자로 받도록 해보자!
추가적으로 InputView 는 객체 생성이 목적인 클래스가 아니라
유틸 클래스 목적이기 때문에 private 생성자를 만들어서 객체 생성을 금지하자!
기억하자! 테스트코드 자동화를 위해 메서드 내부에는 사용자 입력을 받는 메서드가 없어야 한다!
(사실 Scanner 생성자의 파라미터로 문자열을 넣어서 입력값을 임의로 지정할 수 있지만 우테코에서 Scanner 대신 제공한 Console 클래스의 메서드들은 인자를 받는 메서드가 없기 때문에 그런 것도 있다!)
package baseball.view;
import baseball.domain.ClientNumbers;
import camp.nextstep.edu.missionutils.Console;
public class InputView {
private static final String INPUT_MSG = "숫자를 입력해주세요 : ";
public static void printInputMsg() {
System.out.print(INPUT_MSG);
}
public static ClientNumbers makeClientNumbers() {
String inputStr = Console.readLine();
ClientNumbers clientNumbers = new ClientNumbers(inputStr);
return clientNumbers;
}
}
위 코드에서 아래 코드로 변경!
package baseball.view;
import baseball.domain.ClientNumbers;
public class InputView {
private static final String INPUT_MSG = "숫자를 입력해주세요 : ";
private InputView() {}
public static void printInputMsg() {
System.out.print(INPUT_MSG);
}
public static ClientNumbers makeClientNumbers(String inputStr) {
ClientNumbers clientNumbers = new ClientNumbers(inputStr);
return clientNumbers;
}
}
[10/23 12:11] 생각해보니 코드의 중복이 있다
makeClientNumbers() 메서드는 사실 ClientNumbers() 생성자를 호출하여 객체를 생성하고 반환하는 로직 뿐이다
그럼 makeClientNumbers() 메서드를 지우고 그냥 ClientNumbers() 생성자를 쓰면 더 심플해진다!
public void proceedGame() {
ComputerNumbers computerNumbers = new ComputerNumbers();
boolean ongoing = true;
while(ongoing) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
ClientNumbers clientNumbers = InputView.makeClientNumbers(Console.readLine());
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
ongoing = info.printMsgByUsingInfo();
}
}
위 코드에서 아래 코드로 변경!
public void proceedGame() {
ComputerNumbers computerNumbers = new ComputerNumbers();
boolean ongoing = true;
while(ongoing) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
String inputStr = Console.readLine();
ClientNumbers clientNumbers = new ClientNumbers(inputStr);
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
ongoing = info.printMsgByUsingInfo();
}
}
[10/23 14:31] equals() 와 hashcode() 를 오버라이딩해야 하는 이유가 생겼는데...
compareWithComputerNumbers() 로직은 다음과 같다.
clientNumbers 객체와 ComputerNumbers 객체의 리스트의 각 요소를 돌면서 ball, strike 개수를 센다
만약 동일하다면 strike 는 3이 될 것이다
그 정보로 Info 객체의 strike 멤버변수를 set 한다.
더 이상 비교를 진행할 필요도 없으니 ongoing 도 false 로 set 한다.
즉 clientNumbers 객체와 ComputerNumbers 객체의 값이 동일하다면 Info 의 strike 는 3, ongoing 은 false 값을 가져야 한다는 것이다.
현재 Info 는 getter 를 만들지 않았다
지금까지 쓸 일이 없었기 때문이다.
테스트 코드를 짤 때 getter 를 구현해야 할지 고민이 이제 들었다.
Info 클래스에서 getter 를 구현해서 strike, ongoing 멤버변수를 가져와 비교하는 것이 맞을까?
아니면
Info 클래스의 equals, hashcode 를 재정의하여 동등성 비교하는 것이 맞을까?
사실 둘 다 generate 로 만들면 되기 때문에 어떤 방법을 사용하든 상관없을 것 같긴한데
그건 내 편의만을 생각한 것이고...
아름다운 코드를 위해서는 어떻게 해야할까?
[10/23 14:34] 생성자 구현의 번거로움은 getter 구현의 단점보다 작다?
내 생각은 equals, hashcode 를 오버라이딩하여 동등성 비교하는 것이 더 좋을 것 같다
왜냐하면
1. getter 로 캡슐화를 깨뜨린다고 생각했기 때문이다.
2. 코드의 양을 조금 줄일 수 있을 것이라 생각했기 때문이다.
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
Assertions.assertThat(info.getStrike()).isEqualTo(3);
Assertions.assertThat(info.getOngoing()).isEqualTo(false);
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
Info sameInfo = new Info(0, 3, false); // 생성자 수정 및 구현해야 함
Assertions.assertThat(info.equals(sameInfo)).
근데 막상 또 코드를 짜보니 생성자를 또 구현해야 한다.
캡슐화를 위해 추가될 코드가 많아진다는 것이다. 그래도 캡슐화를 위해서 하는게 맞을 것 같다.
나중에 equals, hashcode 오버라이딩의 장점을 발견할 수 있겠지?
일단 Info 생성자를 만들고
public Info(int ball, int strike, boolean ongoing) {
this.ball = ball;
this.strike = strike;
this.ongoing = ongoing;
}
이를 기반으로 테스트 코드를 짤 수 있었다!
@Test
void clientNumbers_와_computerNumbers_전혀_다른_경우() {
// computerNumbers 와 전혀 다른 clientNumbers 만들기
ComputerNumbers computerNumbers = new ComputerNumbers();
List<Integer> list1to9 = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9));
List<Integer> numbers = computerNumbers.getNumbers();
list1to9.removeIf(i -> numbers.contains(i));
StringBuilder sb = new StringBuilder();
list1to9.forEach(i -> sb.append(i));
ClientNumbers clientNumbers = new ClientNumbers(sb.substring(0, 3).toString());
// 서로 동일한 clientNumbers 와 computerNumbers 를 비교하여 info 만들기
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
// 예상하는 info 만들기
Info infoExpected = new Info(0, 0, true);
// 동등성 비교
Assertions.assertThat(info.equals(infoExpected)).isTrue();
}
@Test
void clientNumbers_와_computerNumbers_일부_같은_경우() {
// computerNumbers 와 1개가 불일치한 clientNumbers 만들기
ComputerNumbers computerNumbers = new ComputerNumbers();
List<Integer> list1to9 = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9));
List<Integer> numbers = computerNumbers.getNumbers();
list1to9.removeIf(i -> numbers.contains(i));
StringBuilder sb = new StringBuilder();
numbers.forEach(i -> sb.append(i));
String differentString = sb.toString().replace(String.valueOf(numbers.get(0)), String.valueOf(list1to9.get(0)));
ClientNumbers clientNumbers = new ClientNumbers(differentString);
// 서로 동일한 clientNumbers 와 computerNumbers 를 비교하여 info 만들기
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
// 예상하는 info 만들기
Info infoExpected = new Info(0, 2, true);
// 동등성 비교
Assertions.assertThat(info.equals(infoExpected)).isTrue();
}
@Test
void clientNumbers_와_computerNumbers_완벽히_같은_경우() {
// computerNumbers 와 동일한 clientNumbers 만들기
ComputerNumbers computerNumbers = new ComputerNumbers();
List<Integer> numbers = computerNumbers.getNumbers();
StringBuilder sb = new StringBuilder();
numbers.forEach(i -> sb.append(i));
String sameString = sb.toString();
ClientNumbers clientNumbers = new ClientNumbers(sameString);
// 서로 동일한 clientNumbers 와 computerNumbers 를 비교하여 info 만들기
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
// 예상하는 info 만들기
Info infoExpected = new Info(0, 3, false);
// 동등성 비교
Assertions.assertThat(info.equals(infoExpected)).isTrue();
}
DAY 4
[10/24 11:52] 도메인은 뷰를 알아서는 안된다! MVC 의 M자도 모르는구나!
MVC를 생각해보니 도메인에서 뷰의 로직을 가져다 쓰면 안된다는 규칙이 있었다.
현재 Game 도메인에서 뷰의 로직들이 들어가있었다...
Game 도메인을 삭제하고 그 코드들을 GameController 로 옮기기만 했다!
package baseball.controller;
import baseball.domain.ClientNumbers;
import baseball.domain.ComputerNumbers;
import baseball.domain.Info;
import baseball.exception.InputNeitherRestartNorExit;
import baseball.view.InputView;
import baseball.view.OutputView;
import camp.nextstep.edu.missionutils.Console;
public class GameController {
public void startGame() {
OutputView.printStartMsg(); // 숫자 야구 게임을 시작합니다.
}
public void proceedGame() {
ComputerNumbers computerNumbers = new ComputerNumbers();
boolean ongoing = true;
while(ongoing) {
InputView.printInputMsg(); // 숫자를 입력해주세요 :
String inputStr = Console.readLine();
ClientNumbers clientNumbers = new ClientNumbers(inputStr);
Info info = clientNumbers.compareWithComputerNumbers(computerNumbers);
ongoing = info.printMsgByUsingInfo();
}
}
public void restartOrExitGame() {
boolean oneMore = true;
while(oneMore) {
OutputView.printRestartOrExitMsg(); // 게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
int inputInt = Integer.parseInt(Console.readLine());
// validateInputNeitherRestartNorExit(inputInt);
if(inputInt == 1) {
proceedGame();
} else {
oneMore = false;
}
}
}
// 우테코에서 제공한 테스트에는 성공하지 못함
public static void validateInputNeitherRestartNorExit(int inputInt) {
if (inputInt != 1 || inputInt != 2) {
throw new InputNeitherRestartNorExit();
}
}
}
[10/24 15:43] pr 올리기 전 마지막 점검!
잔실수가 보였다
1. 클래스 내부에서만 사용할 validation 메서드들이 public 으로 되어있었다 -> private 으로 변경
2. 1주차 과제에서 필수가 아닌 validation 메서드를 GameController 로부터 지웠다. -> 근데 만약 validation 이 존재한다 하더라도 validation 메서드가 컨트롤러에 있는게 MVC 는 아닌 것 같은데... 만약 존재한다면 어디에 존재해야 할까? 도메인을 새로 만들어서 그 곳에서 사용해야 할까? 아니면 InputView 에서 사용해야 할까?
3. 사용되지 않는 import 제거
[제출 후 소감]
README 에 구현하고자 하는 기능들을 먼저 작성하라는 것이 의아했다
일단 부딫혀봐야 뭘 구현해야할지 알 수 있지 않나? 라고 생각했기 때문이다.
한시간 정도 무엇을 클래스로 만들지, 클래스를 만들더라도 객체생성을 위한 목적의 클래스인지, 그저 유틸클래스로 사용하기 위한 클래스인지를 구분했고, 객체간의 관계를 어떻게 설정할지, 객체간의 데이터 전달은 어떻게 할 것인지, 이 생각들을 어떤 식으로 구현할지 등 생각했다.
먼저 구상을 하니 개발이 꼬이지 않았다. 물론 애초에 잘못 생각한, 비효율적으로 생각한 것들을 나중에 일부 수정하긴 해야 했지만 생각하지 못했던 것, 보지 못했던 것을 경험하는 느낌이 들었다.
개발할 때에는 최대한 학교에서 배운 것, 개인적으로 공부한 것들을 적용하려 노력했다.
접근제어자, 상속, 일급컬렉션, 디자인패턴 등을 적용하려 했다. 머리 속으로 알고 있지만 막상 이것저것 적용해보려 하니 어려웠다.
적용에만 힘을 쏟은 탓에 자잘한 실수도 있었다.
어려운 기술을 쓰는 것에만 매몰되는 것이 아니라 내 생각을 분명하게 보여줄 수 있는 코드를 짜려고 노력했다.
무난하고 대단할 것 없는 코드이지만, 아마 처음으로 온전한 내 생각이 들어간 코드를 짠 것 같아 행복했다.
우테코가 왜 프리코스만으로도 성장할 것이라고 말한 것인지 이해가 되었다.
앞으로 남은 프리코스도 열심히 행복하게 해보자!