객체지향
영화관 예매 시스템을 구현하면서 객체지향에 대해 설명한다. 영화관 예매 시스템을 다이어 그램으로 표현하면 다음과 같다.
객체, 클래스, 협력
프로래밍할때, 클래스 단위로 먼저 생각하지 말고 하나의 객체단위로 먼저 생각해라
하나의 기능을 구현할때 객체의 상호협력을 통해서 만들어야 좋은 설계이다.
접근제어자
public , private 으로 객체의 메서드, 필드변수를 내부, 외부로 노출 유무를 판단한다.
-> 클래스 작성자는 private 로 지정한 것들을 걱정없이 수정할 수 있다.
-> 클라이언트 프로그래머(클래스로 기능을 구현하는)는 인터페이스만을 보고 구현할 수 있다.
접근제어자는 유용하다~~
협력하는 객체들의 공동체
예매하기
그럼 실제로 하나의 기능을 구현하기 위해서 객체들의 협력을 통해 코드를 구현해보자
Screen 클래스의 reserve 메서드이다. 영 클래스에서 예매를 하면 예매 객체 생성하여 반환한다.
이때, 영화 비용을 계산해야하는데, 영화 할인정책은 moive 클래스에서 할인정책을 가져와서 사용한다.
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);
}
할인 정책
이때, 할인정책은 여러개이니 추상 클래스나 인터페이스를 만들어서 중복되는 코드를 줄일 수 있다.
예를 들면, 할인정책에서의 여러개의 할인조건들?, 할인 조건에 부합하면 할인하는 로직
금액 할인정책과 비율 할인정책을 추상클래스인 할인정책의 상속을 받아 구현할것이다.
보면 getDiscountAmount 메서드 즉, 할인조건에 부합하면 할인을 계산하는 로직은 추상 메서드로 남겨두었다.
각각 할인정책별로 금액으로 할인할지, %로 할인할지 달라야하기 때문이다.
중간의 로직을 자식 클래스에게 위임하는 패턴을 TEMPLEATE METHOD 라고 한다.
상속으로 중복을 없애는 방법을 차이에 의한 프로그래밍(programming of difference)이라고 한다.
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;
}
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;
}
}
할인조건
할인조건은 순서조건과 기간조건이 있다.
할인조건 클래스는 동일한 isSatisfiedBy 메서드가 있도록 인터페이스를 구현한다. (동일성 유지)
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
영화비를 계산하기 위해서, 할인 정책과 할인조건의 협력으로 영화비를 할인할 수 있었다.
다이어 그램으로 표현하면 다음과 같다.
상속과 다형성
코드시점과 실행시점의 의존성차이
코드 시점(실행전일때) movie 클래스 입장에서 DiscountPolicy 만을 가지고 있다. 이걸로는 movie 클래스가 어떤 할인정책을 가지는지 알 수 없다. (movie 클래스만 볼때)
하지만, 실행시점에서는 movie 클래스가 금액할인정책인지, %할인정책인지 알 수 있다.
public static void main(String[] args) {
Movie movie = new Movie("아바타", Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(10000)),...//할인조건들 );
}
영화 클래스이다.
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;
}
다형성?
이처럼 movie 클래스 생성자에서 AmountDiscoutPolicy 를 사용하면 할인을 계산하는 메서드를 사용할때, AmountDiscoutPolicy에 오버라이드된 메서드를 사용할 것이고, PercentageDiscountPolicy를 사용하면... 알겠죠
실행시점에서 무슨 메서드를 사용할지 정해지는것이 다형성이라고 한다.
(할인 조건을 인터페이스로 구현한것도 다형성)
이처럼 실행시간에서 할인정책을 변경한다.
public void changeDiscountPolicy(DiscountPolicy newPolicy) {
discountPolicy = newPolicy;
}
추상 클래스와 인터페이스 트레이드오프
영화에 할인정책이 없을 경우는?
이렇게 구현하면 된다. 하지만, 할인로직(정책이 없을경우)의 책임이 movie 클래스로 이동하기 때문에 클래스간의 협력관계의 일관성이 무너진다.
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null){
return Money.ZERO;
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
noneDiscountPolicy 를 만들자 이러면, 일관성도 유지하고 정책이 없을 경우의 로직을 생성함
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
그러면, 왜? DiscountPolicy 에서 getDiscountAmount 가 실행되지 않기 때문에, 로직이 깔끔하지 않다.
그러면 DiscountPolicy 를 추상 클래스 말고 인터페이스로 변경하자!
public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
나머지 할인정책들은 DefaultPolicy 로 이동한다.
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DefaultDiscountPolicy(DiscountCondition... conditions) {
this.conditions = Arrays.asList(conditions);
}
@Override
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를 제거하고 바로 Moive 를 상속받아 각각 구현한다면
가독성도 떨어지고 만약, 실행시점에서 금액정책에서 퍼센트정책으로 변경한다면, 인스턴스를 복사해야한다.
ex) 12시 부터는 퍼센트 할인 적용
그래서, 인터페이스, 추상 클래스를 사용해서 DiscountPolicy 를 movie 가 사용하게 하는것이 합성이다.
다만, DiscountPolicy는 상속을 사용했기 때문에, 상속을 아예 사용하지 말라는건 아니다. 사용은 해야한다.
'오브젝트' 카테고리의 다른 글
6장 메세지와 인터페이스 (0) | 2025.01.14 |
---|---|
5장 책임을 할당하기 (0) | 2025.01.07 |
4장 설계품질과 트레이드 오프 (0) | 2025.01.03 |
3장 역할, 책임, 협력 (0) | 2024.12.27 |