[item#18] 상속보다는 컴포지션을 사용하라
interface를 구현 or 상속한는 것이 아닌 구체적인 class를 상속하는 class의 문제점을 말하고 있음.
- 패키지 경계를 넘어 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.
- 상위 클래스에서 제공하는 메서드의 구현이 바뀐다면...
- 상위 클래스에서 새로운 메서드가 생긴다면...
- 컴포지션 (Composition)
- 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조
- 새 클래스의 인스턴스 메서드들은 기존 클래스에 대응하는 메서드를 호출해 그 결과를 반환한다.
- 기존 클래스의 구현이 바뀌거나, 새로운 메서드가 생기더라도 아무런 영향을 받지 않음!
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
// 잘못된 예 - 상속을 잘못 사용함
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 element) {
addCount++;
return super.add(element);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "꽁"));
System.out.println(s.getAddCount()); // 결과 값 : 6
}
}
발생하는 문제!
override한 addAll을 호출했을때 6이 나온다: 상위 클래스의 addAll에서도 count를 더하는 loginc이 있나? => 내부 구현 노출(element마다 add메서드 호출)
상위 클래스의 addAll이 뭔지 알아야 재대로 구현을 할 수있다.
상위 클래스의 구현이 바뀐다면? => 모든 하위 클래스의 구현이 바뀌어야 한다.
HashSet에 새로운 필드나 메서드가 추가 될 수 있다. => 그런 사실을 인지할 수 못할 수 있다. 구현된 메서드가 오동작 할 가능성 있음.
만약 상위 클래스에서 하위클래스에서 새로 만든 getAddCount() 메서드를 새롭게 구현하는 일이 벌어진다면? => 코드가 깨짐
5-1. 예를 들어 getAddCount() 메서드를 하위 클래스에서 private으로 선언을 했을 때 상위 클래스가 그보다 넓은 접근지시자로 구현을 해버리는 경우
=> 캡슐화가 깨짐
Composition
기존 클래스를 확장하는 것이 아니라 재사용하고 싶은 기능들을 모두 가지고 있는 클래스를 private feild로 정의를 하는 것.
// 재사용 할 수 있는 전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {this.s = s;}
@Override
public int size() {return s.size();}
@Override
public boolean isEmpty() {return s.isEmpty();}
@Override
public boolean contains(Object o) {return s.contains(o);}
@Override
public Iterator<E> iterator() {return s.iterator();}
@Override
public Object[] toArray() { return s.toArray();}
@Override
public <T> T[] toArray(T[] a) { return s.toArray(a);}
@Override
public boolean add(E e) { return s.add(e);}
@Override
public boolean remove(Object o) { return s.remove(o); }
@Override
public boolean containsAll(Collection<?> c) { return s.containsAll(c);}
@Override
public boolean addAll(Collection<? extends E> c) {return s.addAll(c); }
@Override
public boolean retainAll(Collection<?> c) { return s.retainAll(c);}
@Override
public boolean removeAll(Collection<?> c) {return s.removeAll(c); }
@Override
public void clear() {
s.clear();
}
}
// 래퍼 클래스 - 상속 대신 컴포지션을 사용
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(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);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
s.addAll(List.of("틱", "탁탁", "꽁")); // 결과 : 3
System.out.println(s.getAddCount());
}
}
장점
- ForwardingSet에 HashSet을 넣어줘서 HashSet을가지고 Forwarding하는 Operation을 사용할수 있다. => side-effect가 발생하지 않는다.
- 아무리 HashSet이 가지고 있는 구현이 바뀐다고 하더라도 안정적으로 동작함
콜백 프레임워크와 래퍼를 같이 사용했을 때 발생할 수 있는 문제
- 콜백함수 : 다른 함수(A)의 인자로 전달된 함수(B)로, 해당 함수(A)의 내부에서 필요한 시점에 호출 될 수있는 함수(B)를 말함
- 래퍼로 감싸고 있는 내부 객체가 어떤 클래스(A)의 콜백으로(B) 사용되는 경우에
this
를 전달한다면 해당 클래스 A는 래퍼가 아닌 내부 객체를 호출한다.(SELF 문제)
public interface FunctionToCall {
public void call();
public void run();
}
public class BobFunction implements FunctionToCall {
private final Service service;
public BobFunction(Service service) {
this.service = service;
}
public Service getService() {
return service;
}
@Override
public void call() {
System.out.println("밥을 먹을까...");
}
@Override
public void run() {
this.service.run(this);
}
}
public class Service {
public void run(FunctionToCall functionToCall) {
System.out.println("뭐 좀 하다가...");
functionToCall.call();
}
public static void main(String[] args) {
Service service = new Service();
BobFunction bobFunction = new BobFunction(service);
bobFunction.run();
// Service service = new Service();
// BobFunction bobFunction = new BobFunction(service);
// BobFunctionWrapper bobFunctionWrapper = new BobFunctionWrapper(bobFunction);
// bobFunctionWrapper.run();
}
}
뭐 좀 하다가...
밥을 먹을까...
Wrapper Class
public class BobFunctionWrapper implements FunctionToCall {
private final BobFunction bobFunction;
public BobFunctionWrapper(BobFunction bobFunction) {
this.bobFunction = bobFunction;
}
@Override
public void call() {
this.bobFunction.call();
System.out.println("커피도 마실까...");
}
@Override
public void run() {
this.bobFunction.run();
}
}
Service service = new Service();
BobFunction bobFunction = new BobFunction(service);
BobFunctionWrapper bobFunctionWrapper = new BobFunctionWrapper(bobFunction);
bobFunctionWrapper.run();
뭐 좀 하다가...
밥을 먹을까...
위에서 '커피도 마실까'는 출력되지 않음! 왜? ==>
자기자신(this)을 넘기기 때문에 wrapper의 call이 아닌 BobFunction의 call이 실행되기 때문
'Study > Effective-Java' 카테고리의 다른 글
[item#20] 추상 클래스보다는 인터페이스를 우선하라 (0) | 2023.11.29 |
---|---|
[item#19] 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) | 2023.11.29 |
[item#17] 변경 가능성을 최소화하라 (1) | 2023.11.29 |
[item#16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2023.11.29 |
[item#15] 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.11.29 |
댓글