TIL

13 서브클래싱과 서브타이핑

01 타입

개념 관점의 타입

프로그래밍 언어 관점의 타입

객체지향 패러다임 관점의 타입

02 타입 계층

타입 사이의 포함 관계

객체지향 프로그래밍과 타입 계층

03 서브클래싱과 서브타이핑

언제 상속을 사용해야 하는가

마틴 오더스키는 아래 질문에 모두 “예”라고 답할 수 있는 경우에만 상속을 사용하라고 조언한다.

is-a 관계

/**
- ex) 새와 펭귄의 관계
    - 펭귄은 새다.
    - 새는 날 수 있다.
**/

public class Bird {
    public void fly() { ... }
}

public class Penguine extends Bird { 
    // 하지만 펭귄은 날 수 없다.
}

행동 호환성

public void flyBird(Bird bird) {
    // 인자로 전달된 모든 bird는 날 수 있어야 한다.
    bird.fly();
}
// 상속 관계를 유지하면서 문제를 해결하는 방법

// 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();
}
public Bird implements Flyer, Walker { ... }

public Penguine implements Walker { ... }

서브 클래싱과 서브 타이핑

04 리스코프 치환 원칙

클라이언트와 대체 가능성

리스코프 치환 원칙은 유연한 설계의 기반이다

타입 계층과 리스코프 치환 원칙

05 계약에 의한 설계와 서브타이핑

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;
    }
}

서브타입과 계약