4단계 : racing car 리팩토링
RacingCar 공부 회고..
리팩토링을 하면서 가장 우선적으로 일급 컬렉션을 사용하기 위하여 노력하였다. 일급 컬렉션의 장점은 일급 컬렉션을 사용하면서 기존에 car에 있던 코드들이 일급 컬렉션 안에 코드로 옮겨가지면서 조금 더 큰 틀을 쉽게 볼 수 있으며 OOP 규칙을 조금 더 잘지키면서 코드를 작성하는 느낌이 든다.
사실 OOP라는 말을 정말 많이 들었다. 근데 문득 "나는 OOP가 뭔지 잘 알고 있을까?" 라는 고민을 가지게 되었다.
OOP는 4가지 규칙을 가지고 있다.
1) 캡슐화
- 캡슐화는 객체의 데이터와 메서드를 하나의 단위로 묶고, 외부로부터 내부 상태를 숨기는 개념이다.
- 객체의 데이터는 private로 막아 외부에서 직접 접근을 불가능하도록 하고, getter, setter 메서드로 간접 접근을 가능하도록 하는 것
여기서 의문 ? setter 도 괜찮을걸까?
왜 setter를 사용하지 말아야하는가? 궁금함을 느꼈고 혼자서 스스로 고민을 해보았다. 그러고 정답인지는 모르겠다. 블로그 글도 찾아봤으며 책도 뒤져 보았다.
setter를 사용하지 않으면 필드 상태 값을 불변 값을 보장 할 수 있다. 즉, private final int age 이런 식으로 값을 보장할 수 있다.
그러면 '값을 어케 바꾸는데?' 라는 고민을 하게 된 것 같다.
정답은 new 연산자를 사용하자였고, 사실 이것은 next-step을 하면서 알려주어서 생각 할 수 있었다.
2) 상속
- 상속은 기존의 클래스의 속성과 메서드를 재사용하여 새로운 클래스를 만드는 개념이다. 즉, 상위 개념을 하위 개념에 적용하는 방법 이렇게 하면 코드의 재사용성을 늘려줄 수 있다.
- 예를 들어, Car 클래스를 상속받아 ElectricCar 클래스와 GasCar 클래스를 만들 수 있습니다. 이렇게 하면 공통된 기능은 Car 클래스에 정의하고, 전기차와 가솔린 차에 특화된 기능만 각각의 클래스에서 구현할 수 있다.
사실 상속으로 적극적으로 사용하려고 노력하지만, 진짜 어렵다. 어떻게 하면 상속을 더 잘 사용할지 나도 많이 고민해봐야겠다.
3) 다형성
- 다형성은 동일한 인터페이스나 메서드를 다양한 방식으로 구현할 수 있게 해주는 것이다.
다형성이야말로 테스트하기 어려운 것을 테스트하기 쉽게 만들어준다.
4. 추상화
- 추상화는 객체의 중요한 속성만을 추출하여 정의하는 것이다.
처음보다 뒤로 갈 수록 이야기가 적어졌지만, 코드를 보며 말할게 많기에 코드를 살펴보자.
Car.class
package racing.car.domain;
import racing.car.model.CarName;
import racing.car.model.Position;
import java.util.Objects;
public class Car implements Movable {
private final CarName name;
private Position position;
public Car(String name) {
this(name, 0);
}
// 생성자에서 name을 반드시 받도록 강제
public Car(String name, int position) {
this.name = new CarName(name);
this.position = new Position(position);
}
@Override
public void move(int number) {
position = position.increase(number);
}
public boolean isSame(int otherPosition) {
return this.position.isSame(otherPosition);
}
public int max(int maxPosition) {
return this.position.max(maxPosition);
}
public String getName() {
return this.name.getName();
}
public int getPosition() {
return this.position.getPosition();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Objects.equals(name, car.name) && Objects.equals(position, car.position);
}
@Override
public int hashCode() {
return Objects.hash(name, position);
}
@Override
public String toString() {
return name.toString();
}
}
Position.class
package racing.car.model;
import java.util.Objects;
public class Position {
private static final int MOVE_THRESHOLD = 4;
private int position;
public Position() {
}
public Position(int position) {
this.position = position;
}
public Position increase(int number) {
if (isMove(number)) {
position++;
}
return new Position(position);
}
private boolean isMove(int number) {
return number >= MOVE_THRESHOLD;
}
public int max(int maxPosition) {
return Math.max(this.position, maxPosition);
}
public boolean isSame(int otherPosition) {
return this.position == otherPosition;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Position position1 = (Position) o;
return position == position1.position;
}
@Override
public int hashCode() {
return Objects.hash(position);
}
public int getPosition() {
return this.position;
}
}
내가 Car 클래스를 만들면서 고민했던 것은 어떻게하면 앞서 말했겠지만 "조금 더 간단하게 쉽게 구현 할 수 있을까?"
외에도 어떻게 하면 "객체에게 조금 더 불변성을 제공할까?" , setter메서드를 사용하지 않고 코딩, 최대한 getter메서드를 사용하지 않기였다.
하지만 getter메서드는 값을 어쩔 수 없이 조회해야하기 때문에 사용하였다.
여기서 중요한 점은 나는 position이라는 원래 int형 변수로 사용되어야 할 값을 Position이라는 객체를 사용하여 만들어주었다.
이 코드에서 아쉬운 점은 'final 값을 Position 객체 안에 int형 을 줄 수 없었나?' 였고 new 를 통해서 충분히 가능하다는 결론이 나왔다.
그래서 new Position을 통해서 객체를 아예 새롭게 반환하고 있는 increase메서드를 볼 수 있다.
또한 Car에 Movable을 implements하여 구현하였고 이를 통해서 interface로 테스트하기 힘든 코드가 빠져버리기에 조금 더 테스트 하기 쉽게 작성해주었다. 사실 굳이 인터페이스가 아니여도 될 것이다는 생각을 나중에 하게되었다.
CarName.class
package racing.car.model;
import java.util.Objects;
public class CarName {
private static final String OVER_MESSAGE_ERROR = "자동차 이름이 5자글자를 초과하였습니다.";
private static final int NAME_LIMIT_LENGTH = 5;
private final String name;
public CarName(String name) {
check(name);
this.name = name;
}
private void check(String name) {
if (name.length() > NAME_LIMIT_LENGTH) {
throw new IllegalArgumentException(OVER_MESSAGE_ERROR);
}
}
public String getName(){
return this.name;
}
@Override
public String toString() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CarName carName = (CarName) o;
return Objects.equals(name, carName.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
차의 이름 또한 원자 값을 감싸주면서 많은 이점을 가져올 수 있었다.
RacingGameController.class
package racing.car.controller;
import racing.car.model.Cars;
import racing.car.random.GenerateRandom;
import racing.car.view.InputView;
import racing.car.view.ResultView;
import java.util.ArrayList;
import java.util.Scanner;
public class RacingGameController {
private static final Scanner scanner = new Scanner(System.in);
private static final InputView INPUT_VIEW = new InputView(scanner);
private static final ResultView RESULT_VIEW = new ResultView();
private static final String INVALID_CAR_COUNT_MESSAGE = "게임을 진행하려면 자동차가 최소 2대 있어야 합니다.";
private static final String INVALID_TRY_COUNT_MESSAGE = "게임을 진행하려면 시도 횟수는 1 이상이어야 합니다.";
public static void main(String[] args) {
RacingGameController game = new RacingGameController();
game.play();
}
public void play() {
INPUT_VIEW.carQuestion();
String[] carNames = INPUT_VIEW.inputCar();
Cars cars = new Cars(new ArrayList<>());
initializeCars(carNames, cars);
INPUT_VIEW.tryQuestion();
int tryCount = validateTryCount(INPUT_VIEW.inputTry());
for (int i = 0; i < tryCount; i++) {
simulateRaceRound(cars);
}
RESULT_VIEW.outputWinnerView(cars.findWinner(max(cars)));
}
public void simulateRaceRound(Cars cars) {
cars.move(new GenerateRandom());
RESULT_VIEW.outputView(cars);
}
public int max(Cars cars) {
return cars.getMaxPosition();
}
public void initializeCars(String[] carNames, Cars cars) {
cars.addCars(carNames);
}
public int validateCarCount(int carCount) {
if (carCount < 2) {
throw new RuntimeException(INVALID_CAR_COUNT_MESSAGE);
}
return carCount;
}
public int validateTryCount(int tryCount) {
if (tryCount <= 0) {
throw new RuntimeException(INVALID_TRY_COUNT_MESSAGE);
}
return tryCount;
}
}
해당 코드는 RacingGameController클래스이고 사실 이번 코드는 불완전한게 조금 더, 여기에 있는 메서드들을 객체 안으로 넣어 줄 수 있다는 생각을 하게 되었다. 그래서 완벽한 코드는 아니지만, 이렇게 보여주었다.
추후에 코드를 다시 작성해보았는데, 어떻게 변할지 두근두근이다.
RacingGameControllerTest
package racing.car.controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racing.car.domain.Car;
import racing.car.model.Cars;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.*;
public class RacingGameControllerTest {
private RacingGameController game;
@BeforeEach
void setUp() {
game = new RacingGameController();
}
@Test
@DisplayName("initializeCars 메서드는 각 Car 객체를 올바르게 초기화해야 한다.")
void initializeCars_메서드_테스트() {
Cars cars = new Cars(new ArrayList<>());
String[] carNames = {"leo", "seoun"};
game.initializeCars(carNames, cars);
assertThat(cars).isEqualTo(new Cars(List.of(new Car("leo", 0), new Car("seoun",0))));
}
@Test
@DisplayName("validateCarCount 메서드는 유효한 자동차 개수 입력 시 해당 값을 반환해야 한다.")
void validateCarCount_메서드_테스트_성공() {
int carCount = 2;
int result = game.validateCarCount(carCount);
assertThat(result).isEqualTo(2);
}
@Test
@DisplayName("validateCarCount 메서드는 자동차 개수가 2 미만일 경우 예외를 발생시켜야 한다.")
void validateCarCount_메서드_테스트_실패() {
int carCount = 0;
assertThatThrownBy(() -> {
game.validateCarCount(carCount);
})
.isInstanceOf(RuntimeException.class)
.hasMessageMatching("게임을 진행하려면 자동차가 최소 2대 있어야 합니다.");
}
@Test
@DisplayName("validateTryCount 메서드는 유효한 시도 횟수 입력 시 해당 값을 반환해야 한다.")
void validateTryCount_메서드_테스트_성공() {
int tryCount = 1;
int result = game.validateTryCount(tryCount);
assertThat(result).isEqualTo(1);
}
@Test
@DisplayName("validateTryCount 메서드는 시도 횟수가 1 미만일 경우 예외를 발생시켜야 한다.")
void validateTryCount_메서드_테스트_실패() {
int tryCount = 0;
assertThatThrownBy(() -> {
game.validateTryCount(tryCount);
})
.isInstanceOf(RuntimeException.class)
.hasMessageMatching("게임을 진행하려면 시도 횟수는 1 이상이어야 합니다.");
}
@Test
@DisplayName("자동차 리스트에서 position 최대 값 구하기")
void 자동차_max_메서드_테스트() {
Cars cars = new Cars(List.of(new Car("dong", 3), new Car("duu", 4)));
int max =game.max(cars);
assertThat(max).isEqualTo(4);
}
}
나는 이번 미션에서는 테스트 코드에 익숙해지기 위해서 모든 것들을 테스트하고자 마음을 먹었고 나의 컨셉이였다.
하지만 솔직하게 Controller 비즈니스 로직이 있는거 같아서 조금 더 다시 작성한다면 잘 작성 할 수 있겠다는 생각을 가지며 TestCode를 작성했다.
CarTest
package racing.car.doamin;
import org.junit.jupiter.api.Test;
import racing.car.domain.Car;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class CarTest {
@Test
void 초기값_0_테스트() {
Car car = new Car("seou");
assertThat(car).extracting("position").isEqualTo(0);
}
}
CarNameTest 클래스
package racing.car.model;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racing.car.model.CarName;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CarNameTest {
@Test
@DisplayName("자동차_이름이_5글자_미만_성공")
void 자동차_이름_5글자_미만_성공() {
String name = "leo";
CarName carName = new CarName(name);
assertThat(carName).isEqualTo(new CarName("leo"));
}
@Test
@DisplayName("자동차_이름이_5글자_초과_에러")
void 자동차_이름_5글자_초과_에러() {
assertThatThrownBy(() -> {
new CarName("hanseounggyun");
}).isInstanceOf(RuntimeException.class).hasMessageMatching("자동차 이름이 5자글자를 초과하였습니다.");
}
}
CarsTest
package racing.car.model;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racing.car.domain.Car;
import racing.car.random.Random;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class CarsTest {
@Test
@DisplayName("Cars 리스트 최대값 반환")
void 최대값_반환_테스트() {
Cars cars = new Cars(List.of(new Car("dong", 5), new Car("pling", 4), new Car("iiwq", 3)));
int max = cars.getMaxPosition();
assertThat(max).isEqualTo(5);
}
@Test
@DisplayName("Cars 클래스 addCars 메서드 초기값 선언 테스트")
void 초기값_선언_테스트() {
List<Car> initialCars = new ArrayList<>();
Cars cars = new Cars(initialCars);
cars.addCars(new String[]{"phobi", "covi", "dong"});
assertThat(initialCars).isEqualTo(List.of(new Car("phobi",0), new Car("covi",0), new Car("dong",0)));
}
@Test
@DisplayName("Cars 클래스 move 메서드 테스트")
void move_메서드_테스트(){
List<Car> initialCars = List.of(new Car("phobi", 0), new Car("doung", 0));
Cars cars = new Cars(initialCars);
cars.move(()-> 4);
assertThat(initialCars).isEqualTo(List.of(new Car("phobi", 1), new Car("doung",1)));
}
@Test
@DisplayName("max 최대 값5와 똑같은 우승자 찾기 메서드 테스트")
void find_winner_우승자_찾기(){
List<Car> initialCars = List.of(new Car("phobi", 5), new Car("doung", 3), new Car("bbj", 5), new Car("kim", 4));
int maxPosition = 5;
Cars cars = new Cars(initialCars);
List<Car> winner = cars.findWinner(maxPosition);
assertThat(winner).contains(new Car("phobi", maxPosition), new Car("bbj", maxPosition));
assertThat(winner).hasSize(2);
}
}
PositionTest
package racing.car.model;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class PositionTest {
@Test
@DisplayName("move 메서드는 3 이하인 경우 position은 증가하지 않는다.")
void 앞으로_이동_4이하() {
Position position = new Position();
position.increase(3);
assertThat(position).isEqualTo(new Position(0));
}
@Test
@DisplayName("move 메서드는 4 이상인 경우 position이 1증가 된다.")
void 앞으로_이동_4이상() {
Position position = new Position();
position.increase(4);
assertThat(position).isEqualTo(new Position(1));
}
@Test
@DisplayName("Position 클래스의 position 값을 비교하여 같으면 True, 다르면 False")
void same_메서드_테스트() {
Position position = new Position(1);
assertThat(position.isSame(1)).isTrue();
assertThat(position.isSame(0)).isFalse();
}
@Test
@DisplayName("max 최대값 반환 테스트")
void max_최대값_테스트() {
Position position = new Position(5);
int max = position.max(3);
assertThat(max).isEqualTo(5);
}
}
이렇게 작성하였고, 아직 부족한 것이 많았지만 늘었고, 코드를 작성할 때, 많은 고민을 하면서 작성하게 되었다는 것에 보람을 느끼며 마무리 할 수 있게 되었다.
사실 중간 중간 느낀점은 더 많았던거 같지만.. 여기까지만 작성 다음에는 다시 작성한 RacingCar, Lotto TDD로 돌아올 예정이다.