아이템 19. 상속을 고려해 설계하고 문서화하라
1. 상속은 강력하지만 위험하다
상속은 캡슐화를 깨뜨린다.
하위 클래스는 상위 클래스의 구현 세부사항에 의존하기 쉽고,
이는 깨지기 쉬운 구조를 낳는다.
예시) 상위 클래스 내부의 어떤 메서드가 다른 메서드를 호출하는 방식이 바뀌면,
하위 클래스에서 오버라이드한 메서드가 예기치 않게 호출될 수 있다.
2. 상속을 허용하려면, 반드시 문서화하라.
재정의(overriding)를 허용할 메서드는 그 의도를 명확히 문서화해야 한다.
재정의가 가능하다는 의미는 단순히 protected 로 선언하는 것만이 아니다.
재정의 해도 정상적으로 동작할 수 있도록 보장된 계약(contract) 이 있어야 한다.
3. 하위 클래스에서 사용할 수 있도록 설계된 메서드는 확실히 알려야 한다.
재정의 가능한 메서드는 명확한 규약(specification) 과 문서가 필요하다.
그렇지 않으면 하위 클래스는 상위 클래스의 구현을 추측해 사용하게 되고,
상위 클래스가 변경되면 하위 클래스가 깨질 수 있다.
이런 메서드는
재정의용으로 설계된 메서드
여야 하며, 그 목적에 맞게 작성되고 테스트되어야 한다.
4. 생성자에서는 재정의 가능한 메서드를 호출하지 마라.
생성자에서 this 를 참조하는 메서드는 하위 클래스의 오버라이드된 메서드가 먼저 실행될 수 있다.
Sub의 생성자가 실행되기도 전에 overrideMe() 가 호출되면서
date 필드가 초기화되기 전에 접근 --> NullPointerException
5. 상속을 안전하게 지원하려면, 이를 염두에 두고 처음부터 설계해야 한다.
상속 가능한 클래스는 반드시 문서화하고, 테스트 가능한 방법으로 확장 가능하게 설계되어야 한다.
또는 상속을 아예 금지할 수도 있다.
6. 결론
상속은 매우 강력하지만 위험한 도구(강결합)
제대로 설계하고 문서화하지 않으면 깨지기 쉬운 API가 된다.
상속을 허용하려면, 정확한 명세, 계약, 문서화, 테스트가 선행되어야 한다.
대부분의 경우에는 컴포지션을 기본 전략으로 삼고, 정말 필요한 경우에만 원칙(문서화, 계약, ..) 에 따라 상속을 허용해야 한다.
7. 비고) 상속 체크리스트
설계
이 클래스는 다른 곳에서 기능 확장을 위해 상속할 필요가 명확한가?
is-a
관계가 진정으로 성립하는가?상속보다 컴포지션으로 더 좋은 구조는 아닌가?
문서화
오버라이드 할 수 있는 메서드는 사용 방법과 규약(contract) 이 명확히 문서화 되어 있는가?
하위 클래스가 호출할 수 있는 protected 메서드나 생성자는 적절히 설계되었는가?
테스트
오버라이드 가능한 메서드가 호출되는 시점이나 방식에 대해 정확히 테스트 되었는가?
하위 클래스에서 발생할 수 있는 예외적인 케이스도 방어할 수 있는가?
생성자
생성자에서는 재정의 가능한 메서드를 호출하는 부분이 없는가?
초기화 순서 문제로 인해 하위 클래스가 깨질 가능성은 없는가?
느낀점
템플릿 메서드 패턴(Template Method Pattern)
무려 GoF 가 설계한 패턴으로, '템플릿 메서드 패턴'은 상속을 적극 활용하는 디자인 패턴이다.
책에서는 직접적으로 언급하지 않지만, 상속을 허용하고자 할 때는 템플릿 메서드 패턴을 적용하는 것이 한 가지 전략이 될 수 있다.
템플릿 메서드 패턴 : 알고리즘의 구조 (고정된 흐름)는 정해놓고, 하위 클래스에서 특정 단계만 오버라이드하게 유도하는 디자인 패턴
추상 클래스 vs 인터페이스 + 기본 구현 클래스 조합
상속을 허용하고자 할 때, 반드시 추상 클래스를 사용해야 하는 것은 아닌 것 같다.
더 안전하고 유연한 방법은
인터페이스 정의
기본 구현 제공 클래스(composition 사용)
확장 필요 시, 구현 클래스 내에서 delegation 사용
상속보다는 컴포지션을 우선하라..
item 18 의 내용을 최우선으로 생각해야되는 건 맞는 것 같다.
상속을 피할 수 없는 경우, 상속을 위한 원칙을 체크리스트로 만들어 보는 것도 좋을 듯 하다.
kotlin 에서는 이런 원리 원칙을 어떻게 적용할까?
실무에선 상속이 있다면 어떻게 리팩토링 할 수 있을까..?