calculateMovieFee 메서드는 discountPolicy 에 calculateDiscountAmount 메시지를 전송해서 할인 요금을 전달받기를 믿는다. 그리고 기본 요금에서 할인 요금만큼 차감한다.
할인 정책에는 (1) 금액 할인 정책 (2) 비율 할인 정책이 있었다. 그런데 위 코드에서는 할인 정책을 표현하지 않고 단지 메세지를 전달한다. 객체지향 패러다임에서 중요한 두 가지 개념이 있다.
하나는 상속(Inheritance), 하나는 다형성(Polymorphism)이다. 그리고 그 기반에는 추상화(Abstraction)라는 원리가 숨겨져있다.
할인 정책은 금액 할인 정책과 비율 할인 정책으로 구분된다. 이 두 객체는 서로 성질이 비슷해서 공통된 클래스가 있으면 좋다. DiscountPolicy 에 공통 속성과 기능을 가지도록 하고, AmountDiscountPolicy 와 PercentDiscountPolicy 가 상속받게한다. 실제 어플리케이션 소비자에서는 DiscountPolicy 인스턴스를 사용하지않기에 추상 클래스로 작성한다.
public abstract DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Array.asList(conditions);
}
public Money calcualteDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
하나의 DiscountPolicy 는 DiscountCondition 들을 가질 수 있다. calculateDiscountAmount() 는 전체 할인 조건들에 대해서 isSatisfiedBy() 메서드를 호출한다. 할인 조건을 하나라도 만족하면 추상 메서드로 선언한 getDiscountAmount() 를 호출해 할인 요금을 계산한다.
전체적인 할인 여부 및 요금 계산을 관장하지만, 실제 요금 계산은 추상 메서드로 위임한다.
위임한 메서드는 추상 클래스를 상속한 AmountDiscountPolicy.getDiscountAmount, PercentDiscountPolicy.getDiscountAmouunt 가 오버라이딩해 구현한다.
부모 클래스에서 기본적인 알고리즘 흐름을 구현하고, 중간 중간 필요한 처리를 자식 클래스에 위임하는 패턴을 TEMPLATE METHOD 패턴이라고 한다.
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
DiscountCondition 은 인터페이스로 구현한다. isSatisfiedBy 오퍼레이션은 상영 정보를 받아 할인이 가능한 지 판단한다. 인터페이스라 구현은 없다.
할인 조건에는 (1) 순번 조건과 (2) 기간 조건 두 가지 할인 조건이 존재한다.
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
오버라이딩은 부모 클래스의 동일한 구조를 가진 메서드 (동일한 이름, 같은 파라미터 목록) 를 재정의하는 것을 말한다. 반면 오버로딩은 메서드의 이름은 같으나 파라미터의 형태, 목록이 다른 경우를 말한다. 오버로딩은 재정의하는 것이 아니라 공존하도록 구현한다. 외부에서는 오버로딩된 함수들을 모두 호출할 수 있다.
하나의 영화에 대해 하나의 할인 정책이 수립된다고 정의했다. 그리고 할인 조건은 여러개를 지정할 수 있었다.
이런 정의는 생성할 때 제약해두면 좋다.
public class Movie {
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountpolicy
}
}
public abstract class DiscountPolicy {
public DiscountPolicy(DiscountPolicy ... conditions) {
this.conditions = Arrays.asList(conditions);
}
}
정의에 어긋나지 않도록 초기화 시점에 상태를 강제했다.
영화 '아바타' 에 대한 인스턴스를 정의해보자.
Movie avatar = new Movie(
"아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(
Money.wons(800),
new SequenceCondition(1),
new SequenceCondition(10),
new PeriodCondition(
DayOfWeek.MONDAY,
LocalTime.of(10, 0),
LocalTime.of(11, 59)
),
new PeriodCondition(
DayOfWeek.THURSDAY,
LocalTime.of(10, 0),
LocalTime.of(20, 59)
)
)