부모 타입이 쓰이는 자리에 자식 타입을 넣어도 프로그램이 깨지지 않아야 한다는 원칙. 상속이 컴파일된다는 것만으로는 부족하고, 부모를 기대한 코드가 자식을 받아도 기대한 대로 동작해야 한다.
정사각형은 직사각형인가
수학적으로는 그렇지만 코드에서는 아닐 수 있다.
class Rectangle {
constructor(protected w = 0, protected h = 0) {}
setWidth(w: number) { this.w = w }
setHeight(h: number) { this.h = h }
area() { return this.w * this.h }
}
class Square extends Rectangle {
setWidth(w: number) { this.w = this.h = w }
setHeight(h: number) { this.w = this.h = h }
}
function check(r: Rectangle) {
r.setWidth(5); r.setHeight(4)
return r.area() === 20 // Square를 넣으면 16
}Rectangle은 "너비와 높이를 따로 바꿀 수 있다"는 암묵적 약속을 하는데 Square가 그것을 어겼다. 상속의 기준은 현실의 is-a가 아니라 부모의 약속을 자식이 모두 지키는가다.
자식이 지킬 계약
- 입력 조건을 부모보다 까다롭게 만들지 않는다(사전조건(Precondition) 강화 금지)
- 결과 보장을 부모보다 약하게 만들지 않는다(사후조건(Postcondition) 약화 금지)
- 부모가 던지지 않던 예외를 던지지 않는다.
fly()를 물려받은 펭귄이 예외를 던지는 것이 전형적인 위반이다. 날 수 있는 능력을 따로 분리해 계층을 다시 짠다
어기는 상속은 위임으로 바꾸는 신호다. 타입 시스템 차원의 대응 개념은 공변성과 반공변성(Covariance and Contravariance)이다. SOLID 원칙의 L이다.