Phone -----> RatePolicy <-----------
↑ |
---- ------- |
BasicRatePolicy AdditionalRatePolicy
↑ ↑
FixedFeePolicy RateDiscountablePolicy
TimeOfDayDiscountPolicy TaxablePolicy
DayOfWeekDiscountPolicy
DurationDiscountPolicy
public class FixedFeePolicy extends BasicRatePolicy {
private Money amount;
private Duration seconds;
// ...
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
DateTimeInterval
클래스 추가
Call
의 원래 인스턴스였던 from
과 to
를 하나의 값객체로 묶을 수 있다.public class Call {
private DateTimeInterval interval;
public Call(LocalDateTime from, LocalDateTime to) {
this.interval = DateTimeInterval.of(from, to);
}
// ...
}
Call
- 통화 기간 정보를 알고 있는 전문가DateTimeInterval
- 기간을 처리하는 방법에 대한 전문가TimeOfDayDiscountPolicy
- 시간대별 기준을 잘 알고 있는 시간대별 분할 작업의 전문가public class TimeOfDayDiscountPolicy extends BasicRatePolicy {
private List<LocalTime> starts = new ArrayList<LocalTime>();
private List<LocalTime> ends = new ArrayList<LocalTime>();
private List<Duration> durations = new ArrayList<Duration>();
private List<Money> amounts = new ArrayList<Money>();
@Override
protected Money calculateCallFee(Call call) {
Money result = Money.ZERO;
for(DateTimeInterval interval : call.splitByDay()) { // call에게 일자별 통화 기간 분리를 요청
for(int loop=0; loop < starts.size(); loop++) {
LocalTime from = from(interval, starts.get(loop));
LocalTime to = to(interval, ends.get(loop));
long intervalSeconds = Duration.between(from, to).getSeconds();
long criteriaSeconds = durations.get(loop).getSeconds();
Money intervalFee = amounts.get(loop);
result.plus(intervalFee.times(intervalSeconds / criteriaSeconds));
}
}
return result;
}
private LocalTime from(DateTimeInterval interval, LocalTime from) {
LocalTime intervalFrom = interval.getFrom().toLocalTime();
return intervalFrom.isBefore(from) ? from : intervalFrom;
}
private LocalTime to(DateTimeInterval interval, LocalTime to) {
LocalTime intervalTo = interval.getTo().toLocalTime();
return intervalTo.isAfter(to) ? to : intervalTo;
List
로 규칙을 정의한 시간대별 방식과 다르게 DayOfWeekDiscountRule
클래스로 규칙을 관리public class DayOfWeekDiscountRule {
private List<DayOfWeek> dayOfWeeks = new ArrayList<>();
private Duration duration = Duration.ZERO;
private Money amount = Money.ZERO;
// 생성자
public Money calculate(DateTimeInterval interval) {
if (dayOfWeeks.contains(interval.getFrom().getDayOfWeek())) {
return amount.times(interval.duration().getSeconds() / duration.getSeconds());
}
return Money.ZERO;
}
}
public class DayOfWeekDiscountPolicy extends BasicRatePolicy {
private List<DayOfWeekDiscountRule> rules = new ArrayList<>();
// 생성자
@Override
protected Money calculateCallFee(Call call) {
Money result = Money.ZERO;
for(DateTimeInterval interval : call.getInterval().splitByDay()) {
for(DayOfWeekDiscountRule rule: rules) {
result.plus(rule.calculate(interval));
}
}
return result;
}
}
List
로 관리 or 별도 클래스로 관리Movie
가 DiscountPolicy
를 의존하여 Screening
을 전달하여 요금 계산을 요청DiscountPolicy
는 DiscountCondition
목록을 의존하여 각 조건에 해당하는 Screening
을 매칭Movie
, DiscountPolicy
, DiscountCondition
사이의 합력은 변경을 기준으로 클래스를 분리한 좋은 예
public
일 필요가 없는 것들은 공개하지 않음으로써 은닉할 수 있다.private
으로 은닉함으로 두 객체 간 관계를 변경하더라도 외부의 영향을 미치지 않을 수 있다.BasicRatePolicy
—의존→ List<FeeRule>
FeeRule
에 feePerDuration
인스턴스 변수 존재FeeRule
—의존→ FeeCondition
TimeOfDayFeeCondition
, DayOfWeekFeeCondition
, DurationFeeCondition
BasicRatePolicy
가 calculateFee
메시지 수신BasicRatePolicy
는 전체 Call
에 대해 요금을 계산Call
별로 FeeRule
의 calculateFee
메서드를 실행FeeCondition
- 전체 통화 시간을 각 ‘규칙’의 ‘적용조건’을 만족하는 구간들로 나누기FeeRule
- 분리된 통화 구간에 ‘단위요금’을 적용하여 요금 계산// Call의 통화 기간 중 '적용조건'을 만족하는 기간의 목록을 반환
public interface FeeCondition {
List<DateTimeInterval> findTimeIntervals(Call call);
}
public class FeeRule {
private FeeCondition feeCondition;
private FeePerDuration feePerDuration;
// ...
public Money calculateFee(Call call) {
return feeCondition.findTimeIntervals(call)
.stream()
.map(each -> feePerDuration.calculate(each)) // 단위요금 계산
.reduce(Money.ZERO, (first, second) -> first.plus(second));
}
}
public final class BasicRatePolicy implements RatePolicy {
private List<FeeRule> feeRules = new ArrayList<>();
// ...
@Override
public Money calculateFee(Phone phone) {
return phone.getCalls()
.stream()
.map(call -> calculate(call))
.reduce(Money.ZERO, (first, second) -> first.plus(second));
}
private Money calculate(Call call) {
return feeRules
.stream()
.map(rule -> rule.calculateFee(call))
.reduce(Money.ZERO, (first, second) -> first.plus(second));
}
}
// 시간대별 정책
public class TimeOfDayFeeCondition implements FeeCondition {
private LocalTime from;
private LocalTime to;
// ...
@Override
public List<DateTimeInterval> findTimeIntervals(Call call) {
return call.getInterval().splitByDay()
.stream()
.filter(each -> from(each).isBefore(to(each)))
.map(each -> DateTimeInterval.of(
LocalDateTime.of(each.getFrom().toLocalDate(), from(each)),
LocalDateTime.of(each.getTo().toLocalDate(), to(each))))
.collect(Collectors.toList());
}
// ...
}
// 요일별 정책
public class DayOfWeekFeeCondition implements FeeCondition {
private List<DayOfWeek> dayOfWeeks = new ArrayList<>();
// ...
@Override
public List<DateTimeInterval> findTimeIntervals(Call call) {
return call.getInterval()
.splitByDay()
.stream()
.filter(each ->
dayOfWeeks.contains(each.getFrom().getDayOfWeek()))
.collect(Collectors.toList());
}
}
// 구간별 정책
public class DurationFeeCondition implements FeeCondition {
private Duration from;
private Duration to;
// ...
@Override
public List<DateTimeInterval> findTimeIntervals(Call call) {
if (call.getInterval().duration().compareTo(from) < 0) {
return Collections.emptyList();
}
return Arrays.asList(DateTimeInterval.of(
call.getInterval().getFrom().plus(from),
call.getInterval().duration().compareTo(to) > 0 ?
call.getInterval().getFrom().plus(to) :
call.getInterval().getTo()));
}
}
public class FixedFeeCondition implements FeeCondition {
@Override
public List<DateTimeInterval> findTimeIntervals(Call call) {
return Arrays.asList(call.getInterval()); // 적용조건이 의미 없기에 call의 전체 기간을 반환
}
}