결합도 문제
상속은 간단해보이지만 사실 많은 한계점이 있다.
상속 관계는 슈퍼-서브 클래스간의 높은 결합도를 낳는다.
상속을 이용해 코드를 재사용하기 위해선 부모 클래스의 개발자가 세운 가정이나 배경을 추론하고 정확히 이해해야 한다.
상속은 자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문에 캡슐화를 약화시킨다.
상속은 재사용성을 위해서 캡슐화를 희생한다.
만약 어떤 객체가 상속을 여러번 사용해야 한다면?
java 에서는 다중 상속을 허용하지 않는다.
슈퍼 클래스 내부의 변경이 있을 때 모든 서브 클래스를 함께 수정해야 할 수도 있다.
취약한 기반 클래스 문제
= 부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상
취약한 기반 클래스는 캡슐화를 약화시키고 결합도를 높인다.
자식 클래스가 부모 클래스의 구현 세부사항에 의존하도록 만들기 때문이다.
불필요한 인터페이스 상속 문제
add, remove 메서드는 원소의 index를 정할 수 있기 때문에 Stack의 LIFO 규칙을 깨버린다.
개발자의 입장에서는 Stack 객체 인스턴스를 만들어서 마지막에 add() 메서드로 스택에 맨 아래에 원소를 삽입한다면 스택의 규칙을 깨게 된다.
메서드 오버라이딩의 부작용 문제
상속의 문제 해결 : 추상화에 의존하자
상속으로 인한 피해를 최소화하는 방법은 추상화다.
코드의 중복을 제거하기 위해선 메서드 추출을 통해 하위 클래스들의 공통 로직을 상위 클래스로 이동시킨다.
-> 객체 응집도 상승 (OCP 준수)
public abstract class Phone {
private double taxRate;
private List<Call> calls = new ArrayList<>();
public Phone(double taxRate) {
this.taxRate = taxRate;
}
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result.plus(result.times(taxRate));
}
protected abstract Money calculateCallFee(Call call);
}
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
super(taxRate);
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
} else {
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
}
결국 추상화가 핵심이다.
공통 코드를 이동한 후에 각 클래스는 서로 다른 변경 이유를 갖게 된다. SRP를 준수한다.
즉, 각 클래스들은 높은 응집도를 갖게 된다.
부모 클래스 또한 자신의 내부에 있는 추상 메서드를 호출하기 때문에 추상화에 의존한다고 말할 수 있다. 즉 DIP 도 준수한다.
새로운 요금제를 추가(기능 확장)하기도 쉽다.calculateCallFee
메서드만 오버라이딩해주면 된다.
확장에 열려있고 수정에 닫혀있기 때문에 OCP를 준수한다.
그래도 상속의 결합도 문제는 피할 수 없다.
추상화를 통해 메서드에 대한 결합을 느슨하게 유지할 수 있다.
객체의 행동만 변경된다면 상속 계층에 속한 각 클래스들을 독립적으로 변경시킬 수 있다.
그러나 인스턴스 변수에 대해서는 결합도 문제를 피할 방법이 없다.
인스턴스 변수가 추가된다면? 혹은 삭제된다면?
결국 자식 클래스는 생성자에서 부모 클래스에 정의된 인스턴스 변수를 초기화(super(...)
해야 하기 때문에 인스턴스 변수의 변경은 최소한 자식 클래스의 초기화 로직에 변경 영향을 미친다.
상속 계층 전반에 걸친 변경을 유발하는 것.
그러나 이것은 상속을 하지 않음으로써 코드를 중복시키는 것보다는 현명한 선택이다.
객체 생성 로직의 변경을 막기보다는 핵심 로직의 중복을 막아라.
상속은 강력한 도구다.
코드 재사용 측면에서 정말 강력한 도구이지만, 잘못 사용할 경우에 돌아오는 피해 역시 크다.
정말로 필요한 경우에만 상속을 사용하자.
코드 재사용을 더 우아하게 해결하려면 합성을 사용할 수 있다.
'CS > OOP' 카테고리의 다른 글
[오브젝트] 12장 - 상속의 목적은 서브타입 다형성 (0) | 2024.02.22 |
---|---|
[오브젝트] 11장 - 합성과 유연한 설계 (0) | 2024.02.15 |
[오브젝트] 9장 : 유연한 설계를 하려면 OCP, DIP를 지켜라 (0) | 2024.02.01 |
[오브젝트] 8장 - 좋은 의존성과 나쁜 의존성 (0) | 2024.01.29 |
[오브젝트] 7장 : 기능 분해의 측면에서 본 OOP (1) | 2024.01.23 |