: 객체지향의 가장 중요한 요소 중 하나인 상속에 관련된 리팩터링들. 대부분의 경우 상속을 잘못 사용하기 쉽다.
1. 메서드 올리기
: 두 서브클래스의 메서드가 같은 일을 수행한다면 슈퍼클래스의 공통된 메서드로 올려 중복을 제거한다.
- 보통 함수 매개변수화(리터럴을 매개변수로 받기)를 수행한 후에 수행이 가능하기도 하다.
- 서브클래스의 필드가 필요한 상황이라면 먼저 필드 올리기를 수행한다.
- 두 메서드의 세부 내용이 다르다면 템플릿 메서드를 고려한다.
2. 필드 올리기
: 같은 역할을 수행하는 필드들을 슈퍼 클래스로 올려 중복을 제거한다.
3. 생성자 본문 올리기
: 생성자에서 공통된 부분을 슈퍼 클래스로 올려 중복을 제거한다. 만약 서브 클래스에서 값을 세팅한 후에 처리해야하는 중복 로직이라면 그 부분을 함수로 추출하고, 슈퍼 클래스로 올린다.
4. 메서드 내리기
: 특정 서브클래스 하나와만 관련된 메서드는 슈퍼클래스에서 서브클래스로 내린다.
- 어떤 서브클래스가 해당 메서드를 구현해야하는지 정확히 알아야 그 클래스에만 해당 메서드를 구현할 수 있다.
- 그렇지 않으면 다형성(모든 곳에 구현)으로 구현해야한다.
5. 필드 내리기
: 특정 서브클래스에서만 사용되는 필드는 해당 서브클래스로 옮긴다.
6. 타입 코드를 서브클래스로 바꾸기
: 타입 코드 필드로 대상을 분류해야 할 경우가 있다. 그런데 이 대상들에게 타입을 통한 구분 이상의 것이 필요하다면 이 리팩터링을 수행하면 된다.
- 다형성이 필요한 경우
- 특정 타입에서만 필요한 필드나 메서드가 있는 경우
7. 서브클래스 제거하기
: 더이상 서브클래스가 서브클래스의 역할을 잘 수행하지 못한다고 판단되면 슈퍼클래스의 필드로 대체해 제거한다.
8. 슈퍼클래스 추출하기
: 비슷한 일을 수행하는 두 클래스가 보이면 비슷한 부분을 공통의 슈퍼 클래스로 옮긴다.
- 객체지향의 상속은 현실세계의 분류체계로도 힌트가 되지만, 그렇지만은 않은 공통 요소가 발견되었을 때도 상속이 적용되기도 한다.
9. 계층 합치기
: 상속 구조가 시간이 지나면서 슈퍼, 서브클래스간 큰 차이가 없다면 둘을 하나로 합친다.
10. 서브클래스를 위임으로 바꾸기
: 서브클래스를 위임클래스로 바꾸고, 슈퍼 클래스에서 전달하도록 리팩터링한다.
위임 = 조합 + 전달
상속보다는 조합을 사용하라 - 상속보다 조합이 권장되는 이유?
- C++ 과 다르게, Java, JavaScript에서는 다중상속이 불가능하다. 다시 말해, 하나의 속성을 기준으로 상속을 한다면 다른 속성에 대한 상속은 포기해야한다. e.g. ‘나이’를 기준으로 상속을 한다면 ‘직업’ 기준으로의 상속은 포기해야한다.
- 상속은 캡슐화를 깨트리기도 하고, 슈퍼클래스의 변경으로 의도치 않은 서브클래스 로직변경이 일어날 수 있다.
- 상속받지 않고 객체를 필드로 갖도록 하면, 실제 객체 구현체가 아니라 객체의 인터페이스를 갖도록 할 수 있다. 이렇게 하면 DI받고 싶은 실제 구현체를 필요에 따라 다르게 DI받을 수도 있다.
e.g. 기존 클래스는
Set
, 전달 클래스는ForwardingSet
, 새로운 클래스는InstrumentedSet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
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 class ForwardingSet<E> implements Set<E> { private final Set<E> s; public add(E e) { return s.add(e); } public addAll(Collection<? extends E> c) { return s.addAll(c); } }
- (조합) 전달 클래스 내부에서 기존 클래스가 새로운 클래스의 구성요소로 쓰인다.
- (전달) 서브클래스의 인스턴스 메서드 내에서 (전달)슈퍼 클래스의 메서드를 호출하여 인스턴스를 전달해 쓴다. 다시 말해, 슈퍼 클래스의 기능 일부를 빌려온다.
- (전달 메서드) 이 때 (전달)서브 클래스들의 메서드들은 전달 메서드가 된다.
11. 슈퍼클래스를 위임으로 바꾸기
: 슈퍼클래스와의 상속관계를 끊고 서브클래스에 슈퍼 클래스 객체를 필드에 저장해두고 필요한 기능만 위임하도록 한다.
보통의 경우 서브 클래스는 슈퍼 클래스의 모든 기능을 사용해야하고, (리스코프 치환 원칙) 슈퍼 클래스가 사용되는 모든 곳을 서브 클래스로 치환해도 동작에 이상이 없어야한다.
위와 같은 경우라면, (혹은 제대로 된 상속관계여도) 슈퍼/서브 관계로 강하게 연결된 결합에선 슈퍼클래스의 변경으로 서브클래스의 메서드가 망가지는 경우가 있으므로 위임으로 바꾸면 좋다. 이 때 기존의 서브클래스들의 메소드들은 모두 전달 메소드로 수정해야한다는 번거로움이 있긴 하다.
e.g. 자바의 stack
1 2 3 4
// Before public class Stack<E> extends Vector<E> { // .. }
- 자바의 Stack은 리스트를 상속받고 있는데, 리스트의 기능 중 스택에 적용되지 않는 것들이 많다.
1 2 3 4 5
// After public class Stack<E> { Vector<E> v; // .. }
- 위와 같이 위임으로 리팩터링한다면 일부 필요한 기능만 가져가되, 결합관계를 끊어 낼 수 있을 것 같다.
Reference)
리팩터링 2판