이펙티브 자바18- 상속보다는 컴포지션을 사용

한주영·2023년 11월 8일
0

이펙티브자바

목록 보기
13/33

상속보다는 컴포지션을 사용하라

1.메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
=>상위클래스가 어떻게 구현되느냐에따라 하위클래스의 동작에 이상이 생길수 있음.

상속의잘못된 예

import java.util.Collection;
import java.util.HashSet;

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

getAddCount메서드를 호출하면 3을 반환하리라 기대하지만 실제로는 6을 반환한다.HashSet의 addAll은 각 원소를 add메서드 호출을 추가해주는데 여기에서 쓰이는 add는 InstrumentedHashSet에서 재정의되었기 때문

addCount에서 값이 중복해서 더해져서 최종값이6으로 늘어났기때문이다.

이 경우 하위클래스에서 addAll메서드를 재정의하지않으면 문제를 고칠수있어도, HashSet의 addAll이 add메서드를 이용해 구현했음을 가장한 해법이라는 한계를 지니는데 이를 자기사용 여부 라고 부르고 해당 클래스의 내부구현방식에 해당하며 , 그다음 릴리스에서도 유지될수는 알수없다

addAll메서드를 다른식으로도 재정의 가능
주어진컬렉션을 순회하며 원소 하나당 add를 한번만 호출
해당 방법도 조금은 나은해법이지만 여전히 문제는 남는다.

기존클래스를 확장하는 대신 새로운 클래스를 만들고 private필드로 기존 클래스의 인스턴스를 참조하게 하는것을 컴포지션 이라고 함

새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해서 그결과를 반환한다 이를 전달 이라고 부른다
새클래스의 메서드들을 전달 메서드라고 부름

상속대신 컴포지션을 사용한 예시코드

public class InstrumentedHashSet<E> extends HashSet<E> {

    private int addCount=0;

    public InstrumentedHashSet(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 class ForwardingSet<E> implements Set<E> {

    private final Set<E> s;
    public ForwardingSet(Set<E> s){
        this.s=s;
    }
    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public boolean contains(Object o) {
        return false;
    }

    @Override
    public Iterator<E> iterator() {
        return null;
    }

    @Override
    public Object[] toArray() {
        return new Object[0];
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return null;
    }

    @Override
    public boolean add(E e) {
        return false;
    }

    @Override
    public boolean remove(Object o) {
        return false;
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return false;
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        return false;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return false;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return false;
    }

    @Override
    public void clear() {
          s.clear();
    }
}

Set인터페이스를 구현했고 인스턴스를 인수로 받는 생성자를 하나 제공
임의의 Set계측 기능을 덧씌워 새로운Set으로 만드는것이 핵심

한번구현해두면 어떠한 Set구현체라도 계측할수있으며
,기존생성자들과도 함께사용할수있다.

위 예시코드와(InstrumentedSet)같은 클래스를 래퍼 클래스라고 부름.
다른Set에 계측기능을 덧씌운다는 뜻에서 데코레이터패턴 이라고 한다.

self문제
콜백 프레임워크에서 자기 자신의 참조를 다른 객체에 넘겨 다음호출때 사용하도록 하고 , 내부 객체는 자신을 감싸고있는 래퍼의 존재를 모르기때문에 this 참조를 넘기고 콜백 때는 래퍼클래스가 아닌 내부 객체를 호출하게된다.

  1. 상속은 반드시 하위클래스가 상위 클래스의 진짜 하위타입인 상황에서만 쓰여아한다.
    ->클래스B가 클래스A와 is-a관계일때만 A를 상속해야함

3.컴포지션을 써야할 상황에서 상속을 사용하는 것은 내부 구현화를 불필요하게 노출하게됨으로 지양해야함.

profile
백엔드개발자가 되고싶은 코린이:)

0개의 댓글