ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템 18. 상속보다는 컴포지션을 사용하라
    Study/Effective Java 2023. 7. 29. 14:48

    패키지 경계를 넘어, 즉 다른 패키지의 구체 클래스를 상속하는 일은 위험하다. (인터페이스 상속과는 무관하다.)
    메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 상위클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.

    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;
        }
    
        public static void main(String[] args) {
            InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
            s.addAll(List.of("틱", "탁탁", "펑"));
            System.out.println(s.getAddCount()); // 3이 아닌 6이 출력된다. addAll에서 add를 호출하고 있기 때문이다.
    
            System.out.println(s);
        }
    }

    HashSet의 addAll 메서드가 add 메서드를 사용해 구현되어있기 때문에 예상과는 다른 값이 출력된다.

    ▼  addAll 구현

        public boolean addAll(Collection<? extends E> c) {
            boolean modified = false;
            for (E e : c)
                if (add(e))
                    modified = true;
            return modified;
        }

    클래스를 확장하더라도 메서드를 재정의하는 대신 새로운 메서드를 추가하면 괜찮으리라 생각할 수도 있지만, 다음 릴리스에서 상위 클래스에 추가된 새 메서드가 운이 없게도 하필 내가 추가한 메서드와 시그니처가 같을 수도 있다.

    기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션(Composition; 구성)이라 한다.

    Composition 사용

    전달 메서드(forwarding method) : private 필드로 참조하는 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환하는 메서드.

    // 코드 18-3 재사용할 수 있는 전달 클래스 (118쪽)
    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)           { 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)
                                       { 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(); }
    }
    // 코드 18-2 래퍼 클래스 - 상속 대신 컴포지션을 사용했다. (117-118쪽)
    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());
        }
    }

    다른 Set 인스턴스를 감싸고(wrap) 있다는 뜻에서 래퍼 클래스라 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern) 이라고 한다.

     

    데코레이터 패턴 (Decorator pattern)

    기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴.
    상속이 아닌 위임(Delegation)을 사용하여 보다 유연하게 부가 기능을 추가하는 것도 가능하다.

    위의 예제 코드에서는 Set이 Component, HashSet이 ConcreteComponent, ForwardingSet이 decorator, InstrumentedSet이 Concrete Decorator에 해당한다.

    댓글

Designed by Tistory.