객체는 협력을 위해 존재한다.
객체지향의 장점은 설계를 재사용할 수 있다는 것이다. 재사용을 위해선 객체들의 협력 방식을 일관성 있게 만들어야 한다.
일관성 있는 협력 패턴을 적용하면 코드가 이해하기 쉽고 직관적이며 유연해진다는 것이 이번 장의 주제다.
11장에서 구현한 핸드폰 과금 시스템의 요금 정책을 수정해야 한다고 가정하자. 이번장에서는 기본 정책을 아래와 같이 4가지 방식으로 확장할 것이다.
그림 14.1은 새로운 기본 정책을 적용할 때 조합 가능한 모든 경우의 수를 나타낸 것이다.
그림 14.2는 이번 장에서 구현하게 될 클래스 구조를 그림으로 나타낸 것이다.
고정 요금 방식은 기존의 일반요금제와 동일하기 때문에 기존의 RegularPolicy 클래스의 이름을 FixedFeePolicy로 수정하기만 하면 된다.
public class FixedFeePolicy extends BasicRatePolicy {
private Money amount;
private Duration seconds;
public FixedFeePolicy(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
@Override
protected Money calculateCallFee(Call call) {
return amount.times((double) call.getDuration().getSeconds() / seconds.getSeconds());
}
}
시간대별 방식에 따라 요금을 계산하기 위해서는 통화 기간을 정해진 시간대별로 나눈 후 각 시간대별로 서로 다른 계산 규칙을 적용해야 한다.
여기서 한 가지 고려해야 할 것이 만약 통화가 여러 날에 걸쳐서 이뤄진다면 어떻게 될까?
이 경우 시간대별 방식에 따라 요금을 구현하려면 규칙에 정의된 구간별로 통화를 구분해야 한다. 즉 위 그림의 통화는 그림 14.5와 같이 통화 구간을 분리한 후 각 구간에 대해 개별적으로 계산된 요금을 합해야 한다.
여기서 이야기하고 싶은 것은 시간대별 방식의 통화 요금을 계산하기 위해서는 통화의 시작 시간과 종료 시간뿐만 아니라 시작 일자와 종료 일자도 함께 고려해야 한다는 것이다.
시간대별 방식을 구현하는 데 있어 핵심은 규칙에 따라 통화 시간을 분할하는 방법을 결정하는 것이다. 이를 위해 기간을 편하게 관리할 수 있는 DateTimeInterval 클래스를 추가하자.
@Getter
public class DateTimeInterval {
private LocalDateTime from;
private LocalDateTime to;
public DateTimeInterval(LocalDateTime from, LocalDateTime to) {
this.from = from;
this.to = to;
}
public static DateTimeInterval of(LocalDateTime from, LocalDateTime to) {
return new DateTimeInterval(from, to);
}
public static DateTimeInterval fromMidNight(LocalDateTime to) {
return new DateTimeInterval(
LocalDateTime.of(to.toLocalDate(), LocalTime.of(0, 0)),
to);
}
public Duration duration() {
return Duration.between(from, to);
}
}
기존의 Call 클래스는 통화 기간을 저장하기 위해 from과 to라는 두 개의 LocalDateTime 타입의 인스턴스 변수를 포함하고 있었다.
public class Call {
private LocalDateTime from;
private LocalDateTime to;
}
이제 기간을 하나의 단위로 표한할 수 있는 DateTimeInterval 타입을 사용할 수 있으므로 from과 to를 interval 이라는 하나의 인스턴스 변수로 묶을 수 있다.
@Getter
public class Call {
private DateTimeInterval interval;
public Call(LocalDateTime from, LocalDateTime to) {
this.interval = DateTimeInterval.of(from, to);
}
public Duration getDuration() {
return interval.duration();
}
public LocalDateTime getFrom() {
return interval.getFrom();
}
public LocalDateTime getTo() {
return interval.getTo();
}
public DateTimeInterval getInterval() {
return interval;
}
}
그림 14.5처럼 전체 통화 시간을 일자와 시간 기준으로 분할해서 계산해보자. 이를 위해 요금 계산 로직을 다음과 같이 두 개의 단계로 나눠 구현할 필요가 있다.
두 작업을 객체의 책임으로 할당해보자. 책임을 할당하는 기본 원칙은 책임을 수행하는 데 필요한 정보를 잘 알고 있는 객체에 할당하는 것이다. 그건 바로 Call이다. 하지만 Call은 통화 기간은 잘 몰라도 기간 자체를 처리하는 방법에 대해서는 전문가가 아니다. 기간을 처리하는 전문가는 DateTimeInterval이다. 따라서 통화 기간을 일자 단위로 나누는 책임은 DateTimeInterval에게 할당하고 Call이 DateTimeInterval에게 분할을 요청하도록 협력을 설계하는 것이 적절할 것이다.
두 번째 작업인 시간대별로 분할하는 작업의 정보 전문가는 누구인가? 시간대별 기준을 가장 잘 알고 있는 요금 정책이며 여기서는 TimeOfDayDiscountPolicy라는 이름의 클래스로 구현할 것이다.
시간대별 방식 요금제에 가입한 사용자가 1월 1일 10시부터 1월 3일 15시까지 3일에 걸쳐 통화를 했다고 가정해보자.
Call은 이렇게 분리된 List를 시간대별 방식을 위한 TimeOfDayDiscountPolicy 클래스에게 반환한다. TimeOfDayDiscountPollicy 클래스는 일자별로 분리된 각 DateTimeInterval 인스턴스들을 요금 정책에 정의된 각 시간대별로 분할한 후 요금을 부과해야 한다.
이제 TimeOfDayDiscountPolicy 클래스를 구현해보자. 이 클래스에서 가장 중요한 것은 시간에 따라 서로 다른 요금 규칙을 정의하는 방법을 결정하는 것이다.
public class TimeOfDayDiscountPolicy extends BasicRatePolicy {
private List<LocalTime> starts = new ArrayList<>();
private List<LocalTime> ends = new ArrayList<>();
private List<Duration> durations = new ArrayList<>();
private List<Money> amounts = new ArrayList<>();
@Override
protected Money calculateCallFee(Call call) {
Money result = Money.ZERO;
for (DateTimeInterval interval : call.splitByDay()) {
for (int loop = 0; loop < starts.size(); loop++) {
result.plus(amounts.get(loop).times(
(double) Duration.between(from(interval, starts.get(loop)), to(interval, ends.get(loop)))
.getSeconds() / durations.get(loop).getSeconds()
));
}
}
return null;
}
private LocalTime from(DateTimeInterval interval, LocalTime from) {
return interval.getFrom().toLocalTime().isBefore(from) ?
from :
interval.getFrom().toLocalTime();
}
private LocalTime to(DateTimeInterval interval, LocalTime to) {
return interval.getTo().toLocalTime().isAfter(to) ?
to :
interval.getTo().toLocalTime();
}
}
Call의 splitByDay 메서드는 DateTimeInterval에 요청을 전달한 후 응답을 반환하는 간단한 위임 메서드다.
@Getter
public class Call {
private DateTimeInterval interval;
public Call(LocalDateTime from, LocalDateTime to) {
this.interval = DateTimeInterval.of(from, to);
}
public Duration getDuration() {
return interval.duration();
}
public LocalDateTime getFrom() {
return interval.getFrom();
}
public LocalDateTime getTo() {
return interval.getTo();
}
public DateTimeInterval getInterval() {
return interval;
}
public List<DateTimeInterval> splitByDay() {
return interval.splitByDay();
}
}
DateTimeInterval 클래스의 splitByDay 메서드는 통화 기간을 일자별로 분할해서 반환한다. days 메서드는 from과 to 사이에 포함된 날짜 수를 반환한다. 만약 days 메서드의 반환값이 1보다 크다면(여러 날에 걸쳐 있는경우라면) split 메서드를 호출해서 날짜 수만큼 분리한다. 만약 days 메서드의 반환값이 1이라면(하루 안의 기간이라면) 현재의 DateTimeInterval 인스턴스를 리스트에 담아 그대로 반환한다.
@Getter
public class DateTimeInterval {
private LocalDateTime from;
private LocalDateTime to;
public DateTimeInterval(LocalDateTime from, LocalDateTime to) {
this.from = from;
this.to = to;
}
public static DateTimeInterval of(LocalDateTime from, LocalDateTime to) {
return new DateTimeInterval(from, to);
}
public static DateTimeInterval toMidNight(LocalDateTime from) {
return new DateTimeInterval(
from,
LocalDateTime.of(from.toLocalDate().plusDays(1), LocalTime.MIDNIGHT)
);
}
public static DateTimeInterval fromMidNight(LocalDateTime to) {
return new DateTimeInterval(
LocalDateTime.of(to.toLocalDate(), LocalTime.of(0, 0)),
to);
}
public static DateTimeInterval during(LocalDate date) {
return new DateTimeInterval(
LocalDateTime.of(date, LocalTime.of(0, 0)),
LocalDateTime.of(date, LocalTime.of(23, 59, 59, 999_999_999))
);
}
public Duration duration() {
return Duration.between(from, to);
}
public List<DateTimeInterval> splitByDay() {
if (days() > 0) {
return splitByDay(days());
}
return Arrays.asList(this);
}
private long days() {
return Duration.between(from.toLocalDate().atStartOfDay(), to.toLocalDate().atStartOfDay())
.toDays();
}
public List<DateTimeInterval> splitByDay(long days) {
List<DateTimeInterval> result = new ArrayList<>();
addFirstDay(result);
addMiddleDays(result, days);
addLastDay(result);
return result;
}
private void addFirstDay(List<DateTimeInterval> result) {
result.add(DateTimeInterval.toMidNight(from));
}
private void addMiddleDays(List<DateTimeInterval> result, long days) {
for (int loop = 1; loop < days; loop++) {
result.add(DateTimeInterval.during(from.toLocalDate().plusDays(loop)));
}
}
private void addLastDay(List<DateTimeInterval> result) {
result.add(DateTimeInterval.fromMidNight(to));
}
}
요일별 방식은 요일별로 요금 규칙을 다르게 설정할 수 있다. 각 규칙은 요일의 목록, 단위 시간, 단위 요금이라는 세 가지 요소로 구성된다.
먼저 요일별 방식을 구성하는 규칙들을 구현해야 한다. 요일별 방식은 DayOfWeekDiscountRule 이라는 하나의 클래스로 구현할 것이다.
public class DayOfWeekDiscountRule {
private List<DayOfWeek> dayOfWeeks = new ArrayList<>();
private Duration duration = Duration.ZERO;
private Money amount = Money.ZERO;
public DayOfWeekDiscountRule(List<DayOfWeek> dayOfWeeks, Duration duration, Money amount) {
this.dayOfWeeks = dayOfWeeks;
this.duration = duration;
this.amount = amount;
}
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<>();
public DayOfWeekDiscountPolicy(List<DayOfWeekDiscountRule> rules) {
this.rules = rules;
}
@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;
}
}
이제 구간별 방식만 남았다. 지금까지 고정요금, 시간대별, 요일별 방식의 클래스는 따로 떨어뜨려 놓고 보면 그럭저럭 괜찮은 구현으로 보이기까지 한다.
결론은 유사한 기능을 서로 다른 방식으로 구현해서는 안된다는 것이다.
유사한 기능은 유사한 방식으로 구현해야 한다.
다시 구간별 방식을 구현하는 문제를 보자. 여기서는 구현을 새로운 방법으로 하기로 결정한 것이다.
요일별 방식의 경우처럼 규칙을 정의하는 새로운 클래스를 추가하기로 결정했다. 요일별 방식과 다른 점은 코드를 재사용하기 위해 FixedFeePolicy 를 상속한다는 것이다. DurationDiscountRule 클래스의 calculate 메서드 안에서 부모 클래스의 calculateFee 메서드를 호출하는 부분을 눈여겨 보자.
public class DurationDiscountRule extends FixedFeePolicy {
private Duration from;
private Duration to;
public DurationDiscountRule(Duration from, Duration to, Money amount, Duration seconds) {
super(amount, seconds);
this.from = from;
this.to = to;
}
public Money calculate(Call call) {
if (call.getDuration().compareTo(to) > 0) {
return Money.ZERO;
}
if (call.getDuration().compareTo(from) < 0) {
return Money.ZERO;
}
// 부모 클래스의 calculateFee(phone)은 Phone 클래스를 파라미터
// calculateFee(phone)을 재사용하기 위해
// 데이터를 전달할 용도로 임시 Phone 을 만든다.
CompositionPhone phone = new CompositionPhone(null);
phone.calls(new Call(call.getFrom().plus(from),
call.getDuration().compareTo(to) > 0 ? call.getFrom().plus(to) : call.getTo()));
return super.calculateFee(phone);
}
}
이제 여러 개의 DurationDiscountRule을 이용해 DurationDiscountPolicy를 구현할 수 있다.
public class DurationDiscountPolicy extends BasicRatePolicy {
private List<DurationDiscountRule> rules = new ArrayList<>();
@Override
protected Money calculateCallFee(Call call) {
Money result = Money.ZERO;
for (DurationDiscountRule rule : rules) {
result.plus(rule.calculate(call));
}
return result;
}
}
DurationDiscountPolicy 클래스는 할인 요금을 계산하고, 각 클래스는 하나의 책임만을 수행한다. 하지만 설계가 훌륭하다고 할 수 없는게, 기본 정책을 구현하는 기존 클래스들과 일관성이 없기 때문이다. 기존의 설계가 어떤 가이드도 제공하지 않기 때문에 새로운 기본 정책을 구현해야 하는 상황에서 또 다른 개발자는 또 다른 방식으로 기본 정책을 구현할 가능성이 높다.
일관성 있는 설계를 만드는 데 가장 훌륭한 조언은 다양한 설계 경험을 익히라는 것이다. 풍부한 설계 경험을 가진 사람은 어떤 변경이 중요한지, 그리고 그 변경을 어떻게 다뤄야 하는지에 대한 통찰력을 가지게 된다.
협력을 일관성 있게 만들기 위해 다음과 같은 기본 지침을 따르는 것이 도움될 것이다.
다음은 4장에서 절차적인 방식으로 구현했던 ReservationAgency의 기본 구조를 정리한 것이다.
위 코드에는 두 개의 조건 로직이 존재한다. 하나는 할인 조건의 종류를 결정하는 부분이고 다른 하나는 할인 정책을 결정하는 부분이다. 이 설계가 나쁜 이유는 변경의 주기가 서로 다른 코드가 한 클래스 안에 뭉쳐있기 때문이다. 이 설계가 나쁜 이유는 변경의 주기가 서로 다른 코드가 한 클래스 안에 뭉쳐있기 때문이다.
절차지향 프로그램에서 변경을 처리하는 전통적인 방법은 이처럼 조건문의 분기를 추가하거나 개별 분기 로직을 수정하는 것이다.
객체지향은 조금 다른 방법을 취한다. 객체지향에서 변경을 다루는 전통적인 방법은 조건 로직을 객체 사이의 이동으로 바꾸는 것이다. 아래 코드를 보면 Movie는 현재의 할인 정책이 어떤 종류인이 확인하지 않는다. 단순히 현재의 할인 정책을 나타내는 discountPolicy에 필요한 메시지를 전송할 뿐이다. 할인 정책의 종류를 체크하던 조건문이 discountPolicy로의 객체 이동으로 대체된 것이다.
다형성은 바로 이런 조건 로직을 객체 사이의 이동으로 바꾸기 위해 객체지향이 제공하는 설계 기법이다. 할인 금액을 계산하는 구체적인 방법은 메시지를 수신하는 discountPolicy의 구체적인 타입에 따라 결정된다. Movie는 discountPolicy가 자신의 요청을 잘 처리해줄 것이라고 믿고 메시지를 전송할 뿐이다.
DiscountPolicy와 DiscountCondition은 협력에 참여하는 객체들이 수행하는 역할이다. 추상적인 수준에서 협력은 아래와 같이 역할을 따라 흐른다.
하지만 실제로 협력에 참여하는 주체는 구체적인 객체다. 이 객체들은 협력 안에서 DiscountPolicy와 DiscountCondition을 대체할 수 있어야 한다.
많은 사람들은 객체의 캡슐화에 관한 이야기를 들으면 반사적으로 데이터 은닉을 떠올린다. 데이터 은닉이란 오직 외부에 공개된 메서드를 통해서만 객체의 내부에 접근할 수 잇게 제한함으로써 객체 내부의 상태 구현을 숨기는 기법을 가리킨다.
캡슐화의 가장 대표적인 예는 객체의 퍼블릭 인터페이스와 구현을 분리하는 것이다. 객체를 구현한 개발자는 필요할 때 객체의 내부 구현을 수정하기를 원한다. 따라서 자주 변경되는 내부 구현을 안정적인 퍼블릭 인터페이스 뒤로 숨겨야 한다.
캡슐화란 단순히 데이터를 감추는 것이 아니다. 소프트웨어 안에서 변할 수 있는 어떤 '개념'이라도 감추는 것이다.
일관성 있는 협력을 만들기 위한 첫 번째 단계는 변하는 개념과 변하지 않는 개념을 분리하는 것이다.
먼저 시간대, 요일, 구간별 방식은 각 기본 정책을 구성하는 방식이 유사하다는 점이다.
시간대, 요일, 구간별 방식의 차이점은 각 기본 정책별로 요금을 계산하는 '적용조건'의 형식이 다르다는 것이다. 모든 규칙에 '적용조건'이 포함된다는 사실은 변하지 않지만 실제 조건의 세부적인 내용은 다르다.
조건의 세부 내용이 변화에 해당한다.
협력을 일관성 있게 만들기 위해서는 변경을 캡슐화해서 파급효과를 줄여야 한다. 변하지 않는 부분이 오직 이 추상화에만 의존하도록 관계를 제한하면 변경을 캡슐화할 수 있게 된다.
여기서 변하지 않는 것은 '규칙'이다. 변하는 것은 '적용조건'이다. 따라서 '규칙'으로부터 '적용조건'을 분리해서 추상화한 후 시간대, 요일, 구간별 방식을 이 추상화의 서브타입으로 만든다. 이것이 서브타입 캡슐화다. 그 후에 규칙이 적용조건 표현하는 추상화를 합성 관계로 연결한다. 이것이 객체 캡슐화다.
이제 객체들의 협력 방식을 고민해보자. 변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분을 적절히 추상화하고 나면 변하는 부분을 생략한 채 변하지 않는 부분만을 이용해 객체 사이의 협력을 이야기할 수 있다.
그림 14.18은 이 추상화들이 참여하는 협력을 나타낸 것이다.
객체지향에서는 모든 작업을 객체의 책임으로 생객하기 때문에 이 두 개의 책임을 객체에게 할당하자. 전체 통화 시간을 각 '규칙'의 '적용조건'을 만족하는 구간들로 나누는 첫 번째 작업은 '적용조건'을 가장 잘 알고 있는 FeeCondition에게 할당하는 것이 적절할 것이다.
그림 14.20은 이 협력 과정을 그림으로 나타낸 것이다.
이 협력에 FeeCondition이라는 추상화가 참여하고 있다는 것에 주목하라. 만약 시간대별 방식으로 요금을 계산하고 싶으면 아래 그림처럼 TimeOfDayFeeCondition의 인스턴스가 FeeCondition의 자리를 대신 할 것이다.
먼저 '적용조건'을 표현하는 추상화인 FeeCondition에서 시작하자.
public interface FeeCondition {
List<DateTimeInterval> findTimeIntervals(Call call);
}
FeeRule은 단위요금(feePerDuration)과 적용조건(feeCondition)을 저장하는 두 개의 인스턴스 변수로 구성된다.
public class FeeRule {
private FeeCondition feeCondition;
private FeePerDuration feePerDuration;
public FeeRule(FeeCondition feeCondition, FeePerDuration feePerDuration) {
this.feeCondition = feeCondition;
this.feePerDuration = feePerDuration;
}
public Money calculateFee(Call call) {
return feeCondition.findTimeIntervals(call)
.stream()
.map(condition -> feePerDuration.calculate(condition))
.reduce(Money.ZERO, Money::plus);
}
}
FeePerDuration 클래스는 "단위 시간당 요금"이라는 개념을 표현하고 이 정보를 이용해 일정 기간 동안 요금을 계산하는 calculate 메서드를 구현한다.
public class FeePerDuration {
private Money fee;
private Duration duration;
public FeePerDuration(Money fee, Duration duration) {
this.fee = fee;
this.duration = duration;
}
public Money calculate(DateTimeInterval interval) {
return fee.times(Math.ceil((double) interval.duration().toNanos() / duration.toNanos()));
}
}
이제 BasicRatePolicy가 FeeRule 컬렉션을 이용해 전체 통화 요금을 계산하도록 수정할 수 있다.
public abstract class BasicRatePolicy implements RatePolicy {
private List<FeeRule> feeRules;
public BasicRatePolicy(FeeRule... feeRules) {
this.feeRules = Arrays.asList(feeRules);
}
@Override
public Money calculateFee(CompositionPhone phone) {
return phone.getCalls()
.stream()
.map(call -> calculate(call))
.reduce(Money.ZERO, Money::plus);
}
private Money calculate(Call call) {
return feeRules.stream()
.map(rule -> rule.calculateFee(call))
.reduce(Money.ZERO, Money::plus);
}
protected abstract Money calculateCallFee(Call call);
}
지금까지 구현한 클래스, 인터페이스는 변하지 않는 추상화다. 변하지 않는 요소 + 추상적인 요소만으로도 요금 계산에 필요한 전체적인 협력 구조를 설명할 수 있다.
시간대별 정책의 적용조건을 구현하는 TimeOfFeeCondition에서 시작하자.
public class TimeOfDayFeeCondition implements FeeCondition {
private LocalTime from;
private LocalTime to;
public TimeOfDayFeeCondition(LocalTime from, LocalTime to) {
this.from = from;
this.to = 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());
}
private LocalTime from(DateTimeInterval interval) {
return interval.getFrom().toLocalTime().isBefore(from) ?
from : interval.getFrom().toLocalTime();
}
private LocalTime to(DateTimeInterval interval) {
return interval.getTo().toLocalTime().isBefore(to) ?
to : interval.getTo().toLocalTime();
}
}
요일별 정책의 적용조건은 DayOfWeekFeeCondition 클래스로 구현한다. 이 클래스 역시 FeeCondition 인터페이스를 구현한다. 요일별 정책은 펼일이나 주말처럼 서로 인접한 다수의 요일들을 하나의 단위로 묶어 적용하는 것이 일반적이다. 따라서 여러 요일을 하나의 단위로 관리할 수 있도록 DayOfWeek의 컬렉션을 인스턴스 변수로 포함한다.
public class DayOfWeekFeeCondition implements FeeCondition {
private List<DayOfWeek> dayOfWeeks;
public DayOfWeekFeeCondition(DayOfWeek ... dayOfWeeks) {
this.dayOfWeeks = Arrays.asList(dayOfWeeks);
}
@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;
public DurationFeeCondition(Duration from, Duration to) {
this.from = from;
this.to = to;
}
@Override
public List<DateTimeInterval> findTimeIntervals(Call call) {
if (call.getInterval().duration().compareTo(from) < 0) {
return Collections.emptyList();
}
return List.of(DateTimeInterval.of(
call.getInterval().getFrom().plus(from),
call.getInterval().duration().compareTo(to) > 0 ?
call.getInterval().getFrom().plus(to) :
call.getInterval().getTo()
));
}
}
이 예제는 변경을 캡슐화해서 협력을 일관성 있게 만들면 어떤 장점을 얻을 수 있는지를 잘 보여준다. 변하는 부분을 변하지 않는 부분으로부터 분리했기 때문에 변하지 않는 부분을 재사용할 수 있다. 그리고 새로운 기능을 추가하기 위해 변하는 부분만 구현하면 되기 때문에 원하는 기능을 쉽게 완성할 수 있다.
이제 고정요금 정책만 남았다. 여러 개의 '규칙'으로 구성되고 '규칙'이 '적용조건'과 '단위요금'의 조합으로 구성되는 시간대, 요일, 구간별 정책과 달리 고정요금 정책은 '규칙'이라는 개념이 필요하지 않고 '단위요금' 정보만 있으면 충분하다.
이런 경우에 또 다른 협력 패턴을 적용? -> 그렇지 않다. 기존의 협력 패턴에 맞춘다.
어떻게 맞출까? -> FeeCondition의 서브타입을 추가한다.
public class FixedFeeCondition implements FeeCondition {
@Override
public List<DateTimeInterval> findTimeIntervals(Call call) {
return Collections.singletonList(call.getInterval());
}
}