11. 합성과 유연한 설계
상속과 합성의 비교 1
상속 | 합성 |
---|---|
컴파일 타임에 의존성 해결 | 두 객체 사이의 의존성은 런타임에 해결 |
is-a | has-a |
부모클래스의 구현에 의존 | 퍼블릭인터페이스에 의존 |
부모클래스 안에 구현된 코드 재사용 | 객체의 퍼블릭 인터페이스 재사용 |
상속과 합성의 비교 2
같은 요구사항을 상속과 합성을 사용해서 각각 구현한 사례를 보고 비교해 보자. 우선 요구사항은 아래와 같다.
- 기본 정책의 계산 결과에 적용된다.
- 세금 정책은 기본 정책인 RegularPhone이나 NightlyDiscountPhone의 계산이 끝난 결과에 세금을 부과한다.
- 선택적으로 적용할 수 있다.
- 기본 정책의 계산 결과에 세금 정책을 적용할 수도 있고 적용하지 않을 수도 있다.
- 조합 가능하다.
- 기본 정책에 세금 정책만 적용하는 것도 가능하고, 기본 요금 할인정책만 적용하는 것도 가능하다. 또 한 세금 정책과 기본 요금 할인 정책을 함께 적용하는것도 가능헤야 한다.
- 부가 정책은 임의의 순서로 적용 가능하다.
- 기보 정책에 세금 정채과 기본 요금 할인 정책을 함게 적용할 경우 세금 정책을 적용한 후에 기본 요금 할인 정책을 적용할 수도 있고, 기본 요금 할인 정책을 적용한 후에 세금 정책을 적용할 수도 있다.
두 가지 기본 정책이 있고, 두 가지 부가 정책이 있다.
그리고 이 정책들을 조합하면 다음과 같은 경우의 수가 나온다.
상속을 통한 구현
상속관계는 컴파일 타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에는 변경할 수 없다. 따라서 요금과 관련된 모든 기본 정책과 부가 정책의 조합 가능한 경우의 수를 각각 하나의 클래스로 구현하였다. 10가지의 종류가 있었고 10개의 클래스가 생성되었다. 여기서 요금정책의 수가 늘어난다면 모든 조합 가능한 경우를 따져 각각에 해당하는 클래스를 만들어야 하며 클래스의 개수는 굉장히 많이 증가하게 될것이다.
합성을 통한 구현
상속과는 달리 합성은 런타임에 객체간 관계를 변경할 수 있다.
우선 요금 정책 인토페이스를 만들고 이를 구현한 BasicRatePolict와 AdditionallRatePolicy 추상클래스를 만든다. 그리고 BasicRatePolict와 AdditionallRatePolicy를 상속받아 최종적으로 요금제 클래스를 만든다. 새로운 고정 요금제가 필요하다면 BasicRateePolicy를 상속받아 클래스를 만들면 되고, 새로운 부가정책이 필요하다면 AdditionalRatePolicy를 상속받아 클래스를 만들면 된다.
객체 합성이 클래스 상속보다 좋은 방법이다.
객체지향에서 코드를 재사용하기 위하여 상속을 주로 사용하지만(클래스를 상속받아 사용하는 경우), 이는 그리 우아한 방법은 아니다. 상속은 부모 클래스의 세부적인 구현에 자식클래스를 강하게 결합시키기 때문에 코드의 진화를 방해한다. 코드를 재사용하면서도 건전한 결합도를 유지할 수 있는 더 좋은 방법은 합성을 이용하는 것이다.
상속을 합성으로 변경
불필요한 인터페이스 상속 문
합성을 사용한다면 Stack의 퍼블릭 인터페이스에 불필요한 Vector 인터페이스가 포함되지 않음. 마지막 요소만 추가/삭제할 수 있다는 Stack의 규칙을 어길 수 없게 됨. 합성 관계로 변경하여 클라이언트가 Stack을 잘못 사용할 수도 있다는 가능성을 제거.
상속
public class Stack<E> extends Vector<E> { public Stack() { } ... public E push(E item) { addElement(item); return item; } public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } ... }
합성
public class Stack<E>{ private Vector<E> elements = new Vector<>(); public E push(E item){ elements.addElement(item); return item; } public E pop(){ if(elements.isEmpty()){ throw new EmptyStackException(); } return elements.remove(elements.size() - 1)l } }
메서드 오버라이딩의 오작용 문제
InstrumentedHashSet에 HashSet을 합성하여 HashSet에 대한 구현 결합도는 제거하면서도 Set 인터페이스를 구현해 퍼블릭 인터페이스를 그대로 유지.
상속
public class InstrumentedHashSet<E> extends HashSet<E>{ private int addCount = 0; @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; } } InstrumentedHashSet<String> languages = new InstrumentedHashSet<>(); languages.addAll(Arrays.asList("Java", "Ruby", "Scala")); System.out.println(languages.getAddCount();// 3을 예상하지만 6
합성
public class InstrumentedHashSet<E> implements Set<E>{ private int addCount = 0; private Set<E> set; public InstrumentedHashSet(Set<E> set){ this.set = set; } @Override public boolean add(E e){ addCount++; return set.add(e); } @Override public boolean addAll(Collection<? extends E> c){ addCount += c.size(); return set.addAll(c); } public int getAddCount(){ return addCount; } ... @Override public boolean remove(Object o){ return set.remove(o);} @Override public void clear() { set.clear(); } ... } InstrumentedHashSet<String> languages = new InstrumentedHashSet<>(); languages.addAll(Arrays.asList("Java", "Ruby", "Scala")); System.out.println(languages.getAddCount();// 예상 대로 6
부모클래스와 자식클래스의 동시 수정 문제
합성을 사용한다고 하더라도 부모 클래스와 자식클래스를 동시에 수정해야하는 문제가 해결되지는 않는다. 그렇지만 향후에 Playlist클래스 내부 구현을 변경하더라도 파급효과를 최대한 PersonalPlaylist 내부로 캡슐화할 수 있기 때문에 상속보다는 합성을 사용하는 것이 좋다.
상속
Playlist에서 singer정보도 관리하도록 변경이 된다면 PersonalPlaylist의 remove도 singer정보가 제거되도록 함께 수정되어야 한다.public class PersonalPlaylist extends Playlist{ ... public remove(Song song){ getTracks().remove(song); getSingers().remove(song.getSinger()); } ... }
합성
public class PersonalPlaylist{ private PlayList playlist = new Playlist(); public void append(Song song){ playlist.append(song); } public void remove(Song song){ playlist.getTracks().remove(song); playlist.getSingers().remove(song.getSinger()); } }