Study/Effective-Java

[item#18] 상속보다는 컴포지션을 사용하라

hongeeii 2023. 11. 29.
728x90
반응형

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
    }
}

image

발생하는 문제!

  1. override한 addAll을 호출했을때 6이 나온다: 상위 클래스의 addAll에서도 count를 더하는 loginc이 있나? => 내부 구현 노출(element마다 add메서드 호출)

  2. 상위 클래스의 addAll이 뭔지 알아야 재대로 구현을 할 수있다.

  3. 상위 클래스의 구현이 바뀐다면? => 모든 하위 클래스의 구현이 바뀌어야 한다.

  4. HashSet에 새로운 필드나 메서드가 추가 될 수 있다. => 그런 사실을 인지할 수 못할 수 있다. 구현된 메서드가 오동작 할 가능성 있음.

  5. 만약 상위 클래스에서 하위클래스에서 새로 만든 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());
    }
}

장점

  1. ForwardingSet에 HashSet을 넣어줘서 HashSet을가지고 Forwarding하는 Operation을 사용할수 있다. => side-effect가 발생하지 않는다.
  2. 아무리 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();
        
 뭐 좀 하다가...
밥을 먹을까...

위에서 '커피도 마실까'는 출력되지 않음! 왜? ==>
image
자기자신(this)을 넘기기 때문에 wrapper의 call이 아닌 BobFunction의 call이 실행되기 때문

728x90
반응형

추천 글