본격 자동차 경주 미션을 시작했다. 코드를 작성할 때 매번 순차적으로 로직을 구현하게 되어 이를 객체지향적으로, 테스트 코드에 유연하게 작성하도록 많이 고민해야한다는 것을 느꼈다.
개발 전에 요구사항 목록을 적어줘야 한다. 얼마든지 개발하는동안 변경되기 때문에 계속 계속 업데이트하기.
이때 예외 사항도 같이 꾸준히 업데이트하면서 이에 따라 클래스 설계와 개발이 진행되어야한다..!
코딩컨벤션에 맞춰 구현을 하는 것이 생각보다 많이 어려운 것을 느꼈다. 놓치게 되는 부분들을 메모해놓고 항상 보면서 습관을 들이자
코딩 컨벤션 일일이 맞추는 게 생각보다 힘들었는데, intelliJ의 포맷을 맞춰주는 기능이 있었다.
단축키 command + shift + option + L
추가적으로 생성자, equals And hashCode 생성 등등 : command + n
이번 미션에서는 MVC 패턴을 사용하여 패키지를 구분했다.
입출력을 담당하는 부분(view), 비즈니스로직 담당과 도메인 부분(model), 모델과 뷰를 연결하는 부분(controller)으로 나누었다.
각 패키지들의 역할이 분리될 수 있고
단일 책임의 원칙도 지키게 된다.
그리고 domain 영역에 대한 부분만 단위 테스트가 가능하기 때문에 사용하게 되었다.
View 패키지에서는 데이터를 꺼내야하니 getter를 사용할 수 밖에 없지만,
도메인에서는 객체를 객체스럽게 사용하기 위해서 getter를 사용하지 말아야한다.
만약에 getter를 계속해서 쓰다보면 객체가 스스로 작업을 하지 않고 외부에서 작업이 일어나기 때문에 무분별한 사용을 하지 말자.
즉, 상태 데이터를 꺼내 로직을 처리하도록 하지 말고 객체 자체에 메시지를 보내 로직을 처리하자..!
public void move(Movable movable)
{
if (movable.moveOrNot()) {
position++;
}
}
자동차를 움직이는 코드가 있다고할 때, 데이터를 꺼내 로직을 처리한다면 getter로 position 데이터를 꺼내 position을 증가시킬 수도 있다.
하지만, move라는 메서드로 Car 객체 자체에 move()라는 메시지를 보내면 데이터를 꺼내지 않고도 객체 자체가 스스로 작업을 하게 가능하다.
이번 과제 전에 '객체지향 생활 체조'와 'SOLID 원칙'을 알게되었다. 그 중 과제를 하면서 많이 어렵고 와닿은 규칙이 있었는데
사실 car 클래스를 처음 작성할 때는 이 책임이 모호해져서 car의 움직임, 움직임 결정 까지 모두 car 클래스에서 담당하도록 했다.
public class Car {
private int position;
public void decideMovable() {
RandomMovable randomMovableImple = new RandomMovable();
int randomValue = randomMovableImple.makeRandomValue();
if (randomMovableImple.MoveOrNot(randomValue)) {
move();
}
}
...
}
그런데, 코드리뷰를 통해 car가 움직일지 말지를 결정하는 것은 car의 역할이 아니란 것을 배웠고 그에 맞춰 리팩토링을 진행했다.
car는 움직인다는 move()만 필요하지 움직임을 결정하는 것은 외부에서 할 일이다.
(실제로도 자동차의 움직임을 결정하는 것은 운전자의 역할이다)
모든 car들을 멈춰야하는 상황의 경우 위와 같이 코드를 작성하면 decideMovable() 메서드 외에 자동차를 멈추기만하는 메서드인 stop()를 따로 작성해야 할 듯하다.
그러니 외부에서 움직임 여부를 관리한다면 자동차는 단지 그 움직임 여부에 따라 이동하기만 하면 된다.
그래서 위와 같은 코드를 아래와 같이 고쳤다.
//CarRacingInformation 클래스
public void decideMovableByRandomValue(Car car) {
randomMovable.makeRandomValue();
car.move(randomMovable);
}
//Car 클래스
public void move(Movable movable)
{
if (movable.moveOrNot()) {
position++;
}
단지 car는 움직임 여부를 받아서 이동만 가능하게 된다.
테스트 코드를 작성하는 데에 있어 어떤 것을 테스트해야하는지 기준이 너무 애매했다.
객체 생성까지 테스트 해야하는 건지, 메서드 하나하나 다 테스트해야하는건지..
리뷰어님에게 물어보니 기능 단위로 테스트 하는 것이 좋다고 한다.
이때 기능단위로 method 단위를 의미하는 것..!
만약 method 단위로 테스트 작성이 어렵다면 테스트 하고자 하는 method가 너무 많은 일을 하고 있을 가능성이 있다. 그럴 땐 method를 분리하자..!
car 클래스의 move()가 만약 아래와 같다면 테스트 코드 작성이 더욱 길어지고 힘들어진다.
public void move() {
if (getRandomNo() >= 4) {
this.position++;
}
}
이를 해결하기 위해선 테스트 가능한 코드와 테스트 하기 힘든 부분을 분리하자.
getRandomNo대신 number를 인자로 받아 테스트 가능한 부분만 메서드에 남기자
public void move(int number) {
if (number >= 4) {
position++;
}
}
이것도 좋지만 인터페이스를 통해 해결하는 방법도 있다.
public interface Movable {
boolean moveOrNot();
}
//car 클래스
public void move(Movable movable)
{
if (movable.moveOrNot()) {
position++;
}
}
//테스트 코드
@Test
@DisplayName("자동차 전진")
public void moveCar() {
Car car = new Car("one");
car.move(() -> true);
assertEquals(1,car.getPoisition());
}
인터페이스를 사용하면 테스트 코드 작성시에 훨씬 가독성있는 코드가 된다.
익명 클래스나 람다로 바로 값을 오버라이드하기 때문에 좋다.