할인 요금 구하기

이번에는 예매 요금을 계산하는 협력을 살펴보자.

public Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }
    
    public Money getFee() {
        return fee;
    }
    
    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

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 PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
    
    public PeriodCondition(DayOfWeek dayOfweek, LocalTime startTime, LocalTime endTime) {
        this.dayOfWeek = dayOfWeek;
        this.startTime = startTime;
        this.endTime = endTime;
    }
    
    public boolean isSatisfiedBy(Screening screening) {
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
            startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
            endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

기간 조건은 상영 날짜가 같으면서, 상영 시간이 할인 시작 시간과 종료 시간 사이에 있는지 판단해 할인 여부를 결정한다.

할인 정책도 구현해보자.

public class AmountDiscountPolicy extends DiscountPolicy {
    private Money discountAmount;
    
    public AmountDiscountPolicy(Money discountAmount, DiscountCondition ... conditions) {
        super(conditions);
        
        this.discountAmount = discountAmount;
    }
    
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return discountAmount;
    }
}

public class PercentDiscountPolicy extends DiscountPolicy {
    private double percent;
    
    public PercenDiscountPolicy(double percent, DiscountCondition ... conditions) {
        super(conditions);
        
        this.percent = percent;
    }
    
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return screening.getMovieFee().times(percent);
    }
}

오버라이딩은 부모 클래스의 동일한 구조를 가진 메서드 (동일한 이름, 같은 파라미터 목록) 를 재정의하는 것을 말한다. 반면 오버로딩은 메서드의 이름은 같으나 파라미터의 형태, 목록이 다른 경우를 말한다. 오버로딩은 재정의하는 것이 아니라 공존하도록 구현한다. 외부에서는 오버로딩된 함수들을 모두 호출할 수 있다.

하나의 영화에 대해 하나의 할인 정책이 수립된다고 정의했다. 그리고 할인 조건은 여러개를 지정할 수 있었다.

이런 정의는 생성할 때 제약해두면 좋다.

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)
                 ) 
               )

Last updated