Closed YJGwon closed 2 months ago
ReservationAgency
에 집중되어 있는 로직을 각각의 객체로 캡슐화하는 작업은 이미 진행했습니다. (2번의 작업)
Screening
과 Movie
에서 할인 금액 계산이라는 책임을 분리해볼 수 있을 것 같아요. 할인에 대한 행동과 상태를 DiscountPolicy
라는 객체로 떼어내볼 수 있을 것 같습니다.
MovieType
보다는 DiscountType
네이밍이 맞는 것 같아 이름을 변경해봤습니다. public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(final Movie movie, final int sequence, final LocalDateTime whenScreened) {
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public Money calculateFee(final int audienceCount) {
return movie.calculateFee(whenScreened, sequence).times(audienceCount);
}
}
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(final String title, final Duration runningTime, final Money fee, final DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
public Money calculateFee(LocalDateTime whenScreened, int sequence) {
return fee.minus(discountPolicy.calculateDiscountAmount(whenScreened, sequence));
}
}
isDiscountable
라는 구현을 캡슐화할 수 있게 되었네요!public class DiscountPolicy {
private DiscountType discountType;
private Money discountAmount;
private double discountPercent;
private List<DiscountCondition> discountConditions;
public Money calculateDiscountAmount(LocalDateTime whenScreened, int sequence, Money movieFee) {
if (!isDiscountable(whenScreened, sequence)) {
return Money.ZERO;
}
return switch (discountType) {
case AMOUNT_DISCOUNT -> discountAmount;
case PERCENT_DISCOUNT -> movieFee.times(discountPercent);
case NONE_DISCOUNT -> Money.ZERO;
};
}
private boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for (DiscountCondition condition : discountConditions) {
if (condition.getType().equals(DiscountConditionType.PERIOD)) {
if (condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
} else {
if (condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
}
할인
에 대한 정보를 너무 많이 가지고 있었는데 해당 로직이 DiscountPolicy
에 캡슐화되면서 책임이 명확해지고 가독성도 더 좋아진 것 같아요.DiscountType
로 역할을 나누고 있는데, 추상클래스로 나누면 훨씬 좋을 것 같네요!
isDiscountable
이라는 구현을 같이 사용해보려 합니다.isDiscountable
의 접근제한자를 private
에서 protected
로 수정했습니다. (상속받은 객체들에서 사용해야 하기 때문에)public abstract class DiscountPolicy {
private List<DiscountCondition> discountConditions;
public DiscountPolicy(final List<DiscountCondition> discountConditions) {
this.discountConditions = discountConditions;
}
public Money calculateDiscountAmount(LocalDateTime whenScreened, int sequence, Money movieFee) {
if(isDiscountable(whenScreened, sequence)) {
return calculateDiscountPolicyAmount(whenScreened, sequence, movieFee);
}
return Money.ZERO;
}
protected boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for (DiscountCondition condition : discountConditions) {
if (condition.getType().equals(DiscountConditionType.PERIOD)) {
if (condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
} else {
if (condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
abstract protected Money calculateDiscountPolicyAmount(
LocalDateTime whenScreened, int sequence, Money movieFee);
}
public class AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(
final Money discountAmount,
final List<DiscountCondition> discountConditions) {
super(discountConditions);
this.discountAmount = discountAmount;
}
@Override
public Money calculateDiscountPolicyAmount(
final LocalDateTime whenScreened, final int sequence, final Money movieFee) {
return discountAmount;
}
}
public class PercentDiscountPolicy extends DiscountPolicy {
private double discountPercent;
public PercentDiscountPolicy(
final double discountPercent,
final List<DiscountCondition> discountConditions) {
super(discountConditions);
this.discountPercent = discountPercent;
}
@Override
public Money calculateDiscountPolicyAmount(
final LocalDateTime whenScreened, final int sequence, final Money movieFee) {
return movieFee.times(discountPercent);
}
}
public class NoneDiscountPolicy extends DiscountPolicy {
public NoneDiscountPolicy(final List<DiscountCondition> discountConditions) {
super(discountConditions);
}
@Override
public Money calculateDiscountPolicyAmount(
final LocalDateTime whenScreened, final int sequence, final Money movieFee) {
return Money.ZERO;
}
}
밸이 위에서 단계적 개선을 4가지 단계로 말씀해주셨는데요! 1번이 안된 상태에서 캡슐화하면 적절하지 못한 책임이 캡슐화될 것 같아요. (왠지 모비닥에는 그런 경우가 많은 것 같아요...!) 그래서 모비닥 만약에 리팩토링하게 된다면, 설계부터 그려본 후에 캡슐화를 시도해볼 것 같습니다!
희망편은 처음 설계부터 객체중심적으로 해나가는 것일 테지만, 실제로 실무에서는 이미 데이터 중심적으로 만들어진 코드를 다루게 될 일도 많은 것 같아요. 이미 데이터 중심적으로 설계된 코드는 변경이 어려워서 리팩토링 역시 큰 비용을 수반하게 되는데요, 이럴 경우에 어떤 시점에 리팩토링을 해야 비용 측면에서 실용적인 리팩토링을 할 수 있을까요? 또, 어떻게 리팩토링해야 단계적으로 개선해나갈 수 있을까요?