![[넥스트스탭] 자동차 경주 게임 - 자바 플레이 그라운드](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2bH4j%2FbtsEf6mQ8iw%2FRhy0POkBaIaaccn94iEY51%2Fimg.jpg)
![leejinwoo1126](https://t1.daumcdn.net/tistory_admin/static/manage/images/r3/default_L.png)
깃 저장소
학습 전
https://github.com/ljw1126/my-java-racingcar-playground/tree/myself/src/main/java
학습 후
https://github.com/ljw1126/my-java-racingcar-playground/tree/practice/src/main/java
복습 - 자동차 경주 게임
[기능 요구사항]
① 각 자동차에 이름을 부여할 수 있다. (이때 자동차 이름은 5자를 초과할 수 없다)
② 자동차 이름은 쉼표(,)를 기준으로 구분한다
③ 전진하는 조건은 0에서 9사이에서 random값을 구한 후 값이 4이상인 경우이다
④ 게임 종료 후 우승자를 출력한다. (이때 우승자는 한명 이상일 수 있다)
[실행 결과]
경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)
pobi,crong,honux
시도할 회수는 몇회인가요?
5
실행 결과
pobi : -
crong : -
honux : -
pobi : --
crong : -
honux : --
pobi : ---
crong : --
honux : ---
pobi : ----
crong : ---
honux : ----
pobi : -----
crong : ----
honux : -----
pobi : -----
crong : ----
honux : -----
pobi, honux가 최종 우승했습니다.
1. 테스트 하기 힘든 코드를 테스트 가능한 구조로
요구사항에 따라 각 턴마다 Car.move() 호출하면 random 값이 4이상인 경우에만 이동을 하게 구현하였다
public class Car {
private static final int FORWARD_NUMBER = 4;
private String name;
private int position;
public Car(String name) {
this(name, 0);
}
public Car(String name, int position) {
this.name = name;
this.position = position;
}
public void move() {
if(getRandom() >= FORWARD_NUMBER) { //*
this.position += 1;
}
}
private int getRandom() {
return RandomNumberGenerator.generateRandomNumber();
}
public int getPosition() {
return position;
}
}
겉으로 보기에 문제가 없는 코드로 보이나, 클래스 내부 random값을 제어할 수 없기 때문에 테스트 실행시 일정하지 않은 결과가 보여졌다
방법1. getRandom() 접근 제어자를 protected 변경하고 오버라이딩 한다
class CarTest {
@Test
void move() {
Car car = new Car("dobby", 0) {
@Override
protected int getRandom() {
return 4;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void stop() {
Car car = new Car("dobby", 0) {
@Override
protected int getRandom() {
return 3;
}
};
car.move();
assertThat(car.getPosition()).isEqualTo(0);
}
}
- 레거시 코드가 변경하기 어려운 코드인 경우 해당 방식을 사용하는 것도 효과적으로 보인다
- 주의. equals() 메소드로 객체를 비교할 경우 false 반환한다. getClass()에서 오버라이드한 Car객체와 신규 Car인스턴스를 다르다 판단 내리는 듯 하다.
-객체와 객체를 비교 못하고 car.getPosition()값을 가져와 원시값 비교하는 부분이 아쉽다
(아래 "원시값과 문자열 포장하기"에서 개선)
방법2. 의존성 주입(DI)을 사용하여 랜덤값을 외부에서 주입할 수 있도록 한다(🌟Pick🌟)
public class Car {
// 필드, 생성자
public void move(int random) {
if(random >= FORWARD_NUMBER) {
this.position += 1;
}
}
}
테스트 코드 수정
class CarTest {
@Test
void move() {
Car car = new Car("dobby");
car.move(4);
assertThat(car.getPosition()).isEqualTo(1);
}
@Test
void stop() {
Car car = new Car("dobby");
car.move(3);
assertThat(car.getPosition()).isEqualTo(0);
}
}
외부에서 랜덤값(경계값)을 인자로 주입하기 때문에 테스트 하기 쉬워졌다
이외에도 디자인 패턴 중 전략 패턴(Strategy Pattern)을 사용해서 외부에서 전략을 주입 하는 방식도 생각할 수 있겠지만,
지금 상황에 너무 확장을 고려해서 작성하는 건 오버 엔지니어링으로 빠질 위험이 있어 보이므로 추후 필요시 고려하도록 하였다.
"정답은 없다. 상황에 따라 최선의 선택을 하면 된다" - 자바지기
2. 원시값과 문자열 포장하기 (Wrapper 클래스)
Car 객체에서 position과 name 필드를 포장하는 클래스를 만든다
원시값을 포장한다 == 원시값을 가진 객체를 생성한다
Position 클래스 생성
- move() 메소드가 Car에서 Position으로 이동했다
- 그리고 move() 호출시 Position을 새로 생성하여 반환하기 때문에 Position은 Immutable 하다 할 수 있겠다
public class Position {
private int position;
public Position(int position) {
if(position < 0) {
throw new IllegalArgumentException("위치값은 0 이상만 가능합니다.");
}
this.position = position;
}
public Position move() {
return new Position(this.position + 1);
}
// equals, hashCode
}
Car 클래스 리팩토링
public class Car {
private static final int FORWARD_NUMBER = 4;
private Name name;
private Position position;
public Car(String name) {
this(name, 0);
}
public Car(String name, int position) {
this.name = new Name(name);
this.position = new Position(position);
}
public void move(int random) {
if(random >= FORWARD_NUMBER) {
this.position = this.position.move();
}
}
public Position getPosition() {
return position;
}
}
테스트 코드 리팩토링
Position 참조값을 가져와 Position 객체와 비교하도록 변경한다
class CarTest {
@Test
void move() {
Car car = new Car("dobby");
car.move(4);
assertThat(car.getPosition()).isEqualTo(new Position(1));
}
@Test
void stop() {
Car car = new Car("dobby");
car.move(3);
assertThat(car.getPosition()).isEqualTo(new Position(0));
}
}
"객체와 객체를 비교하는게 객체 지향 사고이다" - 자바지기
*원시값 포장했을 때 장/단점
장점
① 유효성 검사와 같이 관련 로직이 Wrapper 클래스로 이동시킬 수 있고, 책임을 위임할 수 있다
② 변경이 한 곳으로 최소화됨으로써 유지보수 용이해지고, 변경시 사이드 이펙트 ↓ (SRP 원칙 준수)
③ 테스트하기 쉬워진다
④ 클래스가 작아진다
단점
인스턴스 생성시 Heap 메모리에 저장되고, 주소값 참조 하기 때문에 원시값보다 메모리(리소스)를 잡는다
3. 일급 컬렉션(First Class Collection)
- 일급 컬렉션이란 Collection 자료를 원시값으로 보고 Wrapping한 클래스로, 다른 멤버 변수가 없는 상태를 가진다
- 일급 컬렉션을 사용하면 아래 효과를 얻을 수 있다
① 컬렉션의 불변성 보장 (사이드 이펙트 최소화)
② 관련 비즈니스 함수를 한 곳에서 관리 가능 (유지보수 용이, SRP 원칙 준수)
③ 해당 컬렉션에 대한 동일한 입력과 출력을 반환 (안전성 보장)
참고. 기억보단 기록을
https://jojoldu.tistory.com/412
일급 컬렉션 (First Class Collection)의 소개와 써야할 이유
최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코
jojoldu.tistory.com
[기능 정리]
참가자 정보(List<Car> participant)를 받아 멤버 필드에 담는다
참가자 중 우승자를 구한다 -- findWinners()
매턴 마다 참가자(Car)의 move() 메소드를 호출한다 -- move()
매턴 마다 결과를 기록한다 -- record()
Cars 테스트 코드 작성
class CarsTest {
@Test
void findWinners() {
Car dobby = new Car("dobby", 2);
Car harry = new Car("harry", 3);
Car ron = new Car("ron", 3);
Cars cars = new Cars(Arrays.asList(dobby, harry, ron));
List<Car> winner = cars.findWinner();
assertThat(winner).containsExactly(harry, ron);
}
}
Cars 클래스 생성
public class Cars {
private List<Car> carList;
public Cars(List<Car> participant) {
this.carList = participant;
}
private List<Car> mapping(List<String> participant) {
List<Car> result = new ArrayList<>();
for(String name : participant) {
result.add(new Car(name));
}
return result;
}
public List<Car> findWinner() {
return Collections.emptyList();
}
}
빨간불을 보았으니 빠르게 기능 구현을 한다
Cars에 findWinners()를 아래와 같이 기능 구현
public class Cars {
// 필드, 생성자
public List<Car> findWinner() {
int maxPosition = 0;
for(Car car : carList) {
maxPosition = car.maxPosition(maxPosition);
}
List<Car> result = new ArrayList<>();
for(Car car : carList) {
if(car.isSamePosition(maxPosition)) {
result.add(car);
}
}
return result;
}
}
초록불 확인, 이제 코드를 리팩토링 해본다
Cars.findWinner() 메소드 리팩토링 - 1차 통과
- maxPosition을 구하는 메소드 분리 (extract method refactoring)
- 좀 더 객체지향적으로 하기 위해 int 값이 아닌 Position 객체를 비교하여 구하도록 변경
public List<Car> findWinner() {
Position maxPosition = getMaxPosition();
List<Car> result = new ArrayList<>();
for(Car car : carList) {
if(car.isSamePosition(maxPosition)) {
result.add(car);
}
}
return result;
}
private Position getMaxPosition() {
Position maxPosition = new Position();
for(Car car : carList) {
maxPosition = car.maxPosition(maxPosition);
}
return maxPosition;
}
Cars.findWinner() 메소드 리팩토링 - 2차 통과
Stream API 사용해본다
public List<Car> findWinner() {
Position maxPosition = getMaxPosition();
return this.carList.stream()
.filter(car -> car.isSamePosition(maxPosition))
.collect(Collectors.toList());
}
정상적으로 테스트 통과할 수 있었다
실행 결과
view, controller와 기타 클래스 구현은 생략하였다(저장소 코드 참고)
회고
두 번째 과제를 수행하면서 Wrapper 클래스와 일급 컬렉션에 대해 학습하고 구현에 활용해 볼 수 있었다
3주만에 복습이다 보니 기억나지 않는 부분도 있었지만, 한가지 확실한 사실이라면
"테스트를 작성해두니 좀 더 과감하게 리팩토링을 진행할 수 있었고, 빠른 확인이 가능했다 "
이렇게 조금씩 어제 보다 나은 내가 되어간다
'공부 > Java' 카테고리의 다른 글
[넥스트스탭] 블랙잭 - 자바 플레이 그라운드 (0) | 2024.04.30 |
---|---|
[넥스트스탭] 좌표계산기 - 자바 플레이 그라운드 (3) | 2024.04.30 |
[넥스트스탭] 숫자 야구 게임 - 자바 플레이 그라운드 (0) | 2024.04.30 |
[Java] Reflection API 테스트 학습(with Baeldung) (0) | 2023.11.20 |
[Java] Stream Quiz 개인 풀이 (출처. 망나니 개발자 기술 블로그) (0) | 2023.08.15 |
![leejinwoo1126](https://t1.daumcdn.net/tistory_admin/static/manage/images/r3/default_L.png)
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!