diff --git a/src/__tests__/Car.test.js b/src/__tests__/Car.test.js deleted file mode 100644 index a49ae21..0000000 --- a/src/__tests__/Car.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { Car } from "../domain/index.js"; - -describe("Car 클래스 테스트", () => { - test.each([ - { value: 1 }, - { value: true }, - { value: undefined }, - { value: null }, - { value: {} }, - { value: [] }, - ])( - "자동차의 이름으로 문자열이 아닌 값($value)을 할당하면 오류가 발생한다.", - ({ value }) => { - expect(() => new Car(value)).toThrowError(); - } - ); - - test("자동차의 이름으로 문자열을 할당하면 오류가 발생하지 않는다.", () => { - expect(() => new Car("car")).not.toThrowError(); - }); - - test("자동차 이름이 5자 초과이면 오류가 발생한다.", () => { - expect(() => new Car("123456")).toThrowError(); - }); - - test("자동차 이름이 1자 미만이면 오류가 발생한다.", () => { - expect(() => new Car("")).toThrowError(); - }); -}); diff --git a/src/__tests__/Race.test.js b/src/__tests__/Race.test.js index 6474a61..1778a65 100644 --- a/src/__tests__/Race.test.js +++ b/src/__tests__/Race.test.js @@ -1,15 +1,7 @@ import { describe, expect, test } from "vitest"; -import { Car, Race } from "../domain/index.js"; +import { Racer, Race } from "../domain/index.js"; describe("Race 클래스 테스트", () => { - test("레이서가 Racer 클래스의 자식 클래스가 아니면 오류가 발생한다.", () => { - expect(() => new Race({}, 1)).toThrowError(); - }); - - test("레이서가 Racer 클래스의 자식 클래스면 오류가 발생하지 않는다.", () => { - expect(() => new Race(Car, 1)).not.toThrowError(); - }); - test.each([ { value: "1" }, { value: true }, @@ -20,21 +12,21 @@ describe("Race 클래스 테스트", () => { ])( "레이스 횟수로 숫자가 아닌 값($value)을 할당하면 오류가 발생한다.", ({ value }) => { - expect(() => new Race(Car, value)).toThrowError(); + expect(() => new Race(value)).toThrowError(); } ); test("레이스 횟수로 숫자를 할당하면 오류가 발생하지 않는다.", () => { - expect(() => new Race(Car, 1)).not.toThrowError(); + expect(() => new Race(1)).not.toThrowError(); }); test("레이스 횟수가 0이하면 오류가 발생한다.", () => { - expect(() => new Race(Car, 0)).toThrowError(); - expect(() => new Race(Car, -1)).toThrowError(); + expect(() => new Race(0)).toThrowError(); + expect(() => new Race(-1)).toThrowError(); }); test("레이스 횟수가 1이상이면 오류가 발생하지 않는다.", () => { - expect(() => new Race(Car, 1)).not.toThrowError(); + expect(() => new Race(1)).not.toThrowError(); }); test.each([ @@ -45,67 +37,124 @@ describe("Race 클래스 테스트", () => { { value: null }, { value: {} }, ])( - "레이스 준비시 배열이 아닌 값($value)을 할당하면 오류가 발생한다.", + "레이스 시작시 배열이 아닌 값($value)을 할당하면 오류가 발생한다.", ({ value }) => { - const race = createCarRaceWith5Laps(); + const race = createRaceWith5Laps(); - expect(() => race.ready(value)).toThrowError( - "경기 준비에 적합하지 않은 입력값입니다." + expect(() => race.start(value)).toThrowError( + "레이스 시작에 적합하지 않은 입력값입니다." ); } ); - test("레이스 준비시 배열을 할당하면 오류가 발생하지 않는다.", () => { - const race = createCarRaceWith5Laps(); - - expect(() => race.ready(["1", "2"])).not.toThrowError(); - }); + test("레이스 시작의 입력값이 0개 이하면 오류가 발생한다.", () => { + const race = createRaceWith5Laps(); - test("레이스 준비의 입력값이 0개 이하면 오류가 발생한다.", () => { - const race = createCarRaceWith5Laps(); - - expect(() => race.ready([])).toThrowError( - "경기를 준비하기엔 레이서가 부족합니다." + expect(() => race.start([])).toThrowError( + "레이스를 시작하기엔 레이서가 부족합니다." ); }); - test("레이스 준비의 입력값이 1개 이상이면 오류가 발생하지 않는다.", () => { - const race = createCarRaceWith5Laps(); + test("레이스 시작의 입력값이 레이서들이면 오류가 발생하지 않는다.", () => { + const race = createRaceWith5Laps(); - expect(() => race.ready(["1"])).not.toThrowError(); + expect(() => race.start([new Racer("1")])).not.toThrowError(); }); test("레이스 시작전에 기록을 가져오면 빈배열입니다.", () => { - const race = createCarRaceWith5Laps(); - race.ready(["1", "2"]); + const race = createRaceWith5Laps(); expect(race.records).toEqual([]); }); test("레이스 시작후에 기록을 알 수 있습니다.", () => { - const race = createCarRaceWith5Laps(); - race.ready(["1", "2"]); - race.start(); + const race = createRaceWith5Laps(); + race.start([new Racer("1"), new Racer("2")]); expect(race.records).toHaveLength(5); }); test("레이스 시작전에 우승자를 가져오면 빈배열입니다.", () => { - const race = createCarRaceWith5Laps(); - race.ready(["1", "2"]); + const race = createRaceWith5Laps(); expect(race.winners).toEqual([]); }); - test("레이스 우승자는 1이상입니다..", () => { - const race = createCarRaceWith5Laps(); - race.ready(["1", "2"]); - race.start(); + test("레이스 우승자는 1이상입니다.", () => { + const race = createRaceWith5Laps(); + race.start([new Racer("1"), new Racer("2")]); expect(race.winners.length).toBeGreaterThanOrEqual(1); }); + + test.each([ + { value: 1 }, + { value: "str" }, + { value: true }, + { value: null }, + { value: {} }, + ])( + "레이스 규칙에 적합하지 않은 입력값($value)이면 오류가 발생합니다.", + ({ value }) => { + const race = createRaceWith5Laps(); + + expect(() => + race.start([new Racer("1"), new Racer("2")], value) + ).toThrowError("레이스 규칙에 적합하지 않은 입력값입니다."); + } + ); + + test("레이스 규칙이 1개미만이면 오류가 발생한다.", () => { + const race = createRaceWith5Laps(); + + expect(() => race.start([new Racer("1"), new Racer("2")], [])).toThrowError( + "레이스를 시작하기엔 규칙이 부족합니다." + ); + }); + + test.each([ + { value: 1 }, + { value: "str" }, + { value: true }, + { value: undefined }, + { value: null }, + { value: [] }, + { value: {} }, + ])( + "레이스 규칙이 함수가 아닌 값($value)이면 오류가 발생한다.", + ({ value }) => { + const race = createRaceWith5Laps(); + + expect(() => + race.start([new Racer("1"), new Racer("2")], [value]) + ).toThrowError("레이스 규칙은 함수여야 합니다."); + } + ); + + test.each([ + { fn: () => 1 }, + { fn: () => "str" }, + { fn: () => undefined }, + { fn: () => null }, + { fn: () => [] }, + { fn: () => {} }, + ])("레이스 규칙의 반환값이 불린값이 아니면 오류가 발생한다.", ({ fn }) => { + const race = createRaceWith5Laps(); + + expect(() => + race.start([new Racer("1"), new Racer("2")], [fn]) + ).toThrowError("레이스 규칙의 반환값으로 적합하지 않습니다."); + }); + + test("레이스 규칙이 불린값을 반환하는 함수이면 오류가 발생하지 않는다.", () => { + const race = createRaceWith5Laps(); + + expect(() => + race.start([new Racer("1"), new Racer("2")], [() => true]) + ).not.toThrowError(); + }); }); -function createCarRaceWith5Laps() { - return new Race(Car, 5); +function createRaceWith5Laps() { + return new Race(5); } diff --git a/src/__tests__/RaceEntry.test.js b/src/__tests__/RaceEntry.test.js new file mode 100644 index 0000000..bdc72d6 --- /dev/null +++ b/src/__tests__/RaceEntry.test.js @@ -0,0 +1,68 @@ +import { describe, expect, test, vi } from "vitest"; +import { Race, RaceEntry } from "../domain/index.js"; +import { inputManager } from "../service/index.js"; + +vi.mock("../service/index.js"); + +function retryScanMock(inputValue) { + inputManager.retryScan.mockImplementationOnce(async (_, processFn) => { + return processFn(inputValue); + }); +} + +describe("RaceEntry 클래스 테스트", () => { + test("올바른 유형을 선택하지 않으면 오류가 발생한다.", async () => { + retryScanMock("5"); + + const register = new RaceEntry(); + + await expect(register.selectEntityType()).rejects.toThrowError( + "올바른 유형의 번호가 아닙니다." + ); + }); + + test("올바른 유형을 선택하면 오류가 발생하지 않는다.", async () => { + retryScanMock("1"); + + const register = new RaceEntry(); + + await expect(register.selectEntityType()).resolves.not.toThrowError(); + }); + + test("레이스 횟수로 정수가 아닌 값을 입력하면 오류가 발생한다.", async () => { + retryScanMock("a"); + + const register = new RaceEntry(); + + await expect(register.setRaceLaps()).rejects.toThrowError( + "시도할 횟수로 정수를 입력해야 합니다." + ); + }); + + test("레이스 횟수로 0이하를 입력하면 오류가 발생한다.", async () => { + retryScanMock("0"); + + const register = new RaceEntry(); + + await expect(register.setRaceLaps()).rejects.toThrowError( + "시도할 횟수는 1이상이어야 합니다." + ); + }); + + test("레이스 횟수로 1이상을 입력하면 오류가 발생하지 않는다.", async () => { + retryScanMock("1"); + + const register = new RaceEntry(); + + await expect(register.setRaceLaps()).resolves.not.toThrowError(); + }); + + test("레이스 횟수를 설정하면 Race 클래스 인스턴스를 반환받는다.", async () => { + retryScanMock("1"); + const register = new RaceEntry(); + + const race = await register.setRaceLaps(); + + expect(race).toEqual(new Race(1)); + }); +}); diff --git a/src/__tests__/RaceScoreboard.test.js b/src/__tests__/RaceScoreboard.test.js index bad7733..1c3a4df 100644 --- a/src/__tests__/RaceScoreboard.test.js +++ b/src/__tests__/RaceScoreboard.test.js @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { Car, Race, RaceScoreboard } from "../domain/index.js"; +import { Race, RaceScoreboard } from "../domain/index.js"; describe("RaceScoreboard 클래스 테스트", () => { test("Race 클래스의 인스턴스를 할당하지 않으면 오류가 발생한다.", () => { @@ -10,9 +10,9 @@ describe("RaceScoreboard 클래스 테스트", () => { expect(() => new RaceScoreboard(person)).toThrowError(); }); - test("Race 클래스의 인스턴스를 할당하면 오류가 발생한다.", () => { - const carRace = new Race(Car, 5); + test("Race 클래스의 인스턴스를 할당하면 오류가 발생하지 않는다.", () => { + const race = new Race(5); - expect(() => new RaceScoreboard(carRace)).not.toThrowError(); + expect(() => new RaceScoreboard(race)).not.toThrowError(); }); }); diff --git a/src/__tests__/Racer.test.js b/src/__tests__/Racer.test.js index e26f2c4..29f5857 100644 --- a/src/__tests__/Racer.test.js +++ b/src/__tests__/Racer.test.js @@ -1,27 +1,55 @@ -import { beforeEach, describe, expect, test } from "vitest"; +import { describe, expect, test } from "vitest"; import { Racer } from "../domain/index.js"; describe("Racer 클래스 테스트", () => { - let racer; - - beforeEach(() => { - racer = new Racer(" jeong "); - }); - - test("레이서의 이름 앞뒤 공백은 제거된다.", () => { - expect(racer.name).toHaveLength(5); - }); - test("레이서에 이름을 지어줄 수 있다.", () => { - expect(racer.name).toBe("jeong"); + const racer = new Racer("tom"); + + expect(racer.name).toBe("tom"); }); test("레이서는 움직일 수 있다.", () => { + const racer = createRacerWithName(); + racer.move(); expect(racer.position).toBe(1); }); + test.each([ + { value: 1.1 }, + { value: "1" }, + { value: true }, + { value: null }, + { value: {} }, + ])( + "레이서의 이동 거리로 정수가 아닌 값($value)을 할당하면 오류가 발생한다.", + ({ value }) => { + const racer = createRacerWithName(); + + expect(() => racer.move(value)).toThrowError(); + } + ); + + test.each([2, 3, 4, 5])( + "레이서의 이동 거리(%i)를 지정할 수 있다.", + (distance) => { + const racer = createRacerWithName(); + + racer.move(distance); + + expect(racer.position).toBe(distance); + } + ); + + test("레이서의 위치는 0미만이 될 수 없다.", () => { + const racer = createRacerWithName(); + + racer.move(-1); + + expect(racer.position).toBe(0); + }); + test("레이서의 이름을 수정하려고 하면 오류가 발생한다.", () => { const racer = createRacerWithName(); diff --git a/src/__tests__/RacerRegistry.test.js b/src/__tests__/RacerRegistry.test.js deleted file mode 100644 index 9606f09..0000000 --- a/src/__tests__/RacerRegistry.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { RacerRegistry } from "../domain/index.js"; - -describe("RacerRegistry 클래스 테스트", () => { - test.each([ - { value: 1 }, - { value: true }, - { value: undefined }, - { value: null }, - { value: {} }, - { value: [] }, - ])( - "개체 유형으로 문자열이 아닌($value)을 할당하면 오류가 발생한다.", - ({ value }) => { - expect(() => new RacerRegistry(value, ",")).toThrowError(); - } - ); - - test("개체 유형으로 문자열을 할당하면 오류가 발생하지 않는다.", () => { - expect(() => new RacerRegistry("사람", ",")).not.toThrowError(); - }); - - test("개체 유형이 1자 미만이면 오류가 발생한다.", () => { - expect(() => new RacerRegistry("", ",")).toThrowError(); - }); - - test("개체 유형이 1자 이상이면 오류가 발생하지 않는다.", () => { - expect(() => new RacerRegistry("차", ",")).not.toThrowError(); - }); - - test.each([ - { value: 1 }, - { value: true }, - { value: undefined }, - { value: null }, - { value: {} }, - { value: [] }, - ])( - "분리 문자로 문자열이 아닌($value)을 할당하면 오류가 발생한다.", - ({ value }) => { - expect(() => new RacerRegistry("사람", value)).toThrowError(); - } - ); - - test("분리 문자로 문자열을 할당하면 오류가 발생하지 않는다.", () => { - expect(() => new RacerRegistry("사람", ",")).not.toThrowError(); - }); - - test("분리 문자가 1자 미만이면 오류가 발생한다.", () => { - expect(() => new RacerRegistry("사람", "")).toThrowError(); - }); - - test("분리 문자가 1자 이상이면 오류가 발생하지 않는다.", () => { - expect(() => new RacerRegistry("사람", "-")).not.toThrowError(); - }); -}); diff --git a/src/__tests__/utils.test.js b/src/__tests__/utils.test.js index 74fbcb0..16b7010 100644 --- a/src/__tests__/utils.test.js +++ b/src/__tests__/utils.test.js @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { deepCopy, getRandomNumber, isSubclass } from "../utils/index.js"; +import { deepCopy, getRandomNumber } from "../utils/index.js"; describe("getRandomNumber 함수 테스트", () => { test.each([ @@ -66,24 +66,6 @@ describe("getRandomNumber 함수 테스트", () => { }); }); -describe("isSubclass 함수 테스트", () => { - test("자식 클래스면 true를 반환한다.", () => { - class Parent {} - - class Child extends Parent {} - - expect(isSubclass(Child, Parent)).toBe(true); - }); - - test("자식 클래스가 아니면 false를 반환한다.", () => { - class Cat {} - - class Dog {} - - expect(isSubclass(Cat, Dog)).toBe(false); - }); -}); - describe("deepCopy 함수 테스트", () => { test("Map을 인수로 할당하면 오류가 발생한다.", () => { expect(() => deepCopy(new Map())).toThrowError(); diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..af79359 --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1 @@ +export * from "./racer.js"; diff --git a/src/constants/racer.js b/src/constants/racer.js new file mode 100644 index 0000000..59d4ff6 --- /dev/null +++ b/src/constants/racer.js @@ -0,0 +1,6 @@ +export const RACER_ENTITY_TYPES = { + 1: "자동차", + 2: "사람", + 3: "자전거", + 4: "강아지", +}; diff --git a/src/domain/Car.js b/src/domain/Car.js deleted file mode 100644 index 5e80f0d..0000000 --- a/src/domain/Car.js +++ /dev/null @@ -1,27 +0,0 @@ -import Racer from "./Racer.js"; - -class Car extends Racer { - static #MAX_NAME_LENGTH = 5; - - constructor(name) { - super(name); - - Car.#validateName(this.name); - } - - static #validateName(name) { - if (typeof name !== "string") { - throw new Error("자동차 이름은 문자열이어야 합니다."); - } - - if (Car.#MAX_NAME_LENGTH < name.length) { - throw new Error("자동차 이름은 5자 이하여야 합니다."); - } - - if (name.length < 1) { - throw new Error("자동차 이름은 1자 이상이어야 합니다."); - } - } -} - -export default Car; diff --git a/src/domain/Race.js b/src/domain/Race.js index 26af3f6..fb56042 100644 --- a/src/domain/Race.js +++ b/src/domain/Race.js @@ -1,54 +1,58 @@ -import { deepCopy, getRandomNumber, isSubclass } from "../utils/index.js"; +import { deepCopy, getRandomNumber, validate } from "../utils/index.js"; import Racer from "./Racer.js"; class Race { - #Racer; + static #DEFAULT_RULES = [() => 4 <= getRandomNumber(0, 9)]; #laps; #racers; #records; - constructor(Racer, laps) { - Race.#validateRacer(Racer); + constructor(laps) { Race.#validateLaps(laps); - this.#Racer = Racer; this.#laps = laps; this.#racers = []; this.#records = []; } - ready(racerNameList) { - Race.#validateRacerNameList(racerNameList); + start(racers, rules = Race.#DEFAULT_RULES) { + Race.#validateRacers(racers); + Race.#validateRules(rules); - this.#addRacers(racerNameList); - } + this.#addRacers(racers); - start() { - Array.from({ length: this.#laps }).forEach(() => { - this.#progressRace(); - }); + this.#progressRace(rules); } #addRacer(racer) { + Race.#validateRacer(racer); + this.#racers.push(racer); } - #addRacers(racerNameList) { - racerNameList.forEach((name) => { - this.#addRacer(new this.#Racer(name)); + #addRacers(racers) { + racers.forEach((racer) => { + this.#addRacer(racer); }); } - #progressRace() { + #progressRacePerLap(rule) { let recordPerLap = []; this.#racers.forEach((racer) => { - Race.#movementStrategy(racer); + Race.#moveByRule(racer, rule); recordPerLap.push({ name: racer.name, position: racer.position }); }); this.#records.push(recordPerLap); } + #progressRace(rules) { + Array.from({ length: this.#laps }).forEach(() => { + const rule = rules.length === 1 ? rules[0] : rule.shift(); + this.#progressRacePerLap(rule); + }); + } + get records() { const copiedRecords = deepCopy(this.#records); @@ -67,36 +71,40 @@ class Race { return finalLapRecord.filter((racer) => racer.position === maxPosition); } - static #movementStrategy(racer) { - const number = getRandomNumber(0, 9); - - if (4 <= number) racer.move(); + static #moveByRule(racer, rule) { + if (rule()) racer.move(); } static #validateRacer(racer) { - if (!isSubclass(racer, Racer)) { - throw new Error("레이서가 Racer 클래스를 자식 클래스가 아닙니다."); - } + validate.instance(racer, Racer, "레이스에 적합하지 않은 레이서입니다."); } static #validateLaps(laps) { - if (typeof laps !== "number") { - throw new Error("레이스 횟수는 숫자여야 합니다."); - } + validate.integer(laps, "레이스 횟수는 숫자여야 합니다."); + validate.lessThan(laps, 1, "레이스 횟수는 1이상이어야 합니다."); + } - if (laps < 1) { - throw new Error("레이스 횟수는 1이상이어야 합니다."); - } + static #validateRacers(racers) { + validate.array(racers, "레이스 시작에 적합하지 않은 입력값입니다."); + validate.lessThan( + racers.length, + 1, + "레이스를 시작하기엔 레이서가 부족합니다." + ); } - static #validateRacerNameList(racerNameList) { - if (!Array.isArray(racerNameList)) { - throw new Error("경기 준비에 적합하지 않은 입력값입니다."); - } + static #validateRules(rules) { + validate.array(rules, "레이스 규칙에 적합하지 않은 입력값입니다."); + validate.lessThan( + rules.length, + 1, + "레이스를 시작하기엔 규칙이 부족합니다." + ); - if (racerNameList.length < 1) { - throw new Error("경기를 준비하기엔 레이서가 부족합니다."); - } + rules.forEach((rule) => { + validate.function(rule, "레이스 규칙은 함수여야 합니다."); + validate.boolean(rule(), "레이스 규칙의 반환값으로 적합하지 않습니다."); + }); } } diff --git a/src/domain/RaceEntry.js b/src/domain/RaceEntry.js new file mode 100644 index 0000000..48593ae --- /dev/null +++ b/src/domain/RaceEntry.js @@ -0,0 +1,74 @@ +import { RACER_ENTITY_TYPES } from "../constants/index.js"; +import { inputManager } from "../service/index.js"; +import { validate } from "../utils/index.js"; +import Race from "./Race.js"; +import Racer from "./Racer.js"; + +class RaceEntry { + static #SEPARATOR = ","; + #entityType; + + async selectEntityType() { + const typeNumber = await inputManager.retryScan( + `원하시는 레이서의 유형을 선택해서 번호를 입력해주세요.\n${RaceEntry.#stringifyRacerEntityTypes()}\n`, + (inputValue) => { + RaceEntry.#validateTypeNumber(inputValue); + + return inputValue; + } + ); + + this.#entityType = RACER_ENTITY_TYPES[typeNumber]; + } + + async registerRacers() { + const racers = await inputManager.retryScan( + `경주할 ${this.#entityType} 이름을 입력하세요(이름은 쉼표(${ + RaceEntry.#SEPARATOR + })를 기준으로 구분).\n`, + (inputValue) => { + const racerNameList = inputValue.split(RaceEntry.#SEPARATOR); + + return racerNameList.map((name) => new Racer(name.trim())); + } + ); + + return racers; + } + + async setRaceLaps() { + const laps = await inputManager.retryScan( + "시도할 횟수는 몇회인가요?\n", + (inputValue) => { + const numValue = Number(inputValue); + + RaceEntry.#validateRaceLaps(numValue); + + return numValue; + } + ); + + return new Race(laps); + } + + static #stringifyRacerEntityTypes() { + return Object.entries(RACER_ENTITY_TYPES) + .map(([number, type]) => `${number}. ${type}`) + .join("\n"); + } + + static #validateTypeNumber(typeNumber) { + validate.property( + typeNumber, + RACER_ENTITY_TYPES, + "올바른 유형의 번호가 아닙니다." + ); + } + + static #validateRaceLaps(numValue) { + validate.integer(numValue, "시도할 횟수로 정수를 입력해야 합니다."); + validate.lessThan(numValue, 1, "시도할 횟수는 1이상이어야 합니다."); + } +} + +export default RaceEntry; diff --git a/src/domain/RaceScoreboard.js b/src/domain/RaceScoreboard.js index be7793e..846c7db 100644 --- a/src/domain/RaceScoreboard.js +++ b/src/domain/RaceScoreboard.js @@ -1,4 +1,5 @@ import { outputManager } from "../service/index.js"; +import { validate } from "../utils/index.js"; import Race from "./Race.js"; class RaceScoreboard { @@ -32,9 +33,7 @@ class RaceScoreboard { } static #validateRace(race) { - if (!(race instanceof Race)) { - throw new Error("Race 클래스의 인스턴스가 아닙니다."); - } + validate.instance(race, Race, "Race 클래스의 인스턴스가 아닙니다."); } } diff --git a/src/domain/Racer.js b/src/domain/Racer.js index 7e9d54e..8bdfb76 100644 --- a/src/domain/Racer.js +++ b/src/domain/Racer.js @@ -1,10 +1,13 @@ +import { validate } from "../utils/index.js"; + class Racer { + static #MAX_NAME_LENGTH = 5; #name; #position; constructor(name) { - const trimedName = name.trim(); - this.#name = trimedName; + Racer.#validateName(name); + this.#name = name; this.#position = 0; } @@ -16,8 +19,26 @@ class Racer { return this.#position; } - move() { - this.#position++; + move(distance = 1) { + Racer.#validateDistance(distance); + + const movedPosition = this.#position + distance; + + this.#position = movedPosition < 0 ? 0 : movedPosition; + } + + static #validateName(name) { + validate.string(name, "레이서 이름은 문자열이어야 합니다."); + validate.lessThan( + Racer.#MAX_NAME_LENGTH, + name.length, + "레이서 이름은 5자 이하여야 합니다." + ); + validate.lessThan(name.length, 1, "레이서 이름은 1자 이상이어야 합니다."); + } + + static #validateDistance(distance) { + validate.integer(distance, "이동 거리는 정수여야 합니다."); } } diff --git a/src/domain/RacerRegistry.js b/src/domain/RacerRegistry.js deleted file mode 100644 index f94cd4f..0000000 --- a/src/domain/RacerRegistry.js +++ /dev/null @@ -1,46 +0,0 @@ -import { inputManager } from "../service/index.js"; - -class RacerRegistry { - #entityType; - #separator; - - constructor(entityType, separator) { - RacerRegistry.#validateEntityType(entityType); - RacerRegistry.#validateSeparator(separator); - this.#entityType = entityType; - this.#separator = separator; - } - - async register() { - const inputValue = await inputManager.scan( - `경주할 ${this.#entityType} 이름을 입력하세요(이름은 ${ - this.#separator - }를 기준으로 구분).\n` - ); - const racerNameList = inputValue.split(this.#separator); - - return racerNameList; - } - - static #validateEntityType(entityType) { - if (typeof entityType !== "string") { - throw new Error("개체 유형은 문자열이어야 합니다."); - } - - if (entityType.length < 1) { - throw new Error("개체 유형은 1자 이상이어야 합니다."); - } - } - - static #validateSeparator(separator) { - if (typeof separator !== "string") { - throw new Error("분리 문자는 문자열이어야 합니다."); - } - - if (separator.length < 1) { - throw new Error("분리 문자는 1자 이상이어야 합니다."); - } - } -} - -export default RacerRegistry; diff --git a/src/domain/index.js b/src/domain/index.js index 589e826..86757e6 100644 --- a/src/domain/index.js +++ b/src/domain/index.js @@ -1,5 +1,4 @@ -export { default as Car } from "./Car.js"; export { default as Race } from "./Race.js"; export { default as Racer } from "./Racer.js"; export { default as RaceScoreboard } from "./RaceScoreboard.js"; -export { default as RacerRegistry } from "./RacerRegistry.js"; +export { default as RaceEntry } from "./RaceEntry.js"; diff --git a/src/main.js b/src/main.js index c6ed75b..d1f7376 100644 --- a/src/main.js +++ b/src/main.js @@ -1,18 +1,15 @@ -import { Car, Race, RaceScoreboard, RacerRegistry } from "./domain/index.js"; +import { RaceEntry, RaceScoreboard } from "./domain/index.js"; async function main() { - const carRacerRegistry = new RacerRegistry("자동차", ","); - const racerNameList = await carRacerRegistry.register(); - - const carRace = new Race(Car, 5); - - carRace.ready(racerNameList); - carRace.start(); - - const carRaceScoreboard = new RaceScoreboard(carRace); - - carRaceScoreboard.displayRecords(); - carRaceScoreboard.displayWinners(); + const raceEntry = new RaceEntry(); + await raceEntry.selectEntityType(); + const racers = await raceEntry.registerRacers(); + const race = await raceEntry.setRaceLaps(); + race.start(racers); + + const raceScoreboard = new RaceScoreboard(race); + raceScoreboard.displayRecords(); + raceScoreboard.displayWinners(); } main(); diff --git a/src/service/inputManager.js b/src/service/inputManager.js index d236e7b..72bfc22 100644 --- a/src/service/inputManager.js +++ b/src/service/inputManager.js @@ -10,7 +10,20 @@ class InputManager { async scan(query) { const inputValue = await this.#inputFn(query); - return inputValue; + return inputValue.trim(); + } + + async retryScan(query, processFn) { + try { + const inputValue = await this.scan(query); + + return processFn ? processFn(inputValue) : inputValue; + } catch (error) { + return await this.retryScan( + `${error.message} 다시 입력해주세요.\n`, + processFn + ); + } } } diff --git a/src/utils/index.js b/src/utils/index.js index 7606c58..d3ee8b1 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,4 +1,4 @@ export { default as getRandomNumber } from "./getRandomNumber.js"; -export { default as isSubclass } from "./isSubclass.js"; export { default as deepCopy } from "./deepCopy.js"; export { default as readLineAsync } from "./readLineAsync.js"; +export { default as validate } from "./validate.js"; diff --git a/src/utils/isSubclass.js b/src/utils/isSubclass.js deleted file mode 100644 index 7ab50b8..0000000 --- a/src/utils/isSubclass.js +++ /dev/null @@ -1,15 +0,0 @@ -function isSubclass(child, parent) { - let prototype = Object.getPrototypeOf(child); - - while (prototype) { - if (prototype === parent) { - return true; - } - - prototype = Object.getPrototypeOf(prototype); - } - - return false; -} - -export default isSubclass; diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..743d8e0 --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,48 @@ +class Validator { + static throwErrorWithCondition(condition, errorMessage) { + if (condition) { + throw new Error(errorMessage); + } + } + + static type(value, typeValue, errorMessage) { + Validator.throwErrorWithCondition(typeof value !== typeValue, errorMessage); + } + + static string(value, errorMessage) { + Validator.type(value, "string", errorMessage); + } + + static function(value, errorMessage) { + Validator.type(value, "function", errorMessage); + } + + static boolean(value, errorMessage) { + Validator.type(value, "boolean", errorMessage); + } + + static integer(value, errorMessage) { + Validator.throwErrorWithCondition(!Number.isInteger(value), errorMessage); + } + + static array(value, errorMessage) { + Validator.throwErrorWithCondition(!Array.isArray(value), errorMessage); + } + + static lessThan(value, otherValue, errorMessage) { + Validator.throwErrorWithCondition(value < otherValue, errorMessage); + } + + static instance(value, classValue, errorMessage) { + Validator.throwErrorWithCondition( + !(value instanceof classValue), + errorMessage + ); + } + + static property(value, objectValue, errorMessage) { + Validator.throwErrorWithCondition(!(value in objectValue), errorMessage); + } +} + +export default Validator;