GRASP 패턴으로 설계하기
General Responabity Assaignment Software Pattern
책임위주로 설계해보자
도메인 설계
처음 사용자가 보낼 메세지를 중심으로 도메인을 설계해보자!
영화 상영 예매를 기준으로 설계하면 아래와 같다.
완벽한 도메인 설계는 없고 대충 객체간의 메세지 전달이 이런식으로 되겠구나~ 하는 틀만 잡는거라 정확할 필요는 없다.
메세지를 기반으로 생각하기
책임을 기반으로 변경에 강한 설계를 하려면?
첫번째로 도메인 설계를 하고
두번째로 메세지를 기반으로 각 객체에게 책임을 할당하는거다.
메세지를 어디로 전달 할껀지를 생각할때, 정보 전문가가 누구인지를 생각하라
즉, 예매하라 라는 메세지를 줄건데, 예매 정보전문가는 상영 객체가 좋겠다.
예매하라 -> 상영 클래스
하지만, 상영 클래스는 예매를 하는 과정중에 금액을 계산하여야하는데, 금액을 계산하기 위한 정보 전문가는 아니다.
그래서, 금액을 계산하라 하는 메세지는 영화 클래스에 주자
영화 금액 계산하라 -> 영화 클래스
영화 클래스는 할인 조건에 대한 정보가 없으므로
할인조건에 부합하는지 할인조건 클래스에 "할인조건에 부합하는가?" 라는 메세지를 주어야 한다.
할인조건에 부합하는가? -> 할인조건 클래스
이렇게.. 하나의 메세지(동작)가 여러개의 메세지(작은 동작)으로 나뉘어 질 수 있다.
step 1의 코드 수정
chapter 5 - step1의 DiscountCondition 클래스의 응집도가 낮다.
그래서 클래스를 분리하여 캡슐화해야한다.
왜냐하면, isSatisfiedByPeriod 와 isSatisfiedBySequence가 한 클래스안에 들어있다.
두번째 이유는, 초기화 할때 전체 필드를 초기화 하지않는다.
기간 조건일때, 모든 필드를 사용하지 않고 기간에 관련된 필드만 사용하고 sequence 같은 변수를 아예 사용하지 않는다.
이 때문에, 기간 조건과 순서조건을 다른 클래스로 나눌 필요가 있다.
두개의 클래스로 분리하기
ㅇㅋ 그럼 기간 조건과 순서조건으로 두개의 클래스를 분리한다.
분리 코드는 step 2를 참고하라
문제점?
이렇게 하면... 영화 클래스에 기간 조건과 순서조건 모두 들고 있어야하네...
할인조건을 추가하려면? 영화 클래스에도 할인조건을 추가해야줘야하네? 겁나 불편하네
즉, 캡슐화가 덜 됐네.
그래서... 할인조건이라는 추상클래스나 인터페이스로 만들어줘야 하는게 보인다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
private List<PeriodCondition> periodConditions;
private List<SequenceCondition> sequenceConditions;
'
'
'
인터페이스로 분리하기
자세한 코드는 step 3에 있다.
영화는 할인조건 인터페이스에 할인여부를 판단하라 라는 메세지를 던지고,
할인조건은 기간, 순서로 적용할지 정한다. 하나의 메세지(동작,책임)에 세부 동작을 구현할 수 있다면,
이를 GRASP에서는 다형성(polymorphism)이라고 한다.
인터페이스로 분리하여 movie 클래스에는 할인조건이 추가되어도 따로 필드를 추가해주지 않아도 된다.
이를 GRASP에서 PROTECTED VARIATION 이라고한다.
Movie 추상클래스로 만들기
Moive는 할인 정책을 enum으로 데이터를 두어서 할인정책을 결정한다..
그래서 step 3의 movie 클래스를 보면 %정책일때, 할인금액만큼 할인하는 정책일때, 정책이 없을때를 swich문으로 결정하고, 각각 정책을 계산하는 메서드를 지정한걸 볼 수 있다..
이거 봐도 캡슐화 안되어 있고 책임이 분산되어 있다. 영화를 추상클래스로 만들어서 할인조건을 판단하고 할인 금액을 계산하는 메서드는 남겨두되, 할인정책별로 할인 금액을 계산하는 메서드는 분리하자
step 4의 movie 클래스와 PercentageMovie이다.
package org.eternity.movie.step04;
import org.eternity.money.Money;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
public abstract class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
public Movie(String title, Duration runningTime, Money fee, DiscountCondition... discountConditions) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountConditions = Arrays.asList(discountConditions);
}
//할인금액 계산
public Money calculateMovieFee(Screening screening) {
if (isDiscountable(screening)) {
return fee.minus(calculateDiscountAmount());
}
return fee;
}
//할인조건 반복문으로 할인이 가능한지?
private boolean isDiscountable(Screening screening) {
return discountConditions.stream()
.anyMatch(condition -> condition.isSatisfiedBy(screening));
}
protected Money getFee() {
return fee;
}
//할인정책에 따라 계산
abstract protected Money calculateDiscountAmount();
}
package org.eternity.movie.step04;
import org.eternity.money.Money;
import java.time.Duration;
public class PercentDiscountMovie extends Movie {
private double percent;
public PercentDiscountMovie(String title, Duration runningTime, Money fee, double percent,
DiscountCondition... discountConditions) {
super(title, runningTime, fee, discountConditions);
this.percent = percent;
}
@Override
protected Money calculateDiscountAmount() {
return getFee().times(percent);
}
}
이렇게 정책별로 캡슐화를 할 수 있다.
변경에 강하게 만들기
그렇다면, 오전에는 %할인정책, 오후에는 금액별 할인정책을 시행한다면,
즉, 할인정책을 런타임 도중에 바뀌어야 한다면, 2장에서 언급했듯 합성을 사용하면된다.
%할인 정책적용 영화 , 금액할인정책 적용 영화 이렇게 클래스를 만들지 말고,
인터페이스로 DiscountPolicy를 사용하여 언제든지 할인 정책을 바꿀 수 있게 하자.
2장의 코드를 참고하라..
절차지향 -> 객체지향
처음부터 객체지향, 책임 주도 설계를 하기 어려울때는,
절차지향적으로 코드를 작성하고 객체지향적으로 변경 -> 책임주도 설계로 바꾸는거도 좋다.
이를 리팩토링이라고 한다.
4장의 step1 ReservationAgency 코드이다.
할인 여부를 판단하고, 할인금액을 정책별로 계산한다.
package org.eternity.movie.step01;
import org.eternity.money.Money;
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 ReservationAgency {
public Reservation reserve(Screening screening, Customer customer,
int audienceCount) {
boolean discountable = checkDiscountable(screening);
Money fee = calculateFee(screening, discountable, audienceCount);
return createReservation(screening, customer, audienceCount, fee);
}
private boolean checkDiscountable(Screening screening) {
return screening.getMovie().getDiscountConditions().stream()
.anyMatch(condition -> condition.isDiscountable(screening));
}
private Money calculateFee(Screening screening, boolean discountable,
int audienceCount) {
if (discountable) {
return screening.getMovie().getFee()
.minus(calculateDiscountedFee(screening.getMovie()))
.times(audienceCount);
}
return screening.getMovie().getFee();
}
private Money calculateDiscountedFee(Movie movie) {
switch(movie.getMovieType()) {
case AMOUNT_DISCOUNT:
return calculateAmountDiscountedFee(movie);
case PERCENT_DISCOUNT:
return calculatePercentDiscountedFee(movie);
case NONE_DISCOUNT:
return calculateNoneDiscountedFee(movie);
}
throw new IllegalArgumentException();
}
private Money calculateAmountDiscountedFee(Movie movie) {
return movie.getDiscountAmount();
}
private Money calculatePercentDiscountedFee(Movie movie) {
return movie.getFee().times(movie.getDiscountPercent());
}
private Money calculateNoneDiscountedFee(Movie movie) {
return movie.getFee();
}
private Reservation createReservation(Screening screening,
Customer customer, int audienceCount, Money fee) {
return new Reservation(customer, screening, fee, audienceCount);
}
}
책임 주도 설계 변경
2장 코드 확인
'오브젝트' 카테고리의 다른 글
6장 메세지와 인터페이스 (0) | 2025.01.14 |
---|---|
4장 설계품질과 트레이드 오프 (0) | 2025.01.03 |
3장 역할, 책임, 협력 (0) | 2024.12.27 |
2장 객체지향 프로그래밍 (1) | 2024.12.13 |