[리팩토링] 악취 11 : 기본형 집착 (1)

 

✍️ 악취 11 : 기본형 집착

표현하려는 데이터가 심플하다면 언어가 제공하는 기본 타입으로도 충분히 구현할 수 있지만, 표현할 데이터 구조가 복잡해지고 제공할 기능이 복잡해진다면 단순 기본형으로 원활한 기능을 구현하기 어렵다. 가령 단순 화씨온도를 표현하고 싶다면 실수형으로 가능하겠지만 화씨, 섭씨 등 다양한 온도를 표현하고 싶다면 별도의 클래스로 제공하는 것이 더 합리적일 것이다.

 

여기 기본형 집착 악취를 해결하기 위한 두 가지 리팩토링 기법이 있다.

1. "기본형을 객체로 바꾸기" 

2. "조건부 로직을 다형성으로 바꾸기"

 

🍊 기본형을 객체로 바꾸기

개발 초기엔 기본 타입으로 표현한 데이터를 기능이 추가됨에 따라, 구조가 확장됨에 따라 더 다양한 형태로 표현해야 하는 경우가 발생한다. 앞서 언급했던 예시와 동일하게, 화씨로만 표현하던 데이터를 섭씨로도 제공해야 하는 경우가 그러하다.

 

STEP 1 : 문제 파악

@AllArgsConstructor
@Getter
public class Order {

    private String priority;
}
public class OrderProcessor {

    public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.getPriority() == "high")
                .count();
    }
}

위 구조는 크게 두 가지 문제점을 가지고 있다.

1. priority는 type safety가 보장되지 않는다. 막말로 어떤 String 값이 와도 우선순위로 간주된다.

2. 우선순위 계산을 위한 별도의 메서드가 없고 외부에서 계산하고 있다.

 

STEP 2 : Order의 책임으로

priority의 type safety 보장과 우선순위 계산을 Order의 책임으로 구현해 보자.

@Getter
public class Order {

    private static final List<String> LEGAL_PRIORITY = Arrays.asList("low", "normal", "high");

    private String priority;

    public Order(String priority) {
        validate(priority);

        this.priority = priority;
    }

    public boolean higherThan(String priority) {
        validate(priority);

        return index(this.priority) > index(priority);
    }

    private void validate(String priority) {
        if (!LEGAL_PRIORITY.contains(priority))
            throw new IllegalArgumentException(priority);
    }

    private int index(String priority) {
        return LEGAL_PRIORITY.indexOf(priority);
    }

}
public class OrderProcessor {

    public long numberOfHighPriorityOrders(List<Order> orders) {
        return orders.stream()
                .filter(o -> o.higherThan("normal"))
                .count();
    }
}

 

위 구조는 여전히 문제점이 있다. 가장 큰 문제로, Order가 너무 많은 정보를 알고 있고 동시에 너무 많은 일을 처리한다. 가령 Order는 우선순위 검증을 위해 LEAGAL_PRIORITY도 알아야 하고, 우선순위 계산을 위한 메서드를 가지고 있다. 이는 너무 많은 기능이 Order에게 집중되었다고 볼 수 있다.

 

STEP 3 : Priority의 책임으로

기본형이었던 Priority를 클래스로 변경하고 Order에게 부여했던 책임을 Priority로 옮겨보자.

@Getter
public class Priority {

    private static final List<String> LEGAL_PRIORITY = Arrays.asList("low", "normal", "high");

    private String value;

    public Priority(String value) {
        if (!LEGAL_PRIORITY.contains(value))
            throw new IllegalArgumentException(value);

        this.value = value;
    }

    public boolean higherThan(Priority priority) {
        return index(this) > index(priority);
    }

    private int index(Priority priority) {
        return LEGAL_PRIORITY.indexOf(priority.value);
    }

}
@Getter
public class Order {

    private Priority priority;

    public Order(Priority priority) {
        this.priority = priority;
    }

}
public class OrderProcessor {

    public long numberOfHighPriorityOrders(List<Order> orders) {
        Priority normal = new Priority("normal");

        return orders.stream()
                .filter(o -> o.getPriority().higherThan(normal))
                .count();
    }
}