상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야한다.
재정의 가능한 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야한다.
상속이 캡슐화를 해치기 때문이다.
안전하게 상속할 수 있도록 하려면 내부 구현 방식을 설명해야만 한다.
방법은 없다. 심사숙고하여 직접 하위 클래스를 만들어보는 것이 "유일"하다.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.
public class Super {
// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public class Sub extends Super {
private final Instant instant;
Sub() {
instant = Instant.now();
}
// 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다.
@Override
public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
이 프로그램은 instant를 두 번 출력하는 대신, 첫 번째에서 null을 출력한다.
상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당하다.
일반적인 구체 클래스는 final도 아니고 상속용으로 설계되지도 않았고 문서화되지도 않았다.
따라서 그대로 두면 위험하다. 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 만들 수 있다.
이 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.
첫 번째는 클래스를 final로 선언하는 것이다.
두 번재는 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 것이다.
핵심 기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 개발하는 데 아무런 어려움이 없을 것이다. Set, List, Map이 그 예다. 또는 래퍼 클래스를 이용하는 것도 적절한 대안이 될 수 있다.
클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 문서로 남겨야 한다.
즉, 재정의 가능 메서드를 호출하는 자기사용 코드를 완벽히 제거해야 한다.
이렇게 하면 상속해도 그리 위험하지 않다. 메서드를 재정의해도 다른 메서드의 동작에 아무런 영향을 주지 않기 때문이다.
상속용 클래스를 설계한다면, 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 한다.
일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그러지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다.
다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다.
클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 낫다.
상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.