데이터에 집중한 Movie
클래스가 책임에 집중한 클래스와 다른 점
@Getter
@Setter
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> descountConditions;
private MovieType: movieType; //금액 할인 정책인지, 비율 할인 정책인지
private Money discountAmount;
private double discountPercent;
}
DiscountConditions
이 인스턴수 변수로 포함되어 있다.discountAmount
와 discountPercent
가 인스턴스로 포함되어 있다.MovieType
에 따라 discountAmount
와 discountPercent
중 하나만 사용될 것ReservationAgency
는 영화 예매 절차를 구현하는 클래스다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer,
int audienceCount) {
Movie movie = screening.getMovie();
boolean discountable = false;
for(DiscountCondition condition : movie.getDiscountConditions()) { // 할인 적용 여부 판단
if (condition.getType() == DiscountConditionType.PERIOD) {
discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek()) &&
condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
} else {
discountable = condition.getSequence() == screening.getSequence();
}
if (discountable) {
break;
}
}
Money fee;
if (discountable) { // 할인 적용 대상이면 금액 계산
Money discountAmount = Money.ZERO;
switch(movie.getMovieType()) {
case AMOUNT_DISCOUNT:
discountAmount = movie.getDiscountAmount();
break;
case PERCENT_DISCOUNT:
discountAmount = movie.getFee().times(movie.getDiscountPercent());
break;
case NONE_DISCOUNT:
discountAmount = Money.ZERO;
break;
}
fee = movie.getFee().minus(discountAmount).times(audienceCount);
} else {
fee = movie.getFee().times(audienceCount);
}
return new Reservation(customer, screening, fee, audienceCount);
}
}
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
}
getter
, setter
로 인해 내부 데이터가 드러나 캡슐화를 위반했다.public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer,
int audienceCount) {
// ...
Money fee;
if (discountable) { // 할인 적용 대상이면 금액 계산
// ...
}
fee = movie.getFee().minus(discountAmount).times(audienceCount);
} else {
fee = movie.getFee().times(audienceCount);
}
// ...
}
}
Money
타입의 fee
가 타입이 변경된다면 ReservationAgency
구현도 수정이 필요하다.ReservationAgency
의 변경을 유발한다.private
이라 해도 접근자와 수정자를 통해 외부로 제공하고 있다면 캡슐화를 위반한 것사각형을 표현하는 Rectangle
클래스가 있다고 해보자
@Getter
@Setter
class Rectangle {
private ing left;
private int top;
private int right;
private int bottom;
}
너비와 높이를 증가시키는 코드가 필요하다면 클라이언트에서 아래와 같이 구현할 수 있다.
class AnyClass {
void anyMethod(Rectangle rectangle, int multiple) {
retangle.setRight(rectangle.getRight() * multiple);
rectangle.setBottom(rectangle.getBottom() * multiple);
// ...
}
}
해결 방법은 아래처럼 캡슐화하는 것이다.
class Rectangle {
// ...
public void enlarge(int multiple) {
right *= multiple;
bottom *= multiple;
}
}
Rectangle
이 스스로 증가하도록 ‘책임을 이동’시킨 것public class DiscountCondition {
// ...
public DiscountConditionType getType() {
return type;
}
// 기간 할인 조건인 경우 사용하는 메서드
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) { ... }
// 순서 할인 조건인 경우 사용하는 메서드
public boolean isDiscountable(int sequence) {... }
}
public class Movie
// ...
// 금액 할인 정책일 때 사용하는 메서드
public Money calculateAmountDiscountedFee(){ ... }
// 비율 할인 정책일 때 사용하는 메서드
public Money calculatePercentDiscountedFee() { ... }
// 할인 조건 없을 때 사용하는 메서드
public Money calculateNoneDiscountedFee() { ... }
// 할인 조건 판단 메서드
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) { ... }
}
public class Screening {
// ...
public Money calculateFee(int audienceCount) { ... }
}
설계를 변경하면 클라이언트인 ReservationAgency
의 코드가 개선된다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Money fee = screening.calculateFee(audienceCount);
return new Reservation(customer, screening, fee, audienceCount);
}
}
DiscountCondition
의 문제
public class DiscountCondition {
// ...
public DiscountConditionType getType() { return type; }
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) { ... }
public boolean isDiscountable(int sequence) {... }
}
isDiscountable
의 메서드 시그니처에서 객체 내부 인스턴스가 드러난다.isDiscountable
에도 int 속성의 순번 필드가 있음이 시그니처에서 드러난다.Movie
의 문제
public class Movie
// ...
public Money calculateAmountDiscountedFee(){ ... }
public Money calculatePercentDiscountedFee() { ... }
public Money calculateNoneDiscountedFee() { ... }
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) { ... }
}
캡슐화의 진정한 의미는 데이터 뿐만 아니라 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다.
Movie
의 isDiscountable()
public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
for(DiscountCondition condition : discountConditions) {
if (condition.getType() == DiscountConditionType.PERIOD) {
if (condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
return true;
}
} else {
if (condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
DiscountCondition
의 내부 구현이 드러났기에 Movie
와의 결합도가 높다.PERIOD
) Movie
를 수정해야 한다.if-else
구문을 수정해야 한다.Screening
에 까지 변경을 전파하게 된다.DiscontCondition
, Movie
, Screening
을 함께 수정해야 한다.Screening
의 calculateFee()
public Money calculateFee(int audienceCount) {
switch (movie.getMovieType()) {
case AMOUNT_DISCOUNT:
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculateAmountDiscountedFee().times(audienceCount);
}
break;
case PERCENT_DISCOUNT:
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculatePercentDiscountedFee().times(audienceCount);
}
case NONE_DISCOUNT:
movie.calculateNoneDiscountedFee().times(audienceCount);
}
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
DiscountCondition
과 Movie
내부 구현이 인터페이스에 노출되고 있다.