Java/책읽기

[책읽기] 오브젝트(2) - 객체지향 프로그래밍

무토(MUTO) 2020. 12. 17. 21:57

이번 챕터에서는 어떻게 객체지향을 활용하여 프로그램을 구성할 수 있는지 학습한다.
책에 있는 영화 예매 시스템 예제를 그대로 따라할 것이다.
책을 따라하면서 객체 지향에 대한 감을 잡아보도록 하자.

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