컨트롤러에서
readNumbers, processNumbers, readCommand, processCommand로 분리되어있던걸
readAndProcessNumbers처럼 합쳤는데
1. 입력을 받는거랑 입력값을 처리하는 건 다른 로직이니까 분리해야 한다
2. 입력받는거 그냥 inputView 메서드 호출하는거 꼴랑 하난데 분리해야하나?
A : 분리했다!
작은 책임이더라도, 앱이 확장된다면 (미션 내에서 그럴 일은 없지만) 책임이 커질 수 있고, 그 때 가서 분리하려고 하면 문제가 발생하기 쉽다! 한 함수가 하나의 책임만을 갖게끔 작게 쪼개둬야 확장이 용이해지고 읽기도 좋아진다.
베이스볼게임에서 calculateBallStrikeScore => 입력값을 배열로 바꾸고 / 정답값 배열과 입력값 배열을 비교해서 점수 배열을 반환하는데
뒷부분을 gameUtils.processNumbers(혹은 processNumbersScoreCalculation같은거)로 분리하고
기존의 calculateBallStrikeScore는 입력값을 배열로 바꾸고 정답값(this.#answer)이랑 같이 gameUtils.process어쩌고 함수로 넘겨주는 역할만 하게 할지
A : 이것도 분리했다!
const gameUtils = { //...다른 함수들 calculateBallStrikeScore(answer, pitches) { let ball = 0; let strike = 0; for (let i = 0; i < pitches.length; i += 1) { if (answer[i] === pitches[i]) { strike += 1; continue; } if (answer.includes(pitches[i])) { ball += 1; } } return [ball, strike]; } } //BaseballGame.js handleUserPitches(userInput) { const pitchedBallNumbers = userInput.split("").map(Number); const [ball, strike] = gameUtils.calculateBallStrikeScore( this.#answer, pitchedBallNumbers ); this.#updateGameStateAfterPitch(strike); return [ball, strike]; }
기존의 calculateBallStrikeScore 함수를 handleUserPitches(유저가 공을 던진 결과를 처리하니까)로 바꾸고, 계산하는 부분을 기존 이름으로 분리해서 gameUtils에 넣어줬다. 이제 handleUserPitches는 입력값을 쪼개서 숫자 배열로 만들고, 이를 계산 결과값으로 바꾸고, 게임 상태 업데이트 함수(3스트라이크면 게임 끝냄)를 실행시킨 후에, 다른 메서드에서 활용할 수 있도록 계산 결과값을 반환한다.
왜?
음...
뷰는 내부적으로만 사용하는 메서드도 없고 관리해야할 내부 상태도 없다
따라서 뷰를 클래스에서 객체 리터럴로 바꾼다
ㄴ 컨트롤러에서 뷰를 필드로 갖고있을 필요도 없어지니까 그냥 불러와서 쓴다
ㄴ 뷰에 전역에서 접근할 수 있다
ㄴ 캡슐화 위반 아니냐?
ㄴ 그렇게 따지면 로직 분리한다고 utils에 빼놓은 함수들도 내부에서만 사용하는 로직인데 전역에서 접근되니까 캡슐화 위반이게? 그럼 싹다 내부에 갖고 있어야 함? 그럼 분리 왜했음?
ㄴ오 할말 없는데?
새 게임을 시작할 때, 선언된 베이스볼게임 인스턴스 내에서 단순히 정답을 리셋하도록 되어있는데, 그 대신에 새로운 베이스볼게임 인스턴스를 생성하는 식으로 바꿔야 하는건지… 만약 그렇게 할 경우엔 const baseballGame = new BaseballGame()으로 똑같이 생성하면 당연히 const로 선언된 변수(baseballGame)를 재선언하는거니까 안될거같은데
while반복 내에서/이미 선언되지 않은 변수명으로 BaseballGame인스턴스를 계속 생성할 수 있나?/그렇다면 이전에 생성된 인스턴스 변수는 사라지는건지? 메모리 상에 계속 남아있는건지?
- 기존 인스턴스 리셋 (현재의 구현):
메모리 사용이 더 효율적일 수 있다. 새 객체를 계속 생성하는 것은 메모리 할당과 해제에 관련된 오버헤드를 초래할 수 있기 때문.(=새 객체 만들고 이전꺼 지우는데 리소스가 들어가니까 부담될 수 있단 소리)
하지만 이 경우 객체의 내부 상태를 잘 관리해야 한다. 재시작 시 모든 상태를 올바르게 초기화해줘야 제대로 작동할 것.- 새 인스턴스 생성:
장점: 코드의 복잡성이 감소할 수 있다. 각 게임은 새로운 상태에서 시작하므로 이전 게임의 상태에 대한 걱정이 없다.
단점: 메모리 관리에 대한 고려가 필요하다. 자주 객체를 생성하고 폐기하면 가비지 컬렉션에 부담을 줄 수 있다. 또한, 리팩토링된 현재는 재시작 로직이 BaseballGame에 들어있으므로, 이 로직을 분리해서 controller에서 관리해야 한다.- 게임을 재시작할 때 새로운 BaseballGame 인스턴스를 생성하고 싶으면 그냥 this.#baseballGame (this.#baseballGame = new BaseballGame()하면 된다. 선언되지 않은 변수명 그런거 필요 없고...(지금 보니 되게 바보같네) 만약 const baseballGame = new BaseballGame() 이런 식으로 생성했으면, 반복해서 새 인스턴스를 생성할 수 있게끔 하려면 그냥 let baseballGame = new BaseballGame()이렇게 해두면 나중에 재할당이 가능하다. const랑 let 차이 알지? 아직도 모르면 맞아야됨
- JavaScript에서 객체가 더 이상 참조되지 않으면, 가비지 컬렉터에 의해 메모리에서 자동으로 제거된다. 따라서 새 BaseballGame 인스턴스를 생성하고 이전 인스턴스를 더 이상 참조하지 않으면, 이전 인스턴스는 가비지 컬렉션 대상이 된다.
어제 디코 보다가 누가 'InputView는 야구게임이 아닌 다른 앱에서도 사용할 수 있을 정도로 분리되어있어야 한다'라고 하는 걸 봤는데 이게 가능한가? 만약 그렇게 한다면 어디에나 쓸 수 있을 정도의 범용적인 메서드 (지금 코드로 보면 readUserInput(message)정도로 분리해서)만 가지고 있어야 하는데, 그러면 InputView의 존재 의미가 없지 않나? 그냥 readLineAsync 직접 불러서 쓰는거랑 전혀 차이가 없는 거 같은데
만약 그렇게 한다면,
1. InputView 클래스를 확장해서 BaseballGameInputView를 만들어서 거기다가 readUserInputNumbers, readUserInputCommand(상속받은 readUserInput가져와서 message만 넘겨줌)메서드를 만들어서 쓰던가
2. BaseballGameController에서 직접 inputView.readUserInput을 불러와서 message 넘겨주는 식으로 하든가 (이러면 View가 분리되긴 한건가?)
(이렇게 되면 OutputView도 동일하게 분리한다고 할 때, 컨트롤러가 출력에 관한 메서드를 직접적으로 갖고있게 되니까 분리가 안되는 것 같은데… OutputView랑 InputView에 동일한 분리 로직을 적용해야한다고 하면 1번이 더 나을듯. 근데 그냥 무의미한 것 같음 이렇게 따지면 OutputView에는 Console.print밖에 안남음)
끙...
게임컨트롤러에 게임 로직이 너무 많은 거 아닌가? 게임 안으로 보내야되는거 아닌가?
=> 그러면 inputView, outputView의 로직을 어떻게 연결할건데?
=> 적어도 게임 상태 정도는 게임 안으로 넣을 수 있지 않을까? 그거만 게임으로 넘겨도 괜찮게 분리된 거 아님? 컨트롤러는 게임/인풋뷰/아웃풋뷰/입력 관리 메서드만 가지고 있고, 게임 내의 상태에 따라서 반복 돌리는 일만 하니까
분리했다! 게임 상태를 BaseballGame가 프라이빗 프로퍼티로 가지고 있게 하고, 조건에 따라 (3스트라이크, 재시작이나 종료 명령어 입력) 게임 상태를 변경하는 로직도 BaseballGame의 메서드로 옮겼다. 지면이 부족하므로 이에 대해서는 다음 포스팅에서 다룰 것!
진짜 짜잘하다
추가했다! 덤으로 예외 처리 로직도 세분화하고 순서도 보기 좋게 정리했다.
export const validationUtils = { validateNumbers(input) { const numbers = input.split(""); for (const validation of validations) { if (!validation.isValid(numbers)) { throw new Error(validation.error); } } }, validateCommand(command) { if (!isValidCommand(command)) { throw new Error(INVALID_COMMAND); } }, }; const validations = [ { isValid: isValidLength, error: INVALID_NUMBERS.LENGTH }, { isValid: areAllIntegers, error: INVALID_NUMBERS.INTEGER }, { isValid: areAllWithinRange, error: INVALID_NUMBERS.RANGE }, { isValid: hasNoDuplicateNumber, error: INVALID_NUMBERS.DUPLICATE }, ];
고민은 아니고 메모
왜 우테코 프리코스만 하면 평소에 프로젝트니 포폴이니 만들 때 안하던 고민을 왕창 하게 될까? 합격하고 싶어서도 있겠지만... 포폴이나 프로젝트처럼 실제로 써야되니까 뭔가 대단한 목표를 가지고 만들어야 하는 것을 만들 때는 일단 작동이 되게끔 구현하는 데 집중하느라 구조에 대한 고민에 시간을 쓰기 어려워지는 것 같다. 그에 비해 프리코스 미션은, 닿을 만한 목표를 두고 구현하기 때문에, 도착점에 도달하는 것 자체에서 더 나아가서 어떻게 해야 저기까지 멋지게 효율적으로 잘 도착할 수 있을지를 고민할 기회가 많아지는 느낌이다.
그리고 그런 고민이 즐겁기도 하고! 프로젝트를 하면서는 '비즈니스가 있어야 목적과 문제와 고민과 해결이 발생한다'고 회고하기는 했지만, 비즈니스(써먹을 곳)가 배제된 상황에서는 순수하게 기술적 고민에 몰두할 수 있으니 이것도 나름 괜찮은듯
나아가서 생각해보면, 비즈니스가 있는, 그러니까 프로젝트나 포폴같은 것도 닿을 만한 도착점부터 설정해두고 한다면 좀 더 쓸모있는 고민이나 발상이 가능하지 않을까? 그럼 시간이 더 들려나? 하지만 쓸모 있는 일에 시간 쓰는 걸 비효율이라고 할 수 있나? 모르겠다