1. 영화 예매 시스템 설명
- 영화와 상영의 의미
영화 : 제목, 상영시간, 가격정보처럼 영화가 가진 기본적인 정보
상영 : 상영일자, 시간, 순번 등
=> 사용자가 실제로 예매하는 대상은 영화가 아니라 상영
- 할인 조건이란?
할인조건 | |
순서 조건 | 상영 순번을 이용해 할인 여부 결정 ex) 조조 할인 -매번 첫번째 상영 |
기간 조건 | - 영화 상영 시작시간을 이용해 할인여부 결정 - 요일, 시작시간, 종료시간 ex) 월요일 오전 10시 -오후 1시 기간 동안 할인 혜택 |
-할인 정책이란?
할인 정책 | 설명 |
금액 할인 | 일정 금액을 할인하는 정책 ex) 800원 할인 |
비율 할인 | 일정 비율을 할인하는 정책 ex) 10% 할인 |
2. 객체지향 프로그래밍을 향해
1) 어떤 클래스가 필요한지 고민하기 전에 어떤 객체가 필요한지 고민하라
- 클래스 윤곽을 위해선 어떤 객체가 어떤 상태와 행동을 가지는지 먼저 결정해야 한다
2) 객체를 독립적 존재가 아니라 협력하는 공동체의 일원으로 보아라
- 협력의 문맥을 기반으로 객체를 생각하라
- 객체를 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라
도메인 구조를 따르는 프로그램 구조
도메인 : 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
ex) 영화 예매시스템의 도메인 == 영화를 좀더 쉽게 빠르게 예매하려는 사용자의 문제
클래스 구현하기
영화 : Movie
상영 : Screening
할인 정책 : DiscountPolicy
- 금액 할인 : AmountDiscountPolicy
- 비율 할인 : PercentDiscountPolicy
할인 조건 : DiscountCondition
- 순번 조건 : SequenceCondition
- 기간 조건 : PeriodCondition
상영 클래스
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();
}
}
[필드 설명]
movie : 상영할 영화
sequence : 상영 순서
whenScreened : 상영 시작 시간
[메서드 설명]
isSequence : 매개변수 순번과 같은 순번인지 확인
getMovieFee() : 영화 비용
인스턴스 변수의 가시성은 private이고, 메서드의 가시성은 public이다
== 경계의 명확성 => 객체의 자율성 + 구현의 자유
자율적인 객체
- 1) 객체는 상태와 행동을 지닌다
- 2) 객체는 스스로 판단하고 행동하는 자율적인 존재이다
- 객체는 접근 제어자를 통해 인터페이스와 구현을 분리했다
- 즉, 상태를 숨기고 행동만을 공개해야 한다
프로그래머의 자유
프로그래머의 역할은 클래스 작성자와 클라이언트 프로그래머로 나뉜다
클래스 작성자 : 필요한 부분만을 공개하고 나머지는 꽁꽁숨겨야 한다
=> 구현 은닉
구현 은닉의 기대효과
- 클라이언트 프로그래머 : 공개된 인터페이스만 알면 됨(알아야할 지식이 줌)
- 클래스 작성자 : 변경의 폭이 넓어짐
협력하는 객체들의 공동체
Screnning 객체
> reserve(Customer customer, int audeinceCount) : 영화 예매 기능
- customer : 예매자 정보
- audienceCount : 인원 수
public class Screening {
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount),
audienceCount);
}
}
> calculateFee(int audienceCount) : 요금 계산 기능
- caulateMovieFee를 통해 1인당 예매 요금 산출
- 인원수만큼 곱하여 반환
public class Screening {
private Money calculateFee(int audienceCount) {
return movie.calculateMovieFee(this).times(audienceCount);
}
}
Money 클래스 : 간단한 VO
public class Money {
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
Money(BigDecimal amount) {
this.amount = amount;
}
}
=> Long으로 사용가능한데 왜 Money 객체를 만들어주었나?
- 이유1. 저장하는 값이 금액과 관련있다는 의미를 전달
- 이유2. 금액 관련 로직이 서로 다른 곳에 중복되어 구현되는 것을 막음
=> 도메인 의미를 더 명시적으로 표현(유연성과 명확성의 증가)
Reservation 클래스
고객, 상영정보, 예매 요금, 인원수 속성으로 포함
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;
}
}
협력에 관한 짧은 이야기
메시지와 메서드는 다르다
- 객체가 다른 객체와 상호작용하는 유일한 방법은 메세지 전송이다.
- 다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신했다고 표현한다
- 객체는 자신만의 방법(메서드)로 메시지를 처리한다
ex) Screening이 Movie의 calculateMovieFee를 호출
= Screening이 Movie에게 calculateMovieFee 메시지를 전송
= Movie는 Screening으로부터 calculateMovieFee 메시지 수신
= Movie는 각자만의 방법(메서드)으로 calculateMovieFee에 응답
3. 할인 요금 구하기
할인요금 계산을 위한 협력 시작하기
- Movie의 calculateMovieFee는 할인 정책으로부터 할인요금을 전달받음
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 calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening)); //할인 요금만큼 금액을 뺌
}
}
할인 정책 : DiscountPolicy
- 추상 클래스로 구현
- TEMPLATE METHOD 패턴
: 부모 클래스에 기본적인 알고리즘 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴
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; // 아무 조건도 만족하지 못했다면 0원 반환
}
abstract protected Money getDiscountAmount(Screening Screening);
}
구현된 두 할인정책
> AmountDiscountPolicy(금액 할인 정책)
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; // 일정 금액만큼 할인
}
}
> PercentDiscountPolicy(비율 할인 정책)
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); // 영화금액의 일정 비율만큼 할인
}
}
할인 조건 : DiscountCondition
- 인터페이스로 구현
- isSatisfiedBy(Screening screening)으로 조건이 만족하는지 확인
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
구현된 두 할인 조건
> SequenceCondition (순번 조건)
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence); // 순번이 일치하는지 확인
}
}
> PeriodCondition(기간 조건)
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;
}
public boolean isSatisfiedBy(Screening screening) { //기간이 일치하는지 확인
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
> 영화 계산에 참여하는 클래스 다이어그램
ex) 생성자 파라미터 목록을 통해 초기화에 필요한 정보를 전달하도록 강제할 수 있다
- Movie에서는 오직 하나의 할인 정책만을
- DiscountPolicy에서는 여러개의 DiscountCondition을 받음
생성 예시
Movie avatar = new Movie("아바타",
Duration.ofMinutes(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))
)
);
4. 상속과 다형성
컴파일 시간 의존성과 런타임 의존성
- 컴파일 타임 코드 의존성과 런타임 의존성이 서로 다를 수 있다
- Movie는 컴파일 타임에 DiscountPolicy에만 의존한다.
- 그러나, 런타임에는AmountDiscountPolicy 혹은 PercentDiscountPolicy에 의존한다
=> 이러한 차이는 설계를 유연하게 해주나, 가독성을 해친다(연결지점을 정확히 파악해야 함)
차이에 의한 프로그래밍
: 부모 클래스와 다른 부분만 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법
상속과 인터페이스
: 상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문
== 자식 클래스는 부모 클래스가 수신가능한 모든 메시지를 수신가능하다
ex)
public class Movie {
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening)); //할인 요금만큼 금액을 뺌
}
}
- Movie는 할인 정책의 calculateDiscountAmount 메시지를 전송한다.
- Movie 입장에서는 자신과 협력하는 객체가 어떤 클래스의 인스턴스인지가 중요하지 않다.
- 오직, calculateDiscountAmount를 수신가능한지가 중요하다
=> 따라서 서브 클래스인 AmountDiscountPolicy와 PercentDiscountPolicy는 DiscountPolicy를 대신할 수 있다 (업캐스팅)
다형성
: 동일한 메시지를 수신했을 때 객체 타입에 따라 다르게 응답할 수 있는 능력
: 런타임에 메시지에 대한 메서드를 선택하는 것
>> 동적 바인딩 vs 정적 바인딩
동적 바인딩 => 메시지와 메서드를 런타임에 바인딩
정적 바인딩 => 메시지와 메서드를 컴파일 시점에 바인딩
5. 추상화와 유연성
추상화의 장점
1. 일반화된 요구사항 서술
- 높은 수준의 서술 : 영화 예매 요금은 최대 하나의 할인정책과 다수의 할인 조건을 이용해 계산한다
- 낮은 수준의 서술 : 영화의 예매 요금은 금액 할인 정책과 두개의 순서조건, 한개의 기간 조건을 이용해 계산한다
=> 필요에 따라 표현의 수준을 조정 가능함
2. 유연한 설계
기존 구조를 수정하지 않고도 쉽게 새로운 기능 추가 가능
ex) 할인 정책이 없는 경우 추가
방법1. 예외처리 => 일관성이 없어짐
public Money calculateMovieFee(Screening screening) {
if(discountPolicy == null){
return fee;
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
방법2. NoneDiscountPolicy 확장
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
=> Movie와 DiscountPolicy를 수정하지 않고 확장 가능
=> 컨텍스트 독립성
추상클래스와 인터페이스 트레이드 오프
NoneDiscountPolicy의 문제점
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;
}
}
- DiscountPolicy에서 List<DiscountCondition>이 없으면 바로 Money.ZERO를 반환함
=> 따라서 getDiscountAmount에서 아무 값이나 반환해도 상관이 없어짐
대안 : DiscountPolicy를 인터페이스화 하고, 기존 추상 클래스를 DefaultDiscountPolicy로 구현
- 개념적인 혼란과 결합을 제거할 수 있음
=> 모든 코드에는 합당한 이유가 있어야 한다
=> 비록 아주 사소한 결정이더라도 트레이드오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다
상속보다 Composition
> 상속의 두 가지 단점
1. 캡슐화 위반
: 하위 클래스가 상위 클래스를 잘 알고 있어야 한다
2. 유연한 설계 불가
: 컴파일 시점에 부모 클래스와 자식 클래스의 관계를 결정
ex) 실행 시점에 할인 정책을 변경해야 할 때
- 상속은 인스턴스 자체를 바꾸어 주어야 한다
- 그러나 조합은 필드만 바꾸어 껴주면 된다
public class Movie {
public void changeDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
}
> 조합의 보완점
- 유연한 설계가 가능하다
- 상속은 부모와 자식이 컴파일 타임에 강하게 결합하는데 반해
- 조합은 인터페이스를 통해 약하게 결합된다
but, 다형성을 위해 인터페이스를 재사용해야 할 때는 상속+조합을 함께 사용할 수 밖에 없다
'기술 서적' 카테고리의 다른 글
[오브젝트] ch4. 설계 품질과 트레이드 오프 / 데이터 중심 설계의 문제점 (0) | 2024.04.11 |
---|---|
[오브젝트] ch3. 역할, 책임, 협력 (0) | 2024.04.11 |
[오브젝트] ch1. 객체, 설계 (0) | 2024.04.07 |
[스터디] 이펙티브 자바 - item4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.03.12 |
[좋은 코드, 나쁜 코드] ch1-ch2 (0) | 2024.03.05 |