이펙티브 자바 아이템 18 - 상속보다는 컴포지션을 사용하라 - 완벽 공략
아이템 18 - 상속보다는 컴포지션을 사용하라 - 완벽 공략
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
데코레이터 패턴
데코레이터 패턴은 기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴 이다.
데코레이터는 상속이 아니라 위임 을 사용하는 대표적인 디자인 패턴 중 하나이다.
굳이 새로운 클래스를 만들 필요없이 기존의 클래스를 조합해 새로운 인스턴스를 만들 수 있다.
단점으로는 조합을 하는 코드가 복잡해 질 수 있다.
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); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) {
System.out.println("?????");
return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{
System.out.println("?AWDASD");
return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
ForwardingSet 클래스는 데코레이터 역활을 한다.
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("틱", "탁탁", "펑"));
System.out.println(s.getAddCount());
}
}
InstrumentedSet 클래스는 Concrete 데코레이터 역할을 한다.
HashSet 을 감싸고 있으므로 HashSet 이 Concrete 컴포넌트가 된다.
콜백 프레임워크와 셀프 문제
콜백이란 어떤 함수에 다른 함수를 인자로 전달하는 걸 말한다.
interface FunctionToCall {
void call();
void run();
}
class BobFunction implements FunctionToCall {
private final Service service;
public BobFunction(Service service) {
this.service = service;
}
@Override
public void call() {
System.out.println("밥을 먹을까..");
}
@Override
public void run() {
this.service.run(this);
}
}
BobFunction 이라는 클래스의 run
메서드에서 어떠한 객체를 넘긴다.
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 클래스에서 필요한 순간에 넘겨진 객체를 호출해서 사용한다.
이게 콜백이다.
비동기적으로 함수를 호출하는데 사용된다.
지금까지의 코드에서는 잘 동작하는 것처럼 보이지만 Wrapper 와 같이 사용하게되면 셀프 문제 가 발생한다.
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();
}
}
BobFunctionWrapper 클래스는 메서드들을 bobFunction 에 위임한다.
결과에서는 우리가 기대했던 결과가 나오지 않는다.
@Override
public void run() {
this.bobFunction.run();
}
BobFunctionWrapper 클래스에서 호출하는 run 은 bobFunction 의 run()
이다.
// BobFunction
@Override
public void run() {
this.service.run(this);
}
이때 넘겨주게되는 this
는 BobFunction 이다.
때문에 우리가 기대한 BobFunctionWrapper 의 call()
을 호출한게 아닌 BobFunction 클래스의 call()
을 호출하게된다.
이래서 셀프 문제 라고 한다.
wrapper 클래스를 콜백 프레임워크와 같이 사용했을 때 발생 할 수 있는 문제이다.