TIL

14 일관성 있는 협력

01 핸드폰 과금 시스템 변경하기

기본 정책 확장

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

시간대별 방식 구현하기

public class Call {
	private DateTimeInterval interval;

	public Call(LocalDateTime from, LocalDateTime to) {
		this.interval = DateTimeInterval.of(from, to);
	}
	// ...
}
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;

요일별 방식 구현하기

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

구간별 방식 구현하기 전 기존 방식 문제점

02 설계에 일관성 부여하기

조건 로직 대 객체 탐색

캡슐화 다시 살펴보기

03 일관성 있는 기본 정책 구현하기

변경 분리하기

변경 캡슐화하기

협력 패턴 설계하기

  1. BasicRatePolicycalculateFee 메시지 수신
  2. BasicRatePolicy는 전체 Call에 대해 요금을 계산
  3. Call 별로 FeeRulecalculateFee 메서드를 실행
  4. FeeCondition - 전체 통화 시간을 각 ‘규칙’의 ‘적용조건’을 만족하는 구간들로 나누기
  5. 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의 전체 기간을 반환
    }
}