JAVA/이펙티브자바

이펙티브자바 Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

ghyeong 2024. 10. 7. 23:35
반응형

상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야한다.

  • @implSpec 사용 → 자바독 도구가 생성을 해줌
public class ExtendableClass {

	/**
	* This method can be overridden to print any message
	*
	* @implSpec
	* Please use System.out.println().
	*/
	protected void doSomething() {
		System.out.println("hello");
	}
	
}

문서로 남기는 것 말고도 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected메서드 형태로 공개해야 할 수도 있다

• 훅 : hook: 클래스의 내부 동작 과정 중간에 끼어들 수 있는 코드

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    /**
     * Removes all of the elements from this list (optional operation).
     * The list will be empty after this call returns.
     *
     * @implSpec
     * This implementation calls {@code removeRange(0, size())}.
     *
     * <p>Note that this implementation throws an
     * {@code UnsupportedOperationException} unless {@code remove(int
     * index)} or {@code removeRange(int fromIndex, int toIndex)} is
     * overridden.
     *
     * @throws UnsupportedOperationException if the {@code clear} operation
     *         is not supported by this list
     */
    public void clear() {
        removeRange(0, size());
    }

    /**
     * Removes from this list all of the elements whose index is between
     * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
     * Shifts any succeeding elements to the left (reduces their index).
     * This call shortens the list by {@code (toIndex - fromIndex)} elements.
     * (If {@code toIndex==fromIndex}, this operation has no effect.)
     *
     * <p>This method is called by the {@code clear} operation on this list
     * and its subLists.  Overriding this method to take advantage of
     * the internals of the list implementation can <i>substantially</i>
     * improve the performance of the {@code clear} operation on this list
     * and its subLists.
     *
     * @implSpec
     * This implementation gets a list iterator positioned before
     * {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
     * followed by {@code ListIterator.remove} until the entire range has
     * been removed.  <b>Note: if {@code ListIterator.remove} requires linear
     * time, this implementation requires quadratic time.</b>
     *
     * @param fromIndex index of first element to be removed
     * @param toIndex index after last element to be removed
     */
    protected void removeRange(int fromIndex, int toIndex) {
        ListIterator<E> it = listIterator(fromIndex);
        for (int i = 0, n = toIndex - fromIndex; i < n; i++) {
            it.next();
            it.remove();
        }
    }
}
  • clear()
    • removeRange()를 호출해 index 처음부터 끝까지 삭제
  • removeRange()
    • clear()를 고성능으로 만들기 쉽게 하기 위해 제공
    • 해당 메서드가 없었다면 하위 클래스에서 clear 메서드 호출 시 성능이 느려지거나 새로 구현했어야 함
반응형

어떤 메서드를 protected로 노출해야할지 결정하기

  • 실제 하위 클래스를 만들어 시험해보는 것이 최선이다.
  • protected 메서드는 내부구현에 해당하므로 가능한 적어야한다.
  • 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의해야한다.

상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

  • 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확연히 드러난다.
  • 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 사실 private이었어야 할 가능성이 크다.
  • 하위 클래스는 3개 이상 작성하고 이중 하나는 제 3자가 작성해봐야 한다.
  • 상속용으로 설계한 클래스에 문서화한 내부 사용 패턴, protected 메서드와 필드 구현에 대한 결정들이 클래스의 성능과 기능에 영원한 족쇄가 될 수 있다.

상속용 클래스의 생성자는 재정의 가능 메서드를 호출해서는 안 된다.

  • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
  • 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존하면 의도대로 동작하지 않는다.

상속을 허용해야 한다면

클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남겨야한다.

  1. 각각의 재정의 가능 메서드는 자신의 본문 코드를 private '도우미 메서드(helper method)'로 옮긴다.
  2. 도우미 메서드를 호출하도록 수정한다.
  3. 재정의 가능 메서드를 호출하는 다른 코드들 모두 이 도우미 메서드를 직접 호출하도록 수정한다.
public class Super {
    // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
    public Super() {
        overrideMe();
    }
    
    public void overrideMe() {
    }
}
public final class Sub extends Super {
    // 초기화되지 않은 final 필드, 생성자에서 초기화한다.
    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 를 호출하기 때문

해결방법 → 상속을 금지하도록 한다.

상속을 금지하는 방법

  1. 클래스를 final로 선언
  2. 모든 생성자를 private나 default로 선언 뒤 public 정적 팩토리 생성

정리

클래스를 상속을 할때는 문서화를 하고, 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 낫다



반응형