[Effective Java] Item18 - 상속보다는 컴포지션을 사용하라

지구🌍·2023년 2월 25일
0

Effective Java 공부

목록 보기
5/12
post-thumbnail

상속과 컴포지션

💡 상속 : 하위 클래스가 상위 클래스의 특성을 재정의 한 것 (IS-A 관계)

💡 컴포지션 : 기존 클래스가 새로운 클래스의 구성요소가 되는 것 (HAS-A 관계)

상속의 단점

  1. 상속은 캡슐화를 위반한다.
  2. 상속은 설계에 유연하지 못하다.
  3. 상속은 다중 상속이 불가능하다.

상속은 캡슐화를 깨트린다

상위 클래스가 어떻게 구현되어있느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.

예제

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;
    }
}
import java.util.List;

public class Main {
    public static void main(String[] args) {

        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("용", "용용", "용용용"));
        System.out.println(s.getAddCount());
    }
}

HashSetaddAll은 각 원소마다 add를 호출하게 구현되어있기 때문에 기대와 다르게 아래와 같이 6개 출력된다.

재정의한 addAll

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

        for (E e : c) {
            if (add(e)) {
                modified = true;
            }
        }

        return modified;
    }


재정의를 하면 기대한 값이 나올 수 있다.

즉, 상속의 개념은 우리가 편하게 사용하기 위해 등장했지만, 객체지향 프로그래밍의 캡슐화에 위배될 가능성이 있고, 코드의 유연성도 떨어진다!!

컴포지션

그래서 등장한 컴포지션!
기존 클래스를 확장하는 것 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방법

  • forwarding(전달)
    새 클래스의 메소드들이 기존 클래스를 대응하는 메소드들을 호출해 그 결과를 반환하는 것
  • forwarding method (전달 메소드)
    전달을 수행하는 새로운 클래스의 메소드들
  • wrapper class(래퍼 클래스)
    InstrumentedSet와 같이 Set 인스턴스를 감싸고 기능을 덧씌운다는 뜻

HashSet 클래스는 기능을 규정하는 Set 인터페이스가 있으므로 CustomHashSet 클래스를 Forwarding 방식으로 구현할 수 있다.

예제

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public class ForwardingSet<E> implements Set<E> {

    // 기존 클래스를 Private 인스턴스로 선언
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    // Set methods -> 기존 클래스에 대응하는 메서드를 호출
    @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(); }
}
import java.util.Collection;
import java.util.Set;

public class CustomHashSet<E> extends ForwardingSet<E> {

    private int addCount = 0;

    public CustomHashSet(Set 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 class Main {
    public static void main(String[] args) {

        CustomHashSet<String> customHashSet = new CustomHashSet<>(new HashSet());
        List<String> test = Arrays.asList("ㅎ", "ㅋ", "ㅗ");
        customHashSet.addAll(test);

        System.out.println(customHashSet.getAddCount());
    }
}

Set인터페이스를 구현하는 전달 클래스를 상속하며 CustomHashSet클래스는 Set인터페이스를 구현하며 Set 객체를 인자로 받는 생성자를 갖고 있다.

CustomHashSet 클래스는 어떤 Set 객체를 인자로 받아, 필요한 기능을 가지고있는 Set 객체로 변환시켜주는 역할을 한다. 이러한 클래스를 래퍼(Wrapper) 클래스라고 한다. 다른 Set 객체를 포장하고 있다라는 의미이다.

ForwardingSet클래스에서는 private final 필드로 Set 객체를 생성하였다.

컴포지션의 장단점

장점

  1. 한 번만 구현해두면 사용한 인터페이스의 어떠한 구현체에도 적용이 가능하다.
  2. 기존 클래스 내부 구현방식의 영향을 벗어나며, 기존 클래스의 새로운 메소드가 추가되더라도 전혀 영향 받지 않는다.

단점

  1. 구체 클래스 각각 따로 확장해야하며, 지원하고 싶은 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의 해줘야한다.
  2. 콜백(callback) 프레임 워크와 어울리지 않는다.
  • 상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황 에서만 사용해야한다.
  • 클래스 간에 IS-A 관계일 경우에만 사용해야한다.

🎈귀중한 참고자료🎈
참고자료1
참고자료2

profile
일취월장 하며 성장! 중! 공부한 것을 기록하자(^∀^●)ノシ

0개의 댓글