이전 포스팅에서 초기 구상과 테스트 통과까지를 다뤘고, 이번에는 목표한 바에 맞춰 리팩토링을 해보려고 한다! 미션에 들어가면서 세웠던 목표는 다음과 같다.
- 의존성 주입 원칙을 적용해보자 : 외부의 모듈이 필요한 경우 직접 생성하거나 불러오는 대신, 외부에서 생성한 후 주입하게끔 만들자.
- 과하게 분리하지 말자 : 재사용되지 않는 로직을 분리해서 밖으로 빼지 말자. 분리는 또 다른 의존을 만들어낼 수 있다.
- 한 함수가 하나의 기능만 하게 만들자 : SRP를 준수하자!
async readNamesInput() {
const input = await Console.readLineAsync(GAME_MESSAGES.carNameQuery);
const names = input.split(SYMBOLS.comma);
names.forEach((item) => validateCarName(item));
return names;
},
//App.js
async play() {
const inputView = new InputView();
const outputView = new OutputView();
const racingGame = new RacingGame();
const racingGameController = new RacingGameController(inputView, outputView, racingGame);
racingGame.initiate();
//RacingGameController.js
constructor(inputView, outputView, racingGame) {
this.#inputView = inputView;
this.#outputView = outputView;
this.#racingGame = racingGame
}
class InputValidator {
validateCarNames(names) {
for (const name of names) {
this.#validateNameLength(name);
this.#validateBlankName(name);
}
this.#validateNoDuplicateNames(names);
}
#validateNameLength(name) {
...
}
#validateBlankName(name) {
...
}
#validateNoDuplicateNames(names) {
...
}
validateRoundsNumber(roundsNumber) {
this.#validateIsNumber(roundsNumber);
this.#validateNonNegativeValue(roundsNumber);
}
#validateIsNumber(value) {
...
}
#validateNonNegativeValue(value) {
...
}
//InputView.js
class InputView {
#validator;
constructor(validator){
this.#validator = validator;
}
//App.js
async play() {
const inputValidator = new InputValidator();
const inputView = new InputView(inputValidator);
...
}
이렇게, 외부의 모듈을 직접 불러오는 대신 외부에서 주입받아 사용하도록 바꿔주었다!
//RacingGameController.js
async getCarNames() {
const names = await this.#inputView.readCarNamesInput();
const cars = names.map(item => new RacingCar(item));
this.#racingGame.setCars(cars);
}
//RacingCarFactory.js
class RacingCarFactory {
createRacingCar(name) {
return new RacingCar(name);
}
}
//App.js
async play(){
const racingCarFactory = new RacingCarFactory();
const racingGameController = new RacingGameController(
inputView,
outputView,
racingGame,
racingCarFactory
);
}
//RacingGameController.js
const cars = names.map(item => this.#factory.createRacingCar(item));
this.#racingGame.setCars(cars);
class CustomError extends Error {
constructor(message) {
super(`[ERROR] ${message}`);
this.name = this.constructor.name;
}
}
//InputValidator.js
if (name.trim().length === 0) {
throw new CustomError(ERROR_MESSAGES.carName.blank);
}
moveAllCars() {
this.#cars.forEach((car) => car.move());
return this.#cars.map((car) => car.getMoveResult()).join(`\n`) + '\n';
}
printResult(result) {
Console.print(result + '\n');
}
${name} : ---
) 이런 식으로 바꿔줬다. 이렇게 되면 게임에서 최대 위치값을 구할 때 length를 일일히 붙여서 확인하지 않아도 되고, 결과 문자열을 name과 합칠 필요 없이 그대로 return하면 된다.-constants.js에서 쿼리 메세지(이름과 시도 횟수를 물어보는 메세지)에 줄바꿈을 포함시켜두었는데, 이게 어색하다고 생각해서 View에서 출력할 때 \n을 추가하도록 바꿨다.
validateCarNames(names) {
this.#validateNameLength(names);
this.#validateBlankNames(names);
this.#validateUniqueNames(names);
}
#validateNameLength(names) {
for (let name of names) {
if (name.length > GAME_NUMBERS.carNameMaxLength) {
throw new CustomError(ERROR_MESSAGES.carName.invalidLength);
}
}
}
#validateBlankNames(names) {
for (let name of names) {
if (name.trim().length === 0) {
throw new CustomError(ERROR_MESSAGES.carName.blank);
}
}
}
validateCarNames(names) {
for (let name of names) {
this.#validateNameLength(name);
this.#validateBlankName(name);
}
this.#validateNoDuplicateNames(names);
}
async initiate() {
const { carNames, rounds } = await this.#getUserInputs();
this.#setupCarsFromNames(carNames);
this.#executeRacingRounds(rounds);
this.#racingGame.concludeGame();
this.#displayResult();
}
async #getUserInputs() {
const carNames = await inputView.readNamesInput();
const rounds = await inputView.readRoundsNumberInput();
return { carNames, rounds };
}
#generateRandomRacingGameNumber() {
return Random.pickNumberInRange(
GAME_NUMBERS.rangeMin,
GAME_NUMBERS.rangeMax,
);
}
#canMove(randomNumber) {
return randomNumber >= GAME_NUMBERS.movementThreshold;
}
move() {
const randomNumber = this.#generateRandomRacingGameNumber();
if (this.#canMove(randomNumber)) {
this.#position += 1;
this.#moveResult += SYMBOLS.moveIndicator;
}
동작, 의도를 직관적으로 나타내는 이름을 고민하며 여러 변수명/함수명을 수정했다.
게임 결과를 출력한다기보단 게임 우승자를 출력하는 역할이 더 정확하니까
for (let round = 0; round < roundsNumber; round += 1) {
this.#racingGame.moveAllCars();
const roundResult = this.#racingGame.getAllCarsMovementHistory();
this.#outputView.printRoundResult(roundResult);
}
반복자도 변수니까! 한 번의 반복이 한 라운드라는 걸 명시했다!
'시작한다'는 포괄적인 의미보다, '레이싱 게임을 실행하는' 동작을 잘 드러내게 바꿨다!
'이동 결과'보다는 '이동 기록'이 더 정확한 표현이겠다 싶었다. 근데 movementHistory보다는 movementRecord가 더 나을 것 같기도...
레이싱카 인스턴스 만드는거니까...
printResult라고 하면 '전체 결과를 출력'하는 느낌이 더 강한 것 같아서, '한 라운드의 결과를 출력'하는 의미가 잘 드러나도록 바꿨다.
const ERROR_MESSAGES = {
carName: {
invalidLength: `자동차 이름은 ${GAME_NUMBERS.carNameMaxLength} 이하만 가능합니다.`,
duplicate: '자동차 이름은 중복될 수 없습니다.',
blank: '자동차 이름이 공백입니다.',
},
roundsNumber: {
negativeValue: '시도 횟수는 음수일 수 없습니다.',
notNumber: '시도 횟수는 숫자만 허용됩니다.',
},
};
에러 메세지 상수 객체를 구체화(carName, roundsNumber로 묶었다.
const SYMBOLS = {
comma: ',',
winnerNameSeparator: ', ',
dash: '-',
playerResultSeparator: ' : ',
};
상수의 이름이 형태만 설명하고 있어서, 쓰임새를 명확하게 드러내도록 바꿨다