1. 알고자 하는 것
객체지향에서 상속은 기존 기능을 재사용할 수 있는 강력한 기능을 제공한다.
하지만 상속에도 단점이 존재하며, 상속이 적절치 않은 경우가 존재한다.
이럴 때 상속을 위임(Delegate) 으로 바꾸어 해결할 수 있다.
- 상속을 위임으로 바꾸기 (Replace Inherit with Delegate)
2. 알게 된 것
- 상속은 상위 클래스의 기능을 하위 클래스가 재사용 할 수 있는 좋은 방법이다.
- 상속으로 불필요한 코드의 중복을 막을 수 있고, 공통 기능의 변경 시 변경점이 줄어든다.
- 하지만 이러한 상속이 적절하지 않은 경우가 존재한다.
- 상속 받는 하위 클래스의 경우 상위 클래스의 모든 기능을 지원해야 한다.
- 자바의 Stack 클래스의 경우, Vector<E>를 상속 받는다.
- 상위 클래스인 Vector의 add() 메서드를 지원해야 하므로 특정 위치에 element를 삽입 할 수 있는 add() 메서드가 구현되어 있다.
- 상위 클래스의 모든 기능을 지원해야 하는 상속의 특성으로 인해 Stack의 LIFO 구조가 깨져버렸다.
- * 이로 인해 Stack 클래스 내 주석을 보면 "더 완전하고 일관성 있는 LIFO 연산을 지원하는 Stack을 사용하고자 한다면 Deque 클래스 사용을 권장하고 있다. (Deque는 Queue<E>를 상속받음)
A more complete and consistent set of LIFO stack operations is
* provided by the {@link Deque} interface and its implementations, which
* should be used in preference to this class.
- 또한 상속의 경우 상위 클래스의 기능이 변경되면 모든 하위 클래스가 영향을 받는다.
- 즉, 상속은 상위 클래스와 하위 클래스의 결합도가 높음을 의미한다.
- 상위 클래스의 모든 기능을 지원할 필요가 없거나, 상위 클래스의 기능이 자주 변경될 여지가 있다고 판단된다면 상속이 아닌 클래스를 인스턴스 변수로 가지는 위임(Delegate)을 통해 상속과 마찬가지로 코드를 재사용 할 수 있다.
- 카테고리의 정보를 나타내는 CategoryItem과 이를 상속받은 Scroll이 있다.
public class CategoryItem {
private Integer id;
private String title;
private List<String> tags;
public CategoryItem(Integer id, String title, List<String> tags) {
this.id = id;
this.title = title;
this.tags = tags;
}
public Integer getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean hasTag(String tag) {
return this.tags.contains(tag);
}
}
public class Scroll extends CategoryItem {
private LocalDate dateLastCleaned;
public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
super(id, title, tags);
this.dateLastCleaned = dateLastCleaned;
}
public long daysSinceLastCleaning(LocalDate targetDate) {
return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
}
}
- 이를 상속 구조가 아닌 위임 구조로 변경하고자 한다면, Scroll의 상속을 제거하고 Scroll 클래스에 인스턴스 변수로 CategoryItem을 두면 된다.
public class Scroll {
public static void main(String[] args) {
Scroll scroll = new Scroll(1, "test", List.of("test1", "test2"), LocalDate.now());
Integer id = scroll.getCategoryItem().getId(); // 기존 상위 클래스의 필드 재사용 가능
}
private LocalDate dateLastCleaned;
private CategoryItem categoryItem; // 필드 선언
public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
this.dateLastCleaned = dateLastCleaned;
this.categoryItem = new CategoryItem(id, title, tags);
}
public long daysSinceLastCleaning(LocalDate targetDate) {
return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
}
public LocalDate getDateLastCleaned() {
return dateLastCleaned;
}
public CategoryItem getCategoryItem() {
return categoryItem;
}
}
- 상위 클래스의 상속을 제거하고 필드로 두어, 불필요한 기능 구현이 더 이상 필요하지 않아졌다. (구현 강제 X)
- 만약 CategoryItem의 id를 조회하는 기능을 getId() 메서드로 감싸서 처리한다면, 추후 CategoryItem의 id 필드가 변경되었을 때 단순히 하단 주석의 categoryItem.getId() 만을 변경해주면 된다. (느슨한 결합도 유지)
public class Scroll {
private LocalDate dateLastCleaned;
private CategoryItem categoryItem; // 필드 선언
public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
this.dateLastCleaned = dateLastCleaned;
this.categoryItem = new CategoryItem(id, title, tags);
}
public long daysSinceLastCleaning(LocalDate targetDate) {
return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
}
public LocalDate getDateLastCleaned() {
return dateLastCleaned;
}
public Integer getId() {
return this.categoryItem.getId(); // 해당 코드만 수정하면 됨
}
}
- + CategoryItem을 인터페이스로 만든다면 Scroll 클래스를 생성할 때 조건에 맞는 CategoryItem을 유연하게 주입할 수 있다.
- + 이를 통해 직접적인 상위 클래스와의 결합도를 더욱 낮출 수 있고, Scroll 클래스는 CategoryItem의 내부 구현은 신경쓰지 않고 단순히 위임자로서 CategoryItem의 메서드를 호출만 하면 된다.
3. 정리
- 객체지향에서 상속은 기존 기능을 재사용할 수 있는 강력한 기능을 제공하지만, 상위 클래스의 기능 구현 강제 / 상위 클래스의 변경 시 영향을 받는 강한 결합도가 발생할 수 있다.
- 하위 클래스가 상위 클래스의 기능을 모두 구현하기에 적합하지 않거나, 상위 클래스의 변경이 잦은 경우 위임(Delegate)을 사용할 수 있다.
- 상속을 하지 않고 상위 클래스를 인스턴스 변수로 두어 메서드 호출을 위임한다.
- 이를 통해 상위 클래스의 기능을 모두 구현 할 필요가 없고, 상위 클래스의 변경 시 상위 클래스의 메서드를 호출하는 코드만 변경할 수 있어 느슨한 결합도를 유지할 수 있다.
- 이 때 상위 클래스를 인터페이스로 사용한다면, 클래스 생성 시 유연하게 클래스를 주입해 상위 클래스의 내부 구현에 상관 없이 유연하게 로직을 수행할 수 있어 결합도를 더욱 낮출 수 있다.
Reference
'Clean Code' 카테고리의 다른 글
Java 리팩토링 - 특이 케이스 추가하기 (Introduce Special Case) (0) | 2024.01.16 |
---|---|
Java 리팩토링 - 기본형을 객체로 바꾸기 (Replace Primitive with Object) (0) | 2024.01.15 |
Java 리팩토링 - 함수 인라인, 클래스 인라인 (Function Inline, Class Inline) (0) | 2024.01.14 |
Java 리팩토링 - 함수 옮기기 (Move Function) (0) | 2023.12.25 |
Java 리팩토링 - 변수 캡슐화하기 (with. Getter/Setter) (0) | 2023.12.17 |