Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Final practice #2387

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions docs/README.md

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커밋에 대한 리뷰입니다!
최종 코딩테스트에선 1 ~ 4 주차 모든 공통 피드백 및 기능 요구사항을 확인할 것 같습니다!
기능 단위 커밋도 시간이 오래걸리는 일이기 때문에, 이 점도 함께 연습하시면 시간 관리 연습에 도움이 되실 것 같습니다 😊

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요, 이번 구현에서 정말 아~무 생각 없이 구현을 시작하다 보니 놓쳤던 부분이라 저도 뼈저리게 아쉬운 부분입니다ㅠㅠ
감사합니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 자동차 경주 게임

## 어플리케이션 흐름

1. 자동차 이름 입력
1. 자동차 이름은 ,로 구분
- 입력이 `,`로 끝나는 경우 예외 발생
2. 각 이름은 5자 이하만 가능
3. 자동차 이름은 중복될 수 없다.
2. 이동 시도 횟수 입력
1. 숫자 입력시 예외 발생
2. 0, 또는 음수 입력시 예외 발생
3. 각 라운드 별 이동 시도 후 결과 출력
4. 최종 우승자 출력

## 어플리케이션 구성

### domain

#### \[Car]

#### Car

이름과 위치 정보를 가진다.

- 기능
- 숫자를 입력받아 이동 가능하다면 위치 정보를 업데이트 후 이동 여부를 반환한다.

#### Cars

자동차 객체들을 관리한다.

- 기능
- 숫자를 생성해 각 자동차 객체를 이동시킨다.
- 자동차들의 진행 결과를 받아온다.
- 우승자의 이름을 반환한다.

#### \[numberGenerator]

#### MoveNumber

이동에 사용할 숫자를 래핑한다.

#### NumberGenerator

- 기능
- 숫자 하나를 반환한다.

#### RandomNumberGenerator

- 기능
- 0~9 사이에서 랜덤으로 숫자를 생성해 반환한다.

### view

#### InputView

- 기능
- 자동차 이름 입력
- `,`를 기준으로 나눠서 List<String> 반환
- 시도할 횟수 입력
- 시도 횟수가 0 이하라면 예외 발생

#### OutputView

- 기능
- 라운드 결과 출력
- 최종 우승자 출력

## 고민

자동차의 위치 정보는 어느 곳에서 저장하는 것이 가장 자연스러울까?
-> 출력에서도 Car 객체를 그대로 넘기자. 그렇기에 자동차의 위치 정보 또한 자동차에서 관리하자
우승자 비교는 어떤 식으로 구현하는 것이 가장 자연스러울까?
-> 자동차들의 경주 결과 비교는 자동차의 책임이 아닌, 게임 스태프의 책임으로 생각했음
45 changes: 45 additions & 0 deletions docs/최종코테_준비.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 최종 코테를 대비하기 위해 어떤 준비가 가능할까

- 필요없는 고민을 줄이기
- 어떤 메서드에만 `static`을 사용할 지 기준 미리 정하기
- DTO를 사용할지, getter를 사용할지 미리 정하기
- 나는 getter를 사용하기로 결정
- 검증 로직을 Validator로 분리할지, 도메인 내부에서 모두 검증할지 정하기
- 전체 어플리케이션 흐름 관리를 어디서 할지 정하기
- Application.main() 활용할 것 같음
- 상수 추출은 어떻게 할지 기준 세우기
- `enum`은 언제 사용할지
- 각 클래스 내부 VS 상수 클래스
- 상수 클래스 VS 인터페이스
- 의존성 주입 -> 별도의 Config 클래스 만들지 말기 (그냥 파라미터로 주입정도만)
- 각 디자인 패턴을 사용할 기준 정하기
- 빌더 패턴은 시간이 오래 걸리니 사용 X
- 전략 패턴
- 예외 처리
- 정적 팩토리 메서드
- 생성자의 파라미터로 필드값 자체를 전달해 주는 것이 아니라 로직이 필요한 경우
- 출력 메시지 상수로 관리할 지 정하기 -> 상수로 X
- Javadoc -> X
- 인터페이스 VS 추상클래스
- 상속 VS 조합
- Optional VS enum의 "유효하지 않음" 값
- 리팩토링 우선순위 정하기
- README.md 작성 연습
- 정규 표현식 연습하기

- 리팩토링 리스트
- 단위 테스트
- 상수 추출
- 메서드 추출
- 원시값 래핑
- 추상화 제공
- 네이밍
- 네이밍에 변수 타입 넣지 말기
- 통합 테스트

## 기억할 것

- 꾸준히 기능 단위로 커밋하기
- 변수는 선언시 무조건 `private final`, 나중에 필요하면 변경
- 조건 연산자를 사용한 코드는 조건 연산자 앞에서 줄바꿈
- Console 닫아주기
18 changes: 17 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
package racingcar;

import racingcar.domain.InitController;
import racingcar.domain.PlayController;
import racingcar.domain.car.Cars;

public class Application {
private static final InitController initController = new InitController();
private static final PlayController playController = new PlayController();

public static void main(String[] args) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흐름을 한 눈에 볼 수 있도록 Application은 SpringBoot의 Application 파일과 마찬가지로 코드를 최소화 하고,

하나의 Controller입력-처리-출력을 위임하는 로직을 모으면 어떨까 싶습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 대해 정~~말 많은 고민이 아직까지도 진행중인데요.
조금 더 정확히는, 일반적인 웹 MVC와는 흐름 자체가 조금 다르다 보니, 전체 어플리케이션 흐름을 관리하는 역할을 어느 객체에게 맡겨야 할지가 고민입니다.

자동차 게임의 경우 그래도 어플리케이션이 비교적 단순하지만, 크리스마스와 같이 책임이 여러 가지로 구성되어 있는 상황에서 전체 흐름을 하나의 Controller가 관리하는 것이 맞는지를 아직 잘 모르겠습니다...ㅎㅎ

많은 분들의 코드를 보니 하나의 흐름 제어를 위한 MainController를 두는 방법도 있고, 흐름 제어를 Application.main()에 맡기는 방법도 있더라고요.

@zxcev 님은 하나의 컨트롤러가 전체의 흐름을 제어하고, 각각의 책임은 세부 책임을 가지는 도메인 객체의 메서드를 Controller에서 호출하는 것으로 구현하신거죠?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네. 간단한 프로그램의 경우 굳이 Service를 만들지 않고,

크리스마스같이 도메인 및 처리할 로직이 다수인 경우는 Service를 만들게 되더라구요.

우아한 테크코스에서 제출하는 미션이 모두 입력-처리-출력 3단계로 맞아 떨어지지 않고,

조금씩 벗어나는 예외 시 재입력, 상태 선택, 처리 도중 출력 등의 패턴이 종종 보이기도 하는데

대부분의 경우, 입력-처리-출력 3단계에 가까운 형태로 만들 수 있었고,

이렇게 구조를 정해놓는 경우 하향식으로 코드를 작성할 때, 틀 잡기가 좀 더 수월했으며

전체 흐름은 Controller에서, 핵심 로직은 Service에서, 입출력 및 변환은 InputView에서 빠르게 확인할 수 있어서 좋았습니다.

요청 및 응답이 1:1로 대응되는 웹 MVC와 흐름이 완전히 같지는 않고, CLI 환경에서 작업을 해본 적이 없어서 더 좋은 방법이 분명히 있겠지만, 단기간에 하기엔 익숙한 형태에 접목시키는 것이 더 수월할 것이라고 생각했습니다.

미션에 직접 사용했던 크리스마스 및 로또 Controller 쪽 코드는 아래와 같습니다.

// 크리스마스 미션
public final class OrderController {

   // ...

   public void run() {
        final OrderDiscounting orderDiscounting = inputView.inputOrder();
        final OrderDto orderDto = orderService.order(orderDiscounting);
        outputView.printOrderDto(orderDto);
    }
}
// 로또 미션
/**
 * 입력, 실행, 출력 3단계의 흐름 제어 역할을 맡는 Controller 계층입니다.
 * <p>
 * 실제 로직이 작성된다기보다는, 전체적인 흐름 및 기능 목록을 한 눈에 볼 수 있도록,
 * <p>
 * 하위 계층에게 대부분의 처리를 위임하여 적은 코드량을 유지하여 가독성을 높이는 것이 좋다고 생각했습니다.
 */
public final class LottoController {
    private final InputView inputView;
    private final OutputView outputView;
    private final LottoService lottoService;

    public LottoController(
            final InputView inputView,
            final OutputView outputView,
            final LottoService lottoService
    ) {
        this.inputView = inputView;
        this.outputView = outputView;
        this.lottoService = lottoService;
    }

    /**
     * 로또 프로그램은 총 2회의 입출력으로 이루어져 있음. 첫 번째는 금액 입력 및 로또 구매 후, 구매한 로또 번호 출력 두 번째는 당첨 번호 및 보너스 번호 입력 및 당첨 결과 확인, 그리고 당첨 통계
     * 출력
     * <p>
     * 이를 2개의 메소드로 나누어 흐름을 보기 좋게 정리
     */
    public void run() {
        buyLottos();
        drawLottos();
    }

    /**
     * 첫 번째 입출력을 처리
     * <p>
     * 1. 로또 구매 금액 입력을 받는다.
     * <p>
     * 2. 로직 처리를 서비스에 위임한다
     * <p>
     * 3. 로또 구매 결과를 출력한다.
     */
    private void buyLottos() {
        final BuyLottosInput inputDto = inputView.inputBuyLottos();
        final BuyLottosOutput outputDto = lottoService.buyLottos(inputDto);
        outputView.printLottosBuyingResult(outputDto);
    }

    /**
     * 두 번째 입출력을 처리
     * <p>
     * 1. 로또 당첨 번호 및 보너스 번호를 입력 받는다.
     * <p>
     * -> 웹 페이지로 생각하면, 로그인 시에 이메일을 입력하고 다음 버튼을 누르면 페이지 이동이 아닌, 클라이언트 사이드 렌더링이 이루어 진 뒤, url 이동 없이 비밀번호 입력 창으로 살짝 전환되는
     * 느낌으로 하나의 페이지에서 2개의 입력을 모아서 받는다고 생각하여 1개의 컨트롤러에서 처리. 2. 로직 처리를 서비스에 위임한다 3. 로또 구매 결과를 출력한다.
     * <p>
     * 2. 저장된 모든 로또에 대한 추첨 로직 처리를 서비스에 위임한다.
     * <p>
     * 3. 로또 당첨 통계 결과를 출력한다.
     */
    private void drawLottos() {
        final DrawLottosInput inputDto = inputView.inputDrawLottos();
        final DrawLottosOutput outputDto = lottoService.drawLottos(inputDto);
        outputView.printLottosDrawingResult(outputDto);
    }
}

정답이 아닌 하나의 방법일 뿐이며, 한번 시도해보시면 인사이트를 얻을 수도 있겠다 싶어서 말씀드렸습니다.😊

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zangsu 저도 간단하게 의견을 공유하고 싶어요.

프로젝트가 고도화되고 규모가 확장되면 Controller 분리를 고민해야 하는 시점이 찾아온다고 생각해요. 그런데 이번 6기 프리코스에서 출제된 미션은 모두 하나의 목적을 구현하는 방식이라고 이해했습니다.

숫자 야구(승자 출력), 자동차 경주(승자 출력), 로또(당첨 여부 출력), 크리스마스(예약 내역 출력)

따라서 하나의 Controller에서 구현했고 책임이 과도해질 때 Service로 분리하는 방식으로 구현하고 있어요. 물론 최종에서 여러 가지 목적을 구현해야 하는 미션이 출제될 수도 있지만 고민하고 계신 부분에 도움이 되고 싶어 말씀드려 봅니다. 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hugh-KR
동의합니다!
생각해 보니 우리가 만나게 될 어플리케이션의 관심사는 한 문장으로 요약할 수 있는, 규모가 크지 않은 어플리케이션이군요!

덕분에 앞으로는 하나의 Controller에서 각각의 메서드로 동작만 구분시켜 구현을 하게 될 것 같습니다!

// TODO: 프로그램 구현
Cars cars = initController.initCars();
int rounds = initController.getRounds();
initController.closeConsole();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

충분히 분리하려면 분리하셨을 것 같은데, 시간 관리를 염두하여 main 문에서 처리하도록 하신걸까요 ?!
rounds 변수명을 totalRound로 바꾸면 조금 더 가독성이 좋아질 것 같습니다 😊

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분을 분리한다면 어떤 식으로 분리를 생각하셨을까요??
저는 이번 미션에서는 Application이 전체 흐름을 관리하고, 각각의 Controller부터는 해당 Controller의 관심사에 해당하는 책임만을 신경쓰도록 구현해 보았고, 그 결과 main() 함수에서부터 로직의 흐름이 일부 존재하게 되었습니다.

이 부분은 저 역시 아직은 기준을 정하지 못한 부분이라 고민이 많이 되네요....ㅎㅎ

rounds 변수명에 대한 리뷰는 동의합니다!
아직도 네이밍에는 많이 약하군요...ㅎㅎ

Copy link

@Hugh-KR Hugh-KR Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 로직에서 제 생각을 말씀드리자면.

InitController, PlayController 를 통합하고 현재 main에 있는 로직을 모두 컨트롤러 내부에서 정의해서 사용하면 좋을 듯 합니다.

통상적인 Controller의 역할과도 부합하고 2개 생성되고 있는 컨트롤러의 생성 비용을 1개로 줄일 수 있으니까요.

Application 클래스에서 직관적으로 동작을 보여주고 싶다면 통합된 Controller에서 Init, Play 메서드로 분리해서 출력하면 Controller 생성, Init, Play 메서드 3개 로 main을 구성할 수 있습니다.

현재도 분리는 정말 잘하셨는데 고민하고 계신 것같아 코멘트 납깁니다. 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 "ControllerApplication 중 어느 곳이 전체 흐름을 총괄하는 데 더 적합한가?" 에 대한 결정은 내리지 못했습니다만, 만약 Controller 내부에서 전체 흐름을 총괄한다면 창혁님 말씀처럼 하나의 컨트롤러에 모든 로직을 집중시킬 것 같습니다!

Copy link

@june-777 june-777 Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이번 미션에서는 Application이 전체 흐름을 관리하고, 각각의 Controller부터는 해당 Controller의 관심사에 해당하는 책임만을 신경쓰도록 구현해 보았고, 그 결과 main() 함수에서부터 로직의 흐름이 일부 존재하게 되었습니다.

어떤 의도로 구현하셨는지, 의도를 알게 되었네요!!

저는 크리스마스 프로모션 미션을 진행하며, FrontController에서 전체 흐름을 담당하게 하고 ChristmasController는 Service Layer만 호출하여 응답을 반환하는 구조로 진행했었습니다.
ChristmasController가 굉장히 얇아지는 장점도 있었지만, 일반적인 웹 개발처럼 repository가 없다보니 FrontController에서도 Domain 객체를 알고있게 되는 아쉬움이 뒤섞이더라구요. 전체 흐름을 관리하는 부분이 아직까지 참 고민이 많은 부분이네요 🤔🤔

playController.printResultTitle();
for (int round = 0; round < rounds; round++) {
playController.move(cars);
playController.printRoundResult(cars);
}
playController.printWinner(cars);
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/RacingConst.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar;

public interface RacingConst {
int RACING_NUMBER_START = 0;
int RACING_NUMBER_END = 9;
int RACING_NUMBER_MOVE = 4;

String DELIMITER = ",";
String WINNER_DELIMITER = ", ";
}
23 changes: 23 additions & 0 deletions src/main/java/racingcar/domain/InitController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.domain;

import java.util.List;
import racingcar.domain.car.Cars;
import racingcar.domain.numbergenerator.RandomNumberGenerator;
import racingcar.view.InputView;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InitController를 구현하신게 상당히 신박한 방법같습니다!
약간 repository 느낌도 나는데, 시간 관리를 염두하여 해당 방식으로 구현하신걸까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4주간의 미션을 진행하며 Controller의 책임에 대한 리뷰도 많이 받았는데요.
이번 미션 구현에서는 "각각의 책임들을 Controller에서 부터 구분하여 진행을 해 보자" 라는 생각으로 임했습니다.
다만, 자동차 게임의 경우 3~4주차 미션에 비해 조금은 단순한 어플리케이션이기에 로직 상의 책임을 나누기는 어려웠고, 게임 준비 / 게임 진행 의 두 가지 책임으로 구분하며 만들어진 Controller입니다!!

시간 관리를 생각한다면 하나의 Controller가 모든 로직의 흐름을 관리하는 것이 더 좋은 방법일 수 있을 것 같네요!!

public class InitController {
private final InputView inputView = new InputView();

public Cars initCars() {
List<String> names = inputView.getNames();
return new Cars(names, new RandomNumberGenerator());
}

public int getRounds() {
return inputView.getRoundNumber();
}

public void closeConsole() {
inputView.closeConsole();
}
}
28 changes: 28 additions & 0 deletions src/main/java/racingcar/domain/PlayController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package racingcar.domain;

import java.util.List;
import racingcar.domain.car.Car;
import racingcar.domain.car.Cars;
import racingcar.view.OutputView;

public class PlayController {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전 리뷰와 이어지는 내용입니다.

Controller는 입력, 처리, 출력의 전체 흐름이 모두 보이는 것이 일반적이기 때문에,

InitController - 입력 위임

Application - 처리 위임

PlayController - 출력 위임

구조를 하나의 Controller에 모아주면 좋을 것 같습니다.

private final OutputView outputView = new OutputView();

public void move(Cars cars) {
cars.move();
}

public void printResultTitle() {
outputView.printResultTitle();
}

public void printRoundResult(Cars cars) {
List<Car> roundResult = cars.getCars();
outputView.printRountResult(roundResult);
}

public void printWinner(Cars cars) {
List<String> winnerNames = cars.getWinner();
outputView.printWinners(winnerNames);
}
}
35 changes: 35 additions & 0 deletions src/main/java/racingcar/domain/car/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package racingcar.domain.car;

import racingcar.domain.numbergenerator.MoveNumber;

public class Car {
private static final int INIT_POSITION = 0;
private static final int DISTANCE_INCREASE = 1;
private final CarName name;
private int position;

public Car(String name) {
this.name = new CarName(name);
this.position = INIT_POSITION;
}

public boolean move(MoveNumber number) {
if (number.canMove()) {
this.position += DISTANCE_INCREASE;
return true;
}
return false;
}

public boolean at(int position) {
return this.position == position;
}

public String getName() {
return name.getName();
}

public int getPosition() {
return position;
}
}
23 changes: 23 additions & 0 deletions src/main/java/racingcar/domain/car/CarName.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.domain.car;

import racingcar.exception.RacingException;

public class CarName {
public static final int NAME_MAX_LEN = 5;
private final String name;

public CarName(String name) {
validateName(name);
this.name = name;
}

private void validateName(String name) {
if (name.isBlank() || name.length() > NAME_MAX_LEN) {
throw RacingException.INVALID_CARS_NAME.makeException();
}
}

public String getName() {
return name;
}
}
61 changes: 61 additions & 0 deletions src/main/java/racingcar/domain/car/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package racingcar.domain.car;

import java.util.List;
import racingcar.domain.numbergenerator.NumberGenerator;
import racingcar.exception.RacingException;

public class Cars {
private final List<Car> cars;
private final NumberGenerator numberGenerator;

public Cars(List<String> carNames, NumberGenerator numberGenerator) {
validateCars(carNames);
cars = carNames.stream()
.map(Car::new)
.toList();
this.numberGenerator = numberGenerator;
}
Comment on lines +11 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 로직을 조금 더 효율적으로 만드는 방법을 고민해 봤어요.

Cars 객체는 생성과 동시에 validateCars(carNames);를 통해 비어있는 경우, 중복되는 경우를 검증하고 있습니다.

그렇다면 아래의 cars 리스트 생성 스트림validateCars의 위치를 바꾸면 어떻게 될까요?

Cars 객체가 생성과 동시에 리스트에 Car 객체를 생성 및 할당하고 Car 객체 에서도 빈값을 검증하고 있기에, 입력값이 ""인 경우와 pobi,"",Jon 인 경우를 한 번에 처리할 수 있습니다.

이로써 얻어지는 이점은 다음과 같습니다.

- 2번 할 수 있는 일을 1번으로 줄임
- validateNotEmpty() 메서드 제거 가능

이에 대해 의견을 나누고 싶습니다. 혁수님은 어떻게 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어라, 그렇군요!
이제와서 다시 코드를 살펴보니 제가 왜 validateNotEmpty() 메서드를 만들었는지 모르겠네요 ㅋㅋㅋ

창혁님이 위치를 변경하는 가장 큰 이유로 중복되는 검증이 존재한다 라는 근거를 들어주셨는데, 저는 Cars.vaildateNotEmpty()만 제거해 주고 나머진 그대로 둘 것 같습니다.

중복을 확인하는 부분은 Car 객체보다 String으로 수행하는 것이 훨씬 편할 것이라 생각하기도 했고, 검증된 값들을 이용해서 객체 내부 필드를 업데이트 하는 쪽이 조금 더 취향에 가까워서요...ㅎㅎ


private void validateCars(List<String> carNames) {
validateNotEmpty(carNames);
validateNotDuplicated(carNames);
}

private void validateNotDuplicated(List<String> carNames) {
int distinctSize = (int) carNames.stream()
.distinct()
.count();
if (distinctSize != carNames.size()) {
throw RacingException.DUPLICATE_CARS_NAME.makeException();
}
}

private void validateNotEmpty(List<String> carNames) {
if (carNames.isEmpty()) {
throw RacingException.NO_CAR_FOUNDED.makeException();
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설계에 대해 의견을 나누고 싶어 코멘트 드립니다!!

레이싱 차들을 움직이는 협력 에 대한 설계가 아래와 같은 구조인 것으로 보입니다.
image
위의 구조도 충분히 좋다고 생각됩니다! 아래와 같은 설계는 어떻게 생각하시는지 여쭤보고 싶어요!


Refactor Ver1

image

  • 지극히 개인적인 의견으로는 번호를 생성해서 자동차가 움직이는 일련의 과정들은 자동차 본연의 책임이라는 생각도 드는데, 어떻게 생각하시는지 궁금해요!
  • 장점으로는 Cars와 NumberGenerator 간의 의존 관계를 제거할 수 있는 측면이 있을 것 같고,
  • 단점?으로는 Car에게 책임이 조금 더 집중되는 트레이드 오프가 있을 것 같습니다!

더 나은 설계인지는 저도 물음표네요 ㅎㅎ,, 그저 설계에 대한 다양한 의견들을 나누고 싶어 코멘트 드립니다 😊

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 "좋은 설계" 에 대한 고민은 정답도 없을 뿐더러, 각각의 설계에 대한 장단점이 존재하기에 너무 어려운 것 같아요...ㅎㅎ

제가 위와 같이 설계를 하게 된 이유는 "자동차가 움직인다" 라는 기능을 테스트 할 때 외부에서 값을 받아 오는 편이 더 테스트 하기 쉬울 것이라 생각했기 때문이에요.
그래서 테스트 코드는 단순히 "~~의 값을 전달받으면 이동한다" 와 같이 자동차의 움직임을 더 쉽게 테스트 할 수 있었고요.

다만, @june-777 님의 의견처럼, 제 설계가 "이동의 책임을 가지고 있는 자동차가 자기 책임을 온전히 다하지 않는다"고 생각이 되기도 하네요.
Car 객체 자체가 NumberGenerator를 가지고 있다 할지라도, 고정된 값을 생성해 주는 FixedValueGenerator같은 것을 만들어 주입해 준다면 테스트는 똑같이 어렵지 않을 것 같습니다!

그리고 이 방법 역시 책임이 집중되는 것이긴 하나, 해당 어플리케이션에서의 중심이 되는 클래스이기에 어느 정도는 어쩔 수 없는 부분이지 않나 라는 생각도 들고요! ㅎㅎ

결론만 정리하자면, @june-777 님이 의견 주신 설계 정말 괜찮은 것 같습니다!!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다!! 설계는 정답이 없어서 너무 어려운 영역인 것 같아요 🤔
내가 구현한 설계가 어떤 트레이드 오프를 갖는지 인지하는게 중요한 것 같다는 생각이 드네요!!
여러 생각들을 공유해주셔서 감사합니다 😊 😊

public void move() {
cars.forEach(car -> car.move(numberGenerator.generate()));
}

public List<Car> getCars() {
return cars;
}

public List<String> getWinner() {
int winnersPosition = getWinnersPosition();
return cars.stream()
.filter(car -> car.at(winnersPosition))
.map(Car::getName)
.toList();
}

private int getWinnersPosition() {
return cars.stream()
.mapToInt(Car::getPosition)
.max()
.getAsInt();
}
}
23 changes: 23 additions & 0 deletions src/main/java/racingcar/domain/numbergenerator/MoveNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package racingcar.domain.numbergenerator;

import racingcar.RacingConst;
import racingcar.exception.RacingException;

public class MoveNumber {
private final int number;

public MoveNumber(int number) {
validateNumber(number);
this.number = number;
}

private void validateNumber(int number) {
if (number < RacingConst.RACING_NUMBER_START || number > RacingConst.RACING_NUMBER_END) {
throw RacingException.INVALID_NUMBERS.makeException();
}
}

public boolean canMove() {
return this.number >= RacingConst.RACING_NUMBER_MOVE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar.domain.numbergenerator;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default method를 구현하신 이유가 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음 제가 구현했던 방법은 이동에 사용되는 "숫자 자체"를 도메인에서 사용하는 방법이었습니다!
그리고, 이 "숫자를 생성하는 방법"에 대해 추상화를 진행했던 거고요!

리팩토링을 하며 이동에 사용되는 숫자를 클래스로 래핑하게 되었는데요. 그러나 이 상황 역시 "숫자 생성"에 대해서만 추상화를 제공하고, 생성된 숫자를 "MoveNumber"로 만드는 부분 부터는 중복되는 것으로 보였습니다.
그래서 기존의 숫자를 생성하는 Generator들을 그대로 두고 default 메서드로 해당 숫자를 사용해 MoveNumber 객체를 생성하는 부분만 새로 구현해 주었습니다!

@FunctionalInterface
public interface NumberGenerator {
default MoveNumber generate() {
return new MoveNumber(pickNumber());
}

int pickNumber();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package racingcar.domain.numbergenerator;

import camp.nextstep.edu.missionutils.Randoms;
import racingcar.RacingConst;

public class RandomNumberGenerator implements NumberGenerator {

@Override
public int pickNumber() {
return Randoms.pickNumberInRange(RacingConst.RACING_NUMBER_START,
RacingConst.RACING_NUMBER_END);
}

}
Loading