마틴 오더스키는 아래 질문에 모두 “예”라고 답할 수 있는 경우에만 상속을 사용하라고 조언한다.
/**
- ex) 새와 펭귄의 관계
- 펭귄은 새다.
- 새는 날 수 있다.
**/
public class Bird {
public void fly() { ... }
}
public class Penguine extends Bird {
// 하지만 펭귄은 날 수 없다.
}
public void flyBird(Bird bird) {
// 인자로 전달된 모든 bird는 날 수 있어야 한다.
bird.fly();
}
Penguine
은 Bird
의 자식 클래스이기에 업캐스팅을 허용하지만 펭귄은 날 수 없다.// 상속 관계를 유지하면서 문제를 해결하는 방법
// 1. `Penguine`에서 `fly` 메서드를 오버라이딩하고 내부 구현을 비워두는 방법
public class Penguine extends Bird {
@Override
public void fly() { }
}
// 2. 오버라이딩하고 예외를 던지게 하는 방법
public class Penguine extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException();
}
}
// 3. 클라이언트에서 bird 타입이 Penguine이 아닌 경우에만 메시지를 전송
public void flyBird(Bird bird) {
// 인자로 전달된 펭귄 이외의 새는 날 수 있어야 한다.
if (!(bird instanceof Penguine)) {
bird.fly();
}
}
public class Bird { ... }
public class FlyingBird extends Bird {
public void fly() { ... }
}
public class Penguine extends Bird { ... }
// FlyingBird 타입이 아닌 펭귄이 전달되어 잘못된 행동을 할 위험이 없다
public void flyBird(FlyingBird bird) {
bird.fly();
}
Bird
는 날 수 있으면서 걸을 수 있어야 하고 Penguine
은 걸을 수만 있다고 가정Walker
타입을 사용하면 Bird와 Penguine 모두를 사용 가능하다.Flyer
타입을 사용하면 Bird
만 사용하도록 제약할 수 있다.public Bird implements Flyer, Walker { ... }
public Penguine implements Walker { ... }
Penguine
이 Bird
의 코드를 재사용하고 싶다면 상속할 순 있지만 fly
오퍼레이션이 추가되어 버린다.
Flyer
에 의존하는 Bird
는 영향을 받지만 Penguine
과 ‘걸을 수 있다’는 행동에 의존하는 클라이언트들은 영향을 받지 않는다.Stack
과 Vector
Penguine
과 Bird
Movie
와 DiscountPolicy
간의 계약
Screening
이 null
이 아니여야 한다.Money
가 null
이 아니여야 한다.public class Movie {
// ...
public Money calculateMovieFee(Screening screening) {
if (screening == null ||
screening.getStartTime().isBefore(LocalDateTime.now())) {
throw new InvalidScreeningException();
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
// ...
}
public abstract class DiscountPolicy {
// ...
public Money calculateDiscountAmount(Screening screening) {
checkPrecondition(screening); // 사전조건 체크
Money amount = Money.ZERO;
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
amount = getDiscountAmount(screening);
checkPostcondition(amount);
return amount;
}
}
amount = screening.getMovieFee();
checkPostcondition(amount); // 사후조건 체크
return amount;
}
}