Skip to content

[1단계 - 자동차 경주 구현] 케빈(박진홍) 미션 제출합니다.#165

Merged
choihz merged 33 commits intowoowacourse:xlffm3from
xlffm3:step1
Feb 6, 2021
Merged

[1단계 - 자동차 경주 구현] 케빈(박진홍) 미션 제출합니다.#165
choihz merged 33 commits intowoowacourse:xlffm3from
xlffm3:step1

Conversation

@xlffm3
Copy link

@xlffm3 xlffm3 commented Feb 6, 2021

안녕하세요?

페어와 함께 다음 내용을 고민했습니다.

  • Cars는 String(자동차 이름들)을 인자로 받는 정적 팩토리 메서드만 public이고 기본 생성자(List cars)는 private입니다.
  • Cars의 move는 랜덤값을 생성해 리스트에 속한 자동차들을 움직이게 하는데, 이 때문에 Cars에서 우승한 자동차의 명단을 추출하는 테스트를 작성하기 곤란한 상황입니다.
  • 기본 생성자를 public으로 두고 아래와 같이 테스트를 작성해볼 수 있을 것 같은데요.
@Test
void getWinnerNames_우승자는_pobi_brown이다() {
	 Car pobi = new Car("pobi");
	 Car brown = new Car("brown");
   Car jiko = new Car("jiko");
	 List<Car> cars = new Cars(Arrays.asList(pobi, brown, jiko));

   pobi.move(4);
   brown.move(4);
   List<String> winnerNames = cars.getWinnerNames();
   
   assertThat(winnerNames).containsExactly("pobi", "brown");
}
  • 프로덕션 코드에서 사용하지 않는 기본 생성자를 테스트를 위해 public으로 두는 것이 맞을지 고민입니다. 테스트 하기 어려운 구조라는 것은 아직 리팩토링할 여지가 많다고 생각되는데, 이에 대해 리뷰어님의 의견을 듣고싶습니다!

Livenow14 and others added 30 commits February 3, 2021 15:04
쉼표 혹은 콜론을 구분자로 가지는 문자열을 구분자를 기준으로 분리한 다음 숫자의 합을 반환한다.
 구분자와 숫자를 제외한 문자 혹은 음수가 문자열에 포함되어 있으면 RuntimeException을 던진다
test 클래스에 있던 StringCalulator를 main클래스로 이동한 후, 테스트 확인 완료
10라인 이상의 메소드를 수정하였고, 메소드가 한가지 일만 하도록 수정함
tryCount가 0이면 false를 반환한다.
tdd를 진행하며 사용했던 클래스트들을 test에서 삭재함.
Car 클래스 접근지정자 변경
- 메서드 인라인화 할 수 있는 부분은 인라인화한다.
- 인스턴스 변수는 클래스 선언 사이에 공백을 추가한다.
- 정적 팩토리 메서드 네이밍 규칙에 맞게 of를 from으로 변경한다.
- 콘솔의 화면을 담당하는 Screen 추상 클래스 및 하위 클래스를 삭제한다.
- 기존 InputView와 OutputView가 해당 스크린 클래스들의 역할을 수행한다.
Copy link

@choihz choihz left a comment

Choose a reason for hiding this comment

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

안녕하세요, 케빈! 리뷰어 코니입니다.
첫 미션을 아주 잘 구현해 주셨네요 🏅 컨벤션 지키기, 메서드 분리, 의미 있는 메서드 네이밍, 기능 목록 정리와 기능별 커밋 모두 훌륭합니다 👍 남겨드린 피드백은 생각해 보시고 다음 단계에서 반영해 주세요.

말씀하신 대로 CarsRandomUtils와 강하게 결합되어 있어 테스트 코드 작성이 어려운 상황이네요. 물론 케빈의 제안대로 테스트를 위해 생성자를 오픈할 수도 있겠지만 이번 미션의 경우엔 더 좋은 방법이 있을 것 같아요! 피드백에 남겨드린 힌트를 참고해서, 어떻게 하면 이 결합을 분리할 수 있을지 고민해 보세요.

더 궁금한 점이 있으시다면 언제든 코멘트나 DM 주세요~

class CarsTest {

@Test
void Cars_쉼표로_구분된_문자열_받으면_객체가_생성된다() {
Copy link

Choose a reason for hiding this comment

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

메서드 이름은 영어로 간단히 짓고, 처음에 학습 테스트를 할 때 사용했던 @DisplayName으로 한글 이름을 보여주면 어떨까요? 매번 _를 쓸 수고도 줄어들고 가독성도 더 좋아질 것 같네요!

void Cars_쉼표로_구분된_문자열_받으면_객체가_생성된다() {
String carNames = "pobi,brown";
assertThatCode(() -> Cars.generate(carNames))
.doesNotThrowAnyException();
Copy link

Choose a reason for hiding this comment

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

실행한 코드가 어떤 예외도 던지지 않는다는 사실만으로 "객체가 생성"됨을 보장할 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

확실히 추가적인 테스트가 필요하겠네요!!

@@ -0,0 +1,26 @@
package racing.domain.dto;
Copy link

Choose a reason for hiding this comment

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

dto는 domain의 하위 개념이 아니에요! 패키지 위치가 부적절한 것 같네요~

Comment on lines +15 to +18
public Car(String name) {
this.name = name;
this.position = DEFAULT_POSITION;
validateName();
Copy link

Choose a reason for hiding this comment

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

생성자 안에서 입력값을 검증하고 있군요 👍 하지만, 입력값을 먼저 검증한 이후에 인스턴스 변수에 할당해 주세요!

Copy link
Author

Choose a reason for hiding this comment

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

페어와 방어적 복사에 대한 개념을 잘못 이해하고 유효성 검증 코드를 짰네요... 😓😓

}
}

public boolean move(int randomNumber) {
Copy link

Choose a reason for hiding this comment

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

Car 입장에서, 받는 숫자가 "random"인지 알 필요가 있을까요?

또한 현재 이 메서드는 자동차가 움직였을 때 true, 움직이지 않았을 때 false를 리턴하고 있네요. 그리고 이 반환값은 테스트에서만 쓰이고 있어요! 테스트를 다른 방법으로 할 수 있다면, 이 메서드도 이름이 암시하는 것처럼 움직이는 한 가지 일만 할 수 있지 않을까요?

public List<String> findWinnerNames() {
int maxPosition = getMaxPosition();
return cars.stream()
.filter(car -> car.getPosition() == maxPosition)
Copy link

Choose a reason for hiding this comment

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

람다와 스트림을 쓰셨군요 👍
여기서는 Car의 위치 정보를 가져와 maxPosition과 직접 비교하고 있는데요, 이 역할을 Car에게 위임하면 어떨까요? 이것을 "메시지를 보낸다"고 표현하곤 하는데요, 이렇게 메시지를 보내면 어떤 장점이 있을지도 생각해 보아요.

Copy link

Choose a reason for hiding this comment

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

기능 목록 정리 및 기능별 커밋 👍👍

}

public void race() {
cars.forEach(car -> car.move(RandomUtils.getRandomNumber(START_NUMBER, END_NUMBER)));
Copy link

Choose a reason for hiding this comment

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

CarsRandomUtils와 강하게 결합하고 있네요~ 이렇게 되면 Cars의 테스트가 굉장히 어려워지죠!
다음 단계에서 이 결합을 어떻게 분리시킬지 고민해 보고, 테스트 코드도 작성해 보아요! (힌트: 전략 패턴)

void Car_유효하지_않은_이름_예외가_발생한다(String name) {
assertThatThrownBy(() -> {
new Car(name);
}).isInstanceOf(IllegalArgumentException.class);
Copy link

Choose a reason for hiding this comment

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

예외를 던질 때 예외 상황에 맞는 메시지를 담아주면, 아래와 같이 더 정밀하게 검증할 수 있겠죠?

assertThatThrownBy(() -> {
    // ...
}).isInstanceOf(IllegalArgumentException.class)
  .hasMessageContaining("이름은 5자 이하만 가능합니다.");

}
Matcher matcher = PATTERN.matcher(this.name);
if (!matcher.matches()) {
throw new IllegalArgumentException();
Copy link

Choose a reason for hiding this comment

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

이름 유효성 검증 👍
하지만 예외에 어떤 메시지도 담겨있지 않네요. 그래서 실제로 프로그램을 실행해 잘못된 이름을 입력했을 때, 사용자는 영문도 모른 채 영어로 된 이상한 메시지만 보게 돼요. 상황에 맞는 적절한 예외 메시지를 담아주면 어떨까요?

@choihz choihz merged commit 7c3a084 into woowacourse:xlffm3 Feb 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants