

예시에서 주목해야 할 점은, 인스턴스 변수의 가시성은 private으로, 메서드의 가시성은 public으로 표현된 점이다.
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public LocalDateTime getStartTime() {
return whenScreened;
}
public boolean isSequence(int sequence) {
return this.sequence == sequence;
}
public Money getMovieFee() {
return movie.getFee();
}
}
어떤 부분을 외부에 공개하고, 내부에 감출 것인가?
public, protected, privateprivate 영역 안에 감추어, 변경으로 인한 혼란을 최소화 할 수 있음.Screen, Reservation, Movie
public class Screening {
// ...
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
private Money calculateFee(int audienceCount) {
return movie.calculateMovieFee(this).times(audienceCount);
}
calculateFee라는 private 메서드를 통해 요금을 계산한 후, 그 결과를 Reservation에 전달한다.calculateMovieFee에 관람객 숫자인 audienceCount를 곱한다.public class Money {
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
public static Money wons(long amount) {
return new Money(BigDecimal.valueOf(amount));
}
public static Money wons(double amount) {
return new Money(BigDecimal.valueOf(amount));
}
Money(BigDecimal amount) {
this.amount = amoun;
}
public Money plus(Money amount) {
return new Money(this.amount.add(amount.amount));
}
public Money minus(Money amount) {
return new Money(this.amount.subtract(amount.amount));
}
public Money times(double percent) {
return new Money this.amount.multiply(
BigDecimal.valueOf(percent)));
}
public boolean isLessThan(Money other) {
return amount.compareTo(other.amount) < 0 ;
}
public boolean isGreaterThanOrEqual(Money other) {
return amount.compareTo(other.amount) >= 0;
}
}
Long 대신 Money를 사용하는 경우 얻을 수 있는 장점public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
public Reservation(Customer customer, Screening screening, Money fee, int audienceCount) {
this.customer = customer;
this.screening = screening;
this.fee = fee;
this.audienceCount = audienceCount;
}
}
public class 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));
}
}
discountPolicy에게 메시지를 전송할 뿐이다.AmountDiscountPolicyPercentDiscountPolicyDiscountPolicyDiscountCondition의 리스트를 가지고 있으므로, 여러개의 할인 조건을 포함할 수 있다.calculateDiscountAmountisSatisfiedBy 메서드를 호출한다.Screening의 할인 조건을 점검해 만족 여부를 true/false로 반환한다.package screen;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
// ... ; 가변인자
// ; 메서드 인수의 갯수가 가변적일 때 인자의 갯수를 동적으로 변경할 수 있다.
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.asList(conditions);
}
public Money calculateDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
package fintate.latefee;
import screen.Screening;
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
package fintate.latefee;
import screen.Screening;
import java.time.DayOfWeek;
import java.time.LocalTime;
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;
}
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfMonth().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
package fintate.latefee;
import screen.Money;
import screen.Screening;
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;
}
}
package fintate.latefee;
import screen.Money;
import screen.Screening;
public class PercentDiscountPolicy extends DiscountPolicy {
private double percent;
public PercentDiscountPolicy(double percent, DiscountCondition ... conditions) {
super(conditions);
this.percent = percent;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return screening.getMovieFee().times(percent);
}
}

public class Money {
public Money plus(Money money) {
return new Money(this.amount.add(amount.amount));
}
public Money plus(long amount) {
reutnr new Money(this.amount.add(BiDecimal.valueOf(amount)));
}
}
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
...
}
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.asList(conditions);
}
Movie avatar = new Movie("아바타",
Duration.ofMinus(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))
)
);
Movie 내부에서
할인 정책이금액인지,비율인지를 판단하지 않는다.
내부에 정책을 결정하는 조건문이 없음에도, 어떻게 영화 요금 계산 시 해당 정책을 선택할 수 있을까?
Movie의 의존성Movie는 DiscountPolicy에 의존한다.Movie는 AmountDiscountPolicy나 PercentDiscountPolicy에 의존한다.
구현 상속 vs 인터페이스 상속
상속에는 2종류의 상속이 있다.
- 구현 상속 (implementation inheritance)
- 서브클래싱(subclassing)
- 순수하게 코드를 재사용하려는 목적으로 상속
- 인터페이스 상속 (interface inheritance)
- 서브타이핑(subtyping)
- 다형적인 협럭을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용
상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.
인터페이스 재사용이 아니라 코드 재사용을 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.
public class Movie {
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
return fee;
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
DiscountPolicy 계층에 유지시키는 것이다.public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
DiscountPolicy에서 할인 조건이 없을 경우 getDiscountAMount를 호출하지 않는다.DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킨다.getDiscountAmount()가 호출되지 않을 경우 DiscountPolicy가 0원을 반환할 것이라는 사실을 가정하고 있기 때문이다.DiscountPolicy를 인터페이스로 변경한다.NoneDiscountPolicy가 calculateDiscountAmount()를 오버라이딩 하도록 변경한다.public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
DiscountPolicy의 이름을 DefaultDiscountPolicy로 변경하고, 인터페이스를 구현하도록 수정한다.public abstract class DefaultDiscountPolicy implements DiscountPolicy {
...
}
public class NoneDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
return Money.ZERO;
}
}

어떤 설계가 더 좋은 설계일까?
NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다고 생각될 수도 있다.구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다. 우리가 작성하는 모든 코드에는 합당한 이유가 있어야 한다. 비록 아주 사소한 결정이더라도, 트레이드 오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다. 고민하고 트레이드 오프하라.

Movie의 calculateFee메서드 안에 추상 메서드인 getDiscountAmout()를 호출한다는 사실을 알고 있어야 한다.public class Movie {
private DiscountPolicy discountPolicy;
public void changeDiscountPolicy(DisocuntPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}