객체지향 설계의 핵심은 역할, 책임, 협력이다. 협력은 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용이다. 책임은 객체가 다른 객체와 협력하기 위해 수행하는 행동이고, 역할은 대체 가능한 책임의 집합이다.
이번 장에서는 영화 예매 시스템을 책임이 아닌 상태를 표현하는 데이터 중심의 설계를 살펴보고 객체지향적으로 설계한 구조와 어떤 차이점이 있는지 살펴보겠다.
데이터 중심의 관점에서 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의한다. 책임 중심의 관점에서 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다. 데이터 중심의 관점은 객체의 상태에 초점을 맞추고 책임 중심 관점은 객체의 행동에 초점을 맞춘다.
데이터 중심의 설계란 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할하는 방법이다. 그리고 객체가 내부에 저장해야 하는 '데이터가 무엇인가'를 묻는 것으로 시작한다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
}
책임 중심의 Movie
와의 차이점은 할인 조건의 목록(discountConditions
)이 인스턴스 변수 Movie 안에 직접 포함돼 있다는 것이다. 또한 할인 정책을 DiscountPolicy
라는 별도의 클래스로 분리했던 이전 예제와 달리 금액 할인 정책에 사용되는 할인 금액(discountAmount
)과 비율 할인 정책에 사용되는 할인 비율(discountPercent
)을 Movie
안에서 직접 정의하고 있다.
할인 정책은 영화별로 오직 하나만 지정할 수 있기 때문에 한 시점에 discountAmount
와 discountPercent
중 하나의 값만 사용될 수 있다. 그렇다면 영화에 사용된 할인 정책의 종류를 알 수 있을까? 할인 정책의 종류를 결정하는 것이 바로 movieType이다.
public enum MovieType {
AMOUNT_DISCOUNT,
PERCENT_DISCOUNT,
NONE_DISCOUNT
}
이제 필요한 데이터를 준비했다. 객체지향의 가장 중요한 원칙은 캡슐화이므로 내부 데이터가 객체의 엷은 막을 빠져나가 외부의 다른 객체들을 오염시키는 것을 막아야 한다. 가장 간단한 방법은 내부의 데이터를 반환하는 접근자(accessor)와 데이터를 변경하는 수정자(mutator)를 추가하는 것이다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
public MovieType getMovieType() {
return movieType;
}
public void setMovieType(MovieType movieType) {
this.movieType = movieType;
}
public List<DiscountCondition> getDiscountConditions() {
return Collections.unmodifiableList(discountConditions);
}
public void setDiscountConditions(List<DiscountCondition> discountConditions) {
this.discountConditions = discountConditions;
}
public Money getDiscountAmount() {
return discountAmount;
}
public void setDiscountAmount(Money discountAmount) {
this.discountAmount = discountAmount;
}
public double getDiscountPercent() {
return discountPercent;
}
public void setDiscountPercent(double discountPercent) {
this.discountPercent = discountPercent;
}
}
Movie
를 구현하는 데 필요한 데이터를 결정했고 메서드를 이용해 내부 데이터를 캡슐화하는 데도 성공했다. 이제 할인 조건을 구현해보자. 할인 조건의 타입을 저장한 DiscountConditionType
을 정의하자.
public enum DiscountConditionType {
SEQUENCE, // 순번 조건
PERIOD // 기간 조건
}
할인 조건을 구현하는 DiscountCondition
은 할인 조건의 타입을 저장할 인스턴스 변수인 type을 포함한다. 또한 movieType
의 경우와 마찬가지로 순번 조건에서만 사용되는 데이터인 상영 순번, 기간 조건을 포함한다.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
캡슐화를 위해 메서드를 추가하자.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public DiscountConditionType getType() {
return type;
}
public void setType(DiscountConditionType type) {
this.type = type;
}
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(DayOfWeek dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public LocalTime getStartTime() {
return startTime;
}
public void setStartTime(LocalTime startTime) {
this.startTime = startTime;
}
public LocalTime getEndTime() {
return endTime;
}
public void setEndTime(LocalTime endTime) {
this.endTime = endTime;
}
public int getSequence() {
return sequence;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
}
이어서 Screening 클래스를 구현하자.
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Movie getMovie() {
return movie;
}
public int getSequence() {
return sequence;
}
public LocalDateTime getWhenScreened() {
return whenScreened;
}
public void setMovie(Movie movie) {
this.movie = movie;
}
public void setSequence(int sequence) {
this.sequence = sequence;
}
public void setWhenScreened(LocalDateTime whenScreened) {
this.whenScreened = whenScreened;
}
}
영화 예매 시스템의 목적은 영화를 예매하는 것이다. Reservation
클래스를 추가하자.
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
public Reservation(Customer customer, Screening screening, Money money, int audienceCount) {
this.customer = customer;
this.screening = screening;
this.money = money;
this.audienceCount = audienceCount;
}
public Customer getCustomer() {
return customer;
}
public Screening getScreening() {
return screening;
}
public Money getFee() {
return fee;
}
public int getAudienceCount() {
return audienceCount;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public void setScreening(Screening screening) {
this.screening = screening;
}
public void setFee(Money fee) {
this.fee = fee;
}
public void setAudienceCount(int audienceCount) {
this.audienceCount = audienceCount;
}
}
Customer는 고객의 정보를 보관하는 간단한 클래스다.
public class Customer {
private String name;
private String id;
public Customer(String name, String id) {
this.name = name;
this.id = id;
}
}
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();
case PERCENT_DISCOUNT -> discountAmount = movie.getFee().times(movie.getDiscountPercent());
case NONE_DISCOUNT -> discountAmount = Money.ZERO;
}
fee = movie.getFee().minus(discountAmount);
} else {
fee = movie.getFee();
}
return new Reservation(customer, screening, fee, audienceCount);
}
}
reserve 메서드는 크게 두 부분으로 나눌 수 있다. 첫 번째는 DiscountCondition에 대해 루프를 돌면서 할인 가능 여부를 확인하는 for 문이고, 두 번째는 discountable 변수의 값을 체크하고 적절한 할인 정책에 따라 예매 요금을 계산하는 if문이다.
데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도를 사용하겠다.
상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다. 여기서 구현이란 나중에 변경될 가능성이 높은 어떤 것을 가리킨다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 조절할 수 있는 장치를 제공하기 때문이다.
변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다는 사실을 기억하라. 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.
지금까지 설명한 내용에서 알 수 있는 것처럼 객체지향에서 가장 중요한 원리는 캡슐화다.
응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다.
결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다.
응집도가 높을수록 변경의 대상과 범위과 명확해지기 때문에 코드를 변경하기 쉬워진다. 변경으로 인해 수정되는 부분을 파악하기 위해 코드 구석구석을 헤매고 다니거나 여러 모듈을 동시에 수정할 필요가 없으며 변경을 반영하기 위해 오직 하나의 모듈만 수정하면 된다.
낮은 결합도를 가진 왼쪽 설계에서는 모듈 A를 변경했을 때 오직 하나의 모듈만 영향 받는다는걸 알 수 있다. 반면 높은 결합도를 가진 오른쪽의 설계에서는 모듈 A를 변경했을 때 4개의 모듈을 동시에 변경해야 한다.
데이터 중심의 설계가 가진 문제점을 다음과 같이 요약할 수 있다.
public class Movie {
private Money fee;
public Money getFee() {
return fee;
}
public void setFee(Money fee) {
this.fee = fee;
}
위 코드는 직접 객체의 내부에 접근할 수 없기 때문에 캡슐화 원칙을 지키는 것처럼 보인다. 하지만 접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.
데이터 중심 설계는 접근자와 수정자를 통해 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화를 위반한다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
// ...
Money fee;
if (discountable) {
// ...
fee = movie.getFee().minus(discountAmount);
} else {
fee = movie.getFee();
}
// ...
}
}
이 코드에서 알 수 있는 것처럼 ReservationAgency
는 한 명의 예매 요금을 계산하기 위해 Movie
의 getFee
메서드를 호출하며 계산된 결과를 Money
타입의 fee
에 저장한다. 이때 fee
의 타입을 변경한다고 가정해보자.
이를 위해서는 getFee
메서드의 반환 타입도 함께 수정해야 할 것이다. 그리고 getFee
메서드를 호출하는 ReservationAgency
의 구현도 변경된 타입에 맞게 함께 수정해야 할 것이다.
영화 예매 시스템을 살펴보면 대부분의 제어 로직을 ReservationAgency
가 모든 데이터 객체에 의존한다는 것을 알 수 있다. DiscontCondition
의 데이터가 변경되면 DiscontCondition
뿐만 아니라 ReservationAgency
도 함께 수정해야 한다. Screening
의 데이터가 변경되면 Screening
뿐만 아니라 ReservationAgency
도 함께 수정해야 한다. ReservationAgency
는 모든 의존성이 모이는 결합도의 집결지다.
서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.
아마 다음과 같은 수정사항이 발생하는 경우에 ReservationAgency
의 코드를 수정해야 할 것이다.
낮은 응집도는 두 가지 측면에서 설계에 문제를 일으킨다.
캡슐화는 설계의 제 1원리다.
속성의 가시성을 private
으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.
public class Rectangle {
private int left;
private int right;
private int getRight;
private int bottom;
public Rectangle(int left, int right, int getRight, int bottom) {
this.left = left;
this.right = right;
this.getRight = getRight;
this.bottom = bottom;
}
public int getLeft() {
return left;
}
public int getRight() {
return right;
}
public int getGetRight() {
return getRight;
}
public int getBottom() {
return bottom;
}
public void setLeft(int left) {
this.left = left;
}
public void setRight(int right) {
this.right = right;
}
public void setGetRight(int getRight) {
this.getRight = getRight;
}
public void setBottom(int bottom) {
this.bottom = bottom;
}
}
이 사각형의 너비와 높이를 증가하는 코드가 필요하다고 가정해보자. 아마 이 코드는 Rectangle 외부의 어떤 클래스 안에 다음과 같이 구현돼 있을 것이다.
public class AnyClass {
void anyMethod(Rectangle rectangle, int multiple) {
rectangle.setRight(rectangle.getRight() * multiple);
rectangle.setBottom(rectangle.getBottom() * multiple);
// ...
}
}
이 코드에는 많은 문제점이 있다. 첫 번째는 '코드 중복'이 발생할 확률이 높다. 다른 곳에서도 사각형의 너비와 높이를 증가시키는 코드가 필요하다면 아마 그곳에서도 getRight
와 getBottom
메서드를 호출해서 right와 bottom을 가져온 후 수정자 메서드를 이용해 값을 설정하는 유사한 코드가 존재할 것이다.
두 번째 문제점은 '변경에 취약'하다는 점이다. Rectangle
이 right와 bottom 대신 length와 height를 이용해서 사각형을 표현한다고 수정한다고 가정해보자. 접근자와 수정자는 내부 구현을 인터페이스의 일부로 만들기 때문에 현재의 Rectangle
클래스는 int 타입의 top, left, right, bottom이라는 4가지 인스턴스 변수의 존재 사실을 인터페이스를 통해 외부에 노출시키게 된다.
해결 방법은 캡슐화를 강화시키는 것이다. Rectangle
내부에 너비와 높이를 조절하는 로직을 캡슐화하면 두 가지 문제를 해결할 수 있다.
class Rectangle {
public void enlarge(int multiple) {
right *= multiple;
bottom *= multiple;
}
}
우리는 방금 Rectangle
을 변경하는 주체를 외부의 객체에서 Rectangle
로 이동시켰다. 즉, 자신의 크기를 Rectangle
스스로 증가시키도록 '책임을 이동'시킨 것이다.
우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
따라서 객체를 설계할 때 다음과 같은 두 개의 개별적인 질문으로 분리해야 한다.
다시 영화 예매 시스템 예제로 돌아가 ReservationAgency
로 새어나간 데이터에 대한 책임을 실제 데이터를 포함하고 있는 객체로 옮겨보자.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
두 번째 질문은 이 데이터에 대해 수행할 수 있는 오퍼레이션은 무엇인가를 묻는 것이다.
public class DiscountCondition {
public DiscountConditionType getType() {
return type;
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
if (type != DiscountConditionType.PERIOD) {
throw new IllegalArgumentException();
}
return this.dayOfWeek.equals(dayOfWeek) &&
this.startTime.compareTo(time) <= 0 &&
this.endTime.compareTo(time) >= 0
}
public boolean isDiscountable(int sequence) {
if (type != DiscountConditionType.SEQUENCE) {
throw new IllegalArgumentException();
}
return this.sequence == sequence;
}
이제 Movie
를 구현하자. 첫 번째 질문은 Movie
가 어떤 데이터를 포함해야 하는가를 묻는 것이다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
}
두 번째 질문은 이 데이터에 대해 수행할 수 있는 오퍼레이션은 무엇인가를 묻는 것이다. Movie
가 포함하는 데이터를 보면 영화 요금을 계산하는 오퍼레이션과 할인 여부를 판단하는 오퍼레이션이 필요할 것 같다.
public class Movie {
public Money calculateAmountDiscountedFee() {
if (movieType != MovieType.AMOUNT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(discountAmount);
}
public Money calculatePercentDiscountedFee() {
if (movieType != MovieType.PERCENT_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee.minus(fee.times(discountPercent));
}
public Money calculateNoneDiscountFee() {
if (movieType != MovieType.NONE_DISCOUNT) {
throw new IllegalArgumentException();
}
return fee;
}
}
Movie
는 DiscountCondition
의 목록을 포함하기 때문에 할인 여부를 판단하는 오퍼레이션 역시 포함해야 한다. isDiscountable
메서드를 추가하자.
public class Movie {
public boolean isDiscountable(LocalDateTime whenScreend, int sequence) {
for (DiscountCondition condition : discountConditions) {
if (condition.getType() == DiscountConditionType.PERIOD) {
if (condition.isDiscountable(whenScreend.getDayOfWeek(), whenScreend.toLocalTime())) {
return true;
}
} else {
if (condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
}
Movie
의 isDiscountable 메서드는 discountConditions
에 포함된 DiscountCondition
을 하나씩 훑어 가면서 할인 조건의 타입을 체크한다. 만약 할인 조건이 기간 조건이라면 DiscountCondition
의 isDiscountable(DayOfWeek, dayOfWeek, LocalTime whenScreend)
를 호출하고 순번 조건이라면 DiscountCondition
의 isDiscountable(int sequence)
메서드를 호출한다.
이제 Screening
을 살펴보자.
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 Money calculateFee(int audienceCount) {
switch (movie.getMovieType()) {
case AMOUNT_DISCOUNT -> {
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculateAmountDiscountedFee().times(audienceCount);
}
}
case PERCENT_DISCOUNT -> {
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculatePercentDiscountedFee().times(audienceCount);
}
}
case NONE_DISCOUNT -> {
return movie.calculateNoneDiscountFee();
}
}
return movie.calculateNoneDiscountFee();
}
}
ReservationAgency
는 Screening
의 calculateFee
메서드를 호출해 예매 요금을 계산한 후 계산된 요금을 이용해 Reservation
을 생성한다.
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Money fee = screening.calculateFee(audienceCount);
return new Reservation(customer, screening, fee, audienceCount);
}
}
두 번째 설계가 결합도 측면에서 ReservationAgency
에 의존성이 몰려있던 첫 번째 설계보다는 개선된 것으로 보인다.
두번째 설계에서는 데이터를 처리하는 데 필요한 메서드를 데이터를 가지고 있는 객체 스스로 구현하고 있따. 따라서 이 객체들은 스스로를 책임진다고 말할 수 있다.
아직도 두 번째 설계에서도 문제가 발생한다. 그 이유를 살펴보자.
분명히 수정된 객체들은 자기 자신의 데이터를 스스로 처리한다. 예를 들어 DiscountCondition
은 자기 자신의 데이터를 이용해 할인 가능 여부를 스스로 판단한다.
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public DiscountConditionType getType() {
// ...
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
// ...
}
public boolean isDiscountable(int sequence) {
// ...
}
}
DiscountCondition
에 구현된 두 개의 isDiscountable
메서드를 자세히 살펴보면 이상한 점이 몇 군데 눈에 띈다.
isDiscountable(DayOfWeek dayOfWeek, LocalTime time)
메서드의 시그니처를 살펴보면 DayOfWeek
타입의 요일 정보와 LocalTime
타입의 시간 정보를 파라미터로 받는 것을 알 수 있다. 요일과 이 메서드는 객체 내부에 요일과 시간 정보가 인스턴스 변수로 포함돼 있다는 사실을 인터페이스를 통해 외부에 노출하고 있는 것이다. 두 번째 isDiscountable(int sequence)
메서드 역시 객체가 int 타입의 순번 정보를 포함하고 있음을 외부에 노출한다. 비록 setType
메서드는 없지만 getType
메서드를 통해 내부에 DiscountConditionType
을 포함하고 있다는 정보 역시 노출시키고 있다.
만약 DiscountCondition
의 속성을 변경해야 한다면 어떻게 될까? 아마도 두 isDiscountable
메서드의 파라미터를 수정하고 해당 메서드를 사용하는 모든 클라이언트도 함께 수정해야 할 것이다. 내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect)는 캡슐화가 부족하다는 증거다.
Movie
역시 캡슐화가 부족하기는 마찬가지다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
public MovieType getMovieType() {
// ...
}
public Money calculateAmountDiscountedFee() {
// ...
}
public Money calculatePercentDiscountedFee() {
// ...
}
public Money calculateNoneDiscountFee() {
// ...
}
}
Movie
역시 내부 구현을 인터페이스에 노출시키고 있다. 여기서 노출시키는 것은 할인 정책의 종류다. 할인 정책에는 금액 할인, 비율 할인, 미적용 세 가지가 존재한다는 사실을 드러내고 있다.
캡슐화 위반으로 인해 DiscountCondition
의 내부 구현이 외부로 노출됐기 때문에 Movie
와 DiscountCondition
사이의 결합도는 높을 수밖에 없다.
public boolean isDiscountable(LocalDateTime whenScreend, int sequence) {
for (DiscountCondition condition : discountConditions) {
if (condition.getType() == DiscountConditionType.PERIOD) {
if (condition.isDiscountable(whenScreend.getDayOfWeek(), whenScreend.toLocalTime())) {
return true;
}
} else {
if (condition.isDiscountable(sequence)) {
return true;
}
}
}
return false;
}
DiscountCondition
의 기간 할인 조건의 명칭이 PERIOD에서 다른 값으로 변경되면 Movie
를 수정해야 한다.DiscountCondition
의 종류가 추가되거나 삭제된다면 Movie
안의 if ~ else 구문을 수정해야 한다.DiscountCondition
의 만족 여부를 판단하는 데 필요한 정보가 변경된다면 Movie
의 isDiscountable
메서드로 전달된 파라미터를 변경해야 한다. 이로 인해 Movie
의 isDiscountable
메서드 시그니처도 변경될 것이고 결과적으로 메서드에 의존하는 Screening
에 대한 변경을 초래할 것이다.이번에 Screening
을 살펴보자. DiscountCondition
이 할인 여부를 판단하는 데 필요한 정보가 변경된다면 Movie
의 isDiscountable
메서드로 전달해야 하는 파라미터 종류를 변경해야 하고, 메서드를 호출하는 부분도 함께 수정해야 한다.
public class Screening {
public Money calculateFee(int audienceCount) {
switch (movie.getMovieType()) {
case AMOUNT_DISCOUNT -> {
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculateAmountDiscountedFee().times(audienceCount);
}
}
case PERCENT_DISCOUNT -> {
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculatePercentDiscountedFee().times(audienceCount);
}
}
case NONE_DISCOUNT -> {
return movie.calculateNoneDiscountFee();
}
}
return movie.calculateNoneDiscountFee();
}
}
데이터 중심의 설계가 변경에 취약한 이유는 두 가지다.