# ITEM18 상속보다는 컴포지션을 사용하라
이번 장에서 논하는 문제는, 인터페이스 상속과는 무관함.
# 불안전한 상속
- 항상 재사용의 최선은 아니다 → 컴포지션 사용!
- 오류를 내기 쉬운 소프트웨어를 만들게 된다.
- 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.
# 안전한 상속
- 코드를 재사용하는 강력한 수단이다
- 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는, 패키지 안에서라면 안전한 방법이다
- 확장 목적으로 설계 + 문서화도 잘된 클래스
# 불안전한 상속의 문제점
# 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
상위클래스의 구현 → 하위 클래스의 동작에 영향
상위클래스의 변화
- 릴리스에 따라 내부구현이 달라질수 있음
- 하위클래스는 상위클래스에 맞춰 수정되어야함.
# 상속을 잘못 사용한 예
InstrumentedHashSet<E>
클래스는HasSet<E>
를 확장한다.- HashSet 의 성능을 높이기 위해 처음 생성 이후 원소가 몇개 더해졌는지 (증가만 하는 수) 알수 있어야함
- add, addAll 을 재정의 하였다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() { }
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));
s.getAddCount(); // 6을 반환
1
2
3
2
3
- HashSet 의 addAll 메서드가 add 메서드를 사용해 구현됨 → 6 출력
- 이런 내부구현방식은 HashSet 문서에 쓰여있지 않다.
- 자기 사용 self-use 여부는 해당 클래스의 내부 구현 방식이다.
- 내부 구현 방식은 다음 릴리스에서도 유지될 지 알 수 없다.
문제해결
- addAll 메서드를 재정의 하지 않는다.
- 하위 클래스에서 메서드를 재정의 하는 대신, 새로운 메서드를 추가하는 일도 위험할 수 있다.
- 다음 릴리스에서 상위클래스에 새 메서드가 추가 될 때,
- 하위 클래스에서 정의한 메서드와 시그니처가 동일하고, 반환값이 다른 메서드를 정의할 수 있다.
# 래퍼 클래스 - 상속 대신 컴포지션 사용
# 컴포지션 Composition; 구성
- 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 의미
- 기존 클래스를 확장하는 대신,
- 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.
# 전달 forwarding
새 클래스의 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환함.
# 전달 메서드 forwarding method
새 클래스의 메서드들
# 래퍼 클래스
기본 자료타입(primitive type)을 객체로 다루기 위해서 사용하는 클래스
# 데코레이터 패턴
구조를 변경하지 않고 기존 개체에 새로운 기능을 추가 할 수 있음 → 래퍼 클래스
# 컴포지션 + 전달 → 위임 delegation
래퍼 객체가 내부 객체에 자신의 참조를 넘기는 경우
class In {
private final int in = 0;
public int add(int num) { return num + 1; }
}
class Wrapper {
private final In in = new In(); // 내부 객체
private final int num = 1;
private int call() { // 전달 메서드
in.add(num); // 전달
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 특징
- 새로운 클래스는 기존 클래스의 내부 구현방식에 영향을 받지 않는다.
- 전달메서드의 성능에 주는 영향/ 메모리 사용량은 별다른 영향이 없다.
# 래퍼 클래스 - 상속대신 컴포지션 사용
public class InstrumentSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 재사용 할 수 있는 전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
// ...
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 래퍼 클래스는 콜백 프레임워크와는 어울리지 않는다.
# 콜백 프레임워크
자신의 참조를 다른 객체에 넘겨서 다음 호출 콜백 때 사용하도록 한다.
# SELF 문제
- 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 this (자신)의 참조를 넘긴다.
- 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다.
# 상속을 써야 하는 상황
반드시 하위 클래스가 상위 클래스의 진짜 하위타입인 상황
- is-a 관계
- 상속 → 분류
# 위반한 클래스
- Stack 은 Vector 가 아니지만 확장하고 있다
- Properties 도 HashTable 을 확장해선 안됐다.
# 컴포지션 대신 상속을 사용하기로 결정하기 전 마지막 자문
- 확장하려는 클래스의 API 에 아무런 결함이 없는가
- 결함이 우리의 클래스 API 까지 전파되어도 되는가