예시에서 주목해야 할 점은, 인스턴스 변수의 가시성은 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
, private
private
영역 안에 감추어, 변경으로 인한 혼란을 최소화 할 수 있음.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
에게 메시지를 전송할 뿐이다.AmountDiscountPolicy
PercentDiscountPolicy
DiscountPolicy
DiscountCondition
의 리스트를 가지고 있으므로, 여러개의 할인 조건을 포함할 수 있다.calculateDiscountAmount
isSatisfiedBy
메서드를 호출한다.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;
}
}