[리팩토링] 악취 19 : 상속 (1)

 

✍️ 악취 19 : 상속

객체지향에서 상속은 코드 재사용과 기능 확장이라는 장점이 있지만 상속이 적절하지 않은 케이도 분명 있다.
서브 클래스는 슈퍼 클래스의 모든 기능을 지원해야 한다.(List를 상속받는 Stack이 적절한가?)
서브 클래스는 슈퍼 클래스가 가진 기능적 규약을 위반해선 안 된다. (리스코프 치환 원칙)
서브 클래스는 슈퍼 클래스의 변경에 취약한 다르게 말하면 둘의 결합도가 매우 높다.
무엇보다 다중 상속이 불가능하다.

 

상속은 매우 유용한 기능이지만 적절하지 못한 구조라고 판단하면 위임의 형태로 변경하는 것이 유용할 때가 있다.

 

여기 상속 악취를 해결하기 위한 두 가지 리팩토링 기법이 있다.

1. "슈퍼 클래스를 위임으로 변경하기"

2. "서브 클래스를 위임으로 변경하기"

 

본 포스팅에서 관심있게 살펴볼 리팩토링은 "슈퍼 클래스를 위임으로 변경하기"이다.

🍊 슈퍼 클래스를 위임으로 변경하기

서브 클래스는 슈퍼 클래스의 모든 기능을 지원해야 한다. 슈퍼 클래스의 일부 기능만 지원한다면 상속 구조가 올바른지 의심해야 한다. List를 상속받은 Stack 구현이 그 대표적인 예시이다. 

 

심플한 형태의 List 클래스 add, get, remove, isEmpty를 메서드를 지원한다. 

public class SimpleList {

    protected List<Integer> list = new ArrayList<>();

    public boolean add(int value) {
        return list.add(value);
    }

    public int get(int index) {
        return list.get(index);
    }

    public int remove(int index) {
        return list.remove(index);
    }

    public int size() {
        return list.size();
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }
}

 

SimpleList를 상속한 SimpleStack 클래스도 기본적인 연산 push, peek, pop을 제공한다.

public class SimpleStack extends SimpleList {

    public int push(int value) {
        add(value);

        return value;
    }

    public int peek() {
        if (isEmpty())
            throw new RuntimeException();

        return get(size() - 1);
    }

    public int pop() {
        if (super.isEmpty())
            throw new RuntimeException();

        return remove(size() - 1);
    }
}


그렇다면... SimpleStack이 SimpleList를 상속받는 게 올바를까?
Stack은 최상위 엘리먼트를 타깃으로 연산을 하기 때문에 SimpleList의 인덱스 기반 메서드가 온전한 방향으로 사용되고 있다고 보긴 어렵다. 다르게 말하면 슈퍼 클래스의 모든 기능을 지원하지 않는 케이스에 해당하고 러프하게 보면 리스코프 치환 원칙에도 위배된다.

 

이처럼 상속 구조가 올바르지 못한 구조라고 판단이 되면 상속을 '위임'으로 변경할 수 있다.

public class SimpleStack {

    private SimpleList list = new SimpleList();

    public int push(int value) {
        list.add(value);

        return value;
    }

    public int peek() {
        if (list.isEmpty())
            throw new RuntimeException();

        return list.get(size() - 1);
    }

    public int pop() {
        if (list.isEmpty())
            throw new RuntimeException();

        return list.remove(size() - 1);
    }

    public int size() {
        return list.size();
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }
}