이번 챕터에서는 어떻게 객체지향을 활용하여 프로그램을 구성할 수 있는지 학습한다.
책에 있는 영화 예매 시스템 예제를 그대로 따라할 것이다.
책을 따라하면서 객체 지향에 대한 감을 잡아보도록 하자.
0. 요구사항 살펴보기
- 사용자가 온라인 영화 예매 시스템을 활용해서 영화를 예매할 수 있다.
- 할인조건에 맞는 예매자는 요금을 할인받을 수 있다.
- 할인조건은 순서조건과, 기간조건 두가지가 있다.
- 기간조건은 요일 시작시간 종료시간 세부분으로 구성되며 영화 시작시간이 해당 기간안에 포함될 경우 요금을 할인한다.
- 할인 조건에 해당하는 예매자는 할인정책에 따라 요금을 할인받을 수 있다.
- 할인정책은 금액할인정책과 비율할인 정책이 있다.
- 영화별로 하나의 할인정책만 할당할 수 있다. 할인정책을 지정하지 않는것도 가능하다.
- 예매를 완료하면 다음과 같은 데이터가 예매자에게 전달되어야한다.
정보 | 내용 |
---|---|
제목 | 아바타 |
상영정보 | 2019년 12월 26일(목) 7회 6:00(오후)~8:00(오후) |
인원 | 2명 |
정가 | 20,000원 |
결제금액 | 18,400원 |
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체드링 필요할까를 먼저 생각해야한다.
- 또한 객체를 독립적으로 바라보지말고 기능을 구현하기위해 협력하는 공동체의 일원으로 봐야한다.
- 해당 기능의 도메인을 구조도로 그려보자면 다음과 같다.
이제 이 구조를 바탕으로 책에 나오는 예제를 구현해보도록 하자.
Screening.java
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();
}
public Reservation reserve(Customer customer, int audienceCount) {
return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
}
private Money calculateFee(int audienceCount) {
return movie.calculateMovieFee(this).times(audienceCount);
}
}
- 위 클래스는 상영에 대한 정보를 담고있다. 해당 상영이 오늘 상영관에서 몇 번째 상영인지, 어떤 영화인지, 시작시간은 어떻게 되는지에 대한 정보를 담고있다.
- 이 클래스를 잘 살펴보면 private과 public 접근제어자를 잘 활용하여 구현은 숨기고 인터페이스만 외부로 드러내고있다.
- 또한 자율적으로 자신의 내부에서 다른 객체들과 협력하여 수행해야 할 역할들을 잘 수행한다.
Money.java
package chapter2;
import java.math.BigDecimal;
public class Money {
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
public static Money wons(long amount) {
return new Money(BigDecimal.valueOf(amount));
}
public Money(BigDecimal amount) {
this.amount = amount;
}
public Money plus(Money amount) {
return new Money(this.amount.add(amount.amount));
}
public Money minus(Money amount) {
return new Money(this.amount.subtract(amount.amount));
}
public Money times(double percent) {
return new Money(this.amount.multiply(BigDecimal.valueOf(percent)));
}
public boolean isLessThan(Money other) {
return amount.compareTo(other.amount) < 0;
}
public boolean isGreaterThan(Money other) {
return amount.compareTo(other.amount) >= 0;
}
}
- 저번에는 Long fee의 형태로 가격을 구현했지만 이번에는 Money라는 클래스를 구현하여 해당 돈을 관리하는 로직을 한군데로 모아놨다.
- 이러한 형태로 하나의 로직을 다루는 클래스를 따로 구현해놓는다면 소프트웨어가 더 유연해져 다양한 요구사항에 대응하기 쉬워진다.
Reservation.java
public class Reservation {
private Customer customer;
private Screening screening;
private Money money;
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;
}
}
- 해당 코드는 예약을 하는데 필요한 정보들을 제공하는 클래스이다.
Movie.java
지금부터 나오는 코드들은 유심히 잘 살펴보아햐 한다. 객체지향적으로 코드를 작성하는 방법 분명하게 드러나는 부분이기 때문이다.
package chapter2;
import java.time.Duration;
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 getFee() {
return fee;
}
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
DiscountPolicy.java
import java.util.Arrays;
import java.util.List;
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions;
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;
}
abstract protected Money getDiscountAmount(Screening screening);
}
DiscountPolicy.java
import java.util.Arrays;
import java.util.List;
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions;
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;
}
abstract protected Money getDiscountAmount(Screening screening);
}
DiscountCondition.java
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
SequenceCondition.java
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
PeriodCondition.java
import java.time.DayOfWeek;
import java.time.LocalTime;
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;
}
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
AmountDiscountPolicy.java
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.java
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();
}
}
-
해당 코드를 유심히 살펴봐야한다.Movie클래스는 분명 할인정책을 받아서 영화의 요금을 할인하지만 어떤 할인정책을 선택할 것인지에 대한 내용은 전혀 적혀있지 않다.
-
이 코드에선 상속, 다형성을 사용하여 부모 클래스에서 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식클래스에게 위임하는 탬플릿 메서드 패턴을 사용하였다.
-
이것을 이해하기 위해서는 자바의 컴파일타임과 런타임을 구분할 수 있어야 한다.
-
코드상으로는 컴파일타임에 Movie객체는 DiscountPolicy에 의존성을 가지고있다. 그러나 런타임에서는 Movie의 인스턴스를 생성하는 코드에 하위 구현 클래스를 인자로 전달한다면 다형성에 의해서 하위 구현 클래스에 의존하게 된다.
-
이러한 디자인패턴은 다양한 상황에 대응할 수 있는 유연한 코드가 될 수 있다. 그러나 반면에 의존성을 제대로 파악하기가 어렵다는 단점이 있다. 따라서 코드를 이해하기가 더욱 어려워지고 디버깅하기 힘들어진다. 이 기회비용을 잘 고민해야하는 것이 바로 객체지향이다.
-
컴파일 타임에 메시지와 메서드가 바인딩되지않고 런타임에 바인딩되는것을 지연바인딩 또는 동적바인딩이라고 부른다.
추상클래스와 인터페이스
상속과 컴포지션
'Java > 책읽기' 카테고리의 다른 글
[책읽기] 오브젝트(1) - 객체, 설계 (0) | 2020.12.10 |
---|