티스토리 뷰

1) 영화 예매 시스템 

일반적인 영화 예매시스템을 따르며, 할인조건, 할인정책 구분하여 설계한다.

 


 

2) 객체 지향 프로그래밍을 향해

 

협력, 객체, 클래스

 

생각의 전환이 필요하다.

객체지향 언어에 익숙하면 설계를 시작할때
어떤클래스가 필요하지?

어떤 메서드가 필요하지?

어떤 필드가 필요하지?  가 먼저 떠오르게된다.

 

객체에 초점을 맞추자

첫째, 클래스가 아닌 어떤 객체들이 필요한지 고민해라. 클래스는 공통적인 상태와 행동을 공유하는 객체들을

추상화한 것이다.

 

둘째, 객체를 독립적인 존재가아니라 기능을 구현하기 위한 공동체의 일원이라 보자. 서로 다른 객체에게 도움을

주거나, 의존하면서 살아가는 협력적인 존재다.

 

 

 

도메인의 구조를 따르는 프로그램 구조

 

알고가기.

문제를 해결하기위해 사용자가 프로그램을 사용하는 분야를 '도메인'이라고 부른다.

 

클래스 구조 따라 그려보기

책에 나오는 구조를 draw.io를 통해 따라 그려보기 (연습)

 

영화는 여러번 상영 될 수 있고 상영은 여러번 예매 될 수 있다.

영화는 할인 정책을 할당하지 않거나 할당하더라도 하나만 할당한다.
할인 정책이 존재하는 경우에는 하나이상의 할인조건을 만족해야한다.

 

 

클래스 구현하기

 

자율적인 객체

 

어떠한 부분을 내부에만 공개할지 외부에만 공개할지 결정을 잘 하자. 

객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서이며, 
외부에서는 객체에게 원하는 것을 요청하고는 객체 스스로 최선의 방법을 결정해서 실행한다는 것을 믿고 기다려야한다.

대부분의 객체지향언어는 이 부분을  접근제어 메커니즘을 제공한다.

 

캡슐화와 접근 제어는 객체를 두 부분으로 나누는데,

1. 퍼블릭 인터페이스 - 외부에서 접근 가능한 부분

2. 구현 - 외부에서 접근 불가능하고 오직 내부에서 접근가능한 부분

인터페이스와 구현의 분리 원칙이 존재한다는 것을 알고가자.

 

 

프로그래머의 자유

 

프론트엔드백엔드 구분처럼 프로그래밍 시 클래스 작성자클라이언트 프로그래머 를 구분해보자.

클라이언트 프로그래머는 인터페이스만 알고 있으면 해당 클래스를 사용할 수 있다. 내부구현은 객체가 알아서 하도록
하는 것이다.
클래스 작성자는 인터페이스를 바꾸지 않는한 내부 구현을 마음대로 수정할 수 있다.

서로 자기가하는 역할에만 충실해서 구현, 사용하기만 하면 되는 것이다.

 

 

협력하는 객체들의 공동체

 

영화를 예매하기 위해 Screnning, Movie, Reservation 인스턴스들이 서로의 메서드를 호출하며 상호작용한다.

객체지향 프로그램을 작성할 때는 협력의 관점에서 어떤 객체가 필요한지 결정하고 객체들의 공통 상태와

행위를 구현하기 위해 클래스를 작성한다.

 

 

 

협력에 관한 짧은 이야기

 

객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 

처리 후 응답 한다.

 

메시지메서드를 구분하자.

Screening이 Movie의 '메서드를 호출한다' 고 말하지만, 사실 Screnning이 Movie에게 '메시지를 전송한다'라고 말하는 것이

적절한 표현이다.

메시지를 전달받은 Movie는 스스로 적절한 메서드를 선택하고 처리후 응답한다.

 


 

3) 할인 요금 구하기

 

 

할인 요금 계산을 위한 협력시작하기

 

Movie의 생성자를 잘 살펴보자

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }

분명 할인전략을 받고 있긴 하지만, 어떠한 할인전략(정액제, 정률제)을 사용할지 명시가 되어있지 않다.

이 코드에는 객체지향에서 중요한 개념 두가지가 숨겨져있는데,

하나는 상속, 하나는 다형성이다. 그 기반에는 추상화의 원리가 숨겨져 있다.

 

 

 

할인정책과 할인 조건

부모 클래스인 DiscountPolicy 추상클래스로 구현

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);
}

DiscountPolicy는 할인 조건 달성 여부는 결정하지만, 실제 할인 계산이 되는 부분은 추상메서드 getDiscountAmount에 위임한다.

실제로는 DiscountPolicy를 상속받는 자식클래스에서 오버라이딩한 메서드가 실행될 것이다.

이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식클래스에 위임하는 디자인 패턴을

Template Method 패턴이라고 한다.  -- 지금 생각해보니 외부 라이브러리들이 이 패턴을 많이 사용하는 것 같다.

 

 

정액제 할인 정책

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;
    }
}

 

 

정률제 할인 정책

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);
    }
}

 

 

여기서 하나 이해가 넓어진것

 

getDiscountAmount 메서드에서 screening을 파라미터를 받고 있지만, 꼭 사용해야한다는 고정관념이 있었는데,

생각해보니 너무 편협한 생각이었던 것 같다.

 

 

할인 조건 인터페이스 DiscountCondition 만들기

public interface DiscountCondition {
    boolean isSatisfiedBy(Screening screening);
}

 

두 가지 할인조건을 만들기위해 위의 인터페이스를 구현한 구현 클래스 2개를 만들도록 하자.

 

순서일치할 경우 할인

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);
    }
}

 

 

기간 포함여부에 따른 할인

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;
    }
}

 

 


 

4) 상속과 다형성

 

Movie클래스 어디에도 할인 정책이 정액제인지 정률제인지 정의되어 있지 않다.

그런데, 어떻게 요금 계산시 할인 정책을 정할 수 있을까?

이를 알기 위해서는 상속과 다형성을 알아야한다.

 

 

 

컴파일 시간 의존성과 실행 시간 의존성



코드상에서 Movie는 DiscountPolicy 인스턴스를 의존한다. 그러나 생성시점에 PercentDiscountPolicy 인스턴스를 전달하면

해당 인스턴스로 생성할 수 있다.

Ex)

Movie avartar = new Movie("아바타",
	Duration.ofMinutes(120),
    Money.wons(10000),
    new PercentDisCountPolicy(0,1,....));

여기서 중요한  키포인트는 실행시점과 컴파일 시점의 의존성이 서로 다를 수가 있다는 것이다.

 

간과하지말자.

유연한게 아무리 좋다지만, 실행, 컴파일 시점의 의존성이 다를 수록 코드는 유연해진다. 

하지만, 코드를 이해하기 어려워진다. 현재 Movie 인스턴스가 어떤 의존성을 가지고 있는지 실행시점마다 변경될 수 있기때문에

디버깅하기가 어려워질 수 있다. 이처럼 무조건 유연한 설계도, 읽기쉬운 코드도 정답이 아니다.

 

 

 

차이에 의한 프로그래밍

 

기존 클래스를 기반으로 새로운 클래스를 쉽게 빠르게 생성할 수 있는 방법을 제공하는 것이 상속이다.

DiscountPolicy를 상속받은 AmountDiscountPolicy와 PercentDiscountPolicy가 그 예시이다.

또한, 기반클래스와의 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍 이라고한다.

 

 

 

상속과 인터페이스

 

상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.

(단순히 메서드나 인스턴스 변수를 재사용하는 것이 아닌)

 

Movie의 calculateMovieFee메서드와 같이 discountPolicy.calculateDiscountamount메서드를 호출 할 때 ,

DiscountPolicy를 상속받은 인스턴스들이 이 인터페이스를 알고 있기 때문에, 해당 인스턴스들이 들어와도

메시지를 송수신 할 수 있는 것이다.

컴파일러는 코드상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용한다.

 

이처럼 자식 클래스가 부모클래스를 대신 하는 것을 업캐스팅이라고 부른다.

(자식 클래스가 위에 위치한 부모클래스로 자동적으로 타입 캐스팅되는 것처럼 보이기 때문)

 

 

 

다형성

 

메시지와 메서드의 차이를 알자.

Movie는 DiscountPolicy의 인스턴스에게 calculateDiscountAmount 메시지를 전송한다. 

이후 연결된 인스턴스가 오버라이딩된 메서드를 실행한다.

Movie가 동일한 메시지를 전송하지만, 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체 클래스가

무엇 이냐에 따라 달라지는 데, 이를 다형성이라고 부른다.

 

그리고 메시지와 메서드를 실행 시점에 바인딩 하는 것을 지연 바인딩, 동적 바인딩이라고 부른다.

그에 반해 전통적인 함수 호출 처럼 컴파일 시점에 실행될 함수나 프로시져를 결정하는 것을 초기 바인딩, 적정바인딩

이라고 부른다.

 

하나의 메시지를 선택적으로 서로 다른 메서드에 연결 할 수 있는 것은 지연바인딩이라는 메커니즘을 사용하기 때문이다.

 

 


 

추상화와 유연성

 

 

추상화의 힘 

 

영화 - 할인정책 - 할인 조건

1. 추상화의 계층만 따로 떼어놓고 보면  요구 사항의 정책을 높은 수준에서 서술 할 수 있다.

 ex) 영화 예매 요금은 최대 하나의 할인 정책과 다수의 할인 조건을 이용해 계산할 수 있다.

2. 추상화를 이용하면 설계갸 유연해진다.

다형성을 활용해 새로운 정책, 조건 등이 추가 가능하다.

 

 

 

유연한 설계

 

할인 정책이 없는 경우는 어떻게 할까??

Policy를 Null로 보내고

다음과 같이 처리 하게 되면 어떨까??

 

public Money calculateMovieFee(Screening screening) {
	if (discountPolicy == null) {
    	return fee;
    }
    
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}

 

나쁘지 않은 방법이지만, 정책 추가 처리를 하기 위해 Movie클래스를 수정해야한다. 즉 책임이 Movie클래스에 생긴다.

 

이런 경우에, NoneDiscountPolicy라는 것을 추가하여, 할인정책을 사용하지 않을 경우 , 이 정책을 사용하도록 하자.

public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

이 정책을 사용하면 Movie클래스를 수정할 필요도 없거니와, 기능상 문제 없이 작동 시킬 수 있다.

 

 

 

추상 클래스와 인터페이스의 트레이드오프

 

   public Money calculateDiscountAmount(Screening screening) {
        for (DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

 

사실 NoneDiscountPolicy는 할인조건을 사용하지 않기 때문에 getDiscountAmount가 동작하지 않는다.

이런 경우 Interface를 사용하도록 변경한 설계가 더 좋을 수 있는데,  NoneDiscounPolicy만을 위해 인터페이스로 변경하는 것은

과하다는 생각이 들 수 있다. 이처럼 구현 관련된 모든 것들이 트레이드 오프 대상이 될 수 있다는 것을 명심하자.

 

 

 

코드 재사용

 

코드 재사용에는 여러가지 방법이 있지만, 지금까지 봐왔던 방법중 하나가 상속 이고 객체 지향 설계에서 좋은 방법은 합성을 

이용하는 것이다.

예시로 들자면 Movie가 DiscountPolicy 코드를 재사용하는것이 바로 합성이다.

만약에 이것을 상속으로 사용하고자하면 Movie를 상속받는 AmountdiscountMovie, PercentDiscountMoive를 만들 수 있다.

상속대신 합성을 선호하는 이유는 무엇일까?

 

 

 

상속

 

상속은 캡슐화를 위반하며 설계를 유연하게 하지 못한다.

상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.

또한, 부모클래스를 변경하면 자식클래스에 영향이 가기 때문에 상속을 과도하게 사용한 코드는 변경하기 어려워진다.

그리고, 위에서 AmountDiscountMovie를 한번 생성하면 정책이 정해져있기 때문에 중간에 정책변경하기가 어렵다.

(그러나 기존 인스턴스 방식으로 정책을 관리하면 changeDiscountPolicy 같은 메서드를 만들어서 상태를 변경해줄 수도 있다.)

 

 

 

합성

 

Movie는 요금을 계산하기 위해 DiscountPolicy의 코드를 재사용한다. Movie가 DiscountPolicy의 인터페이스를 통해 약하게 결합되는

점을 이용하는 것이다. Movie는 DiscountPolicy의 calculateDiscountAmount 메시지를 통해 코드를 재사용한다.

이 것을 합성이라고 한다.

 

합성은 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 캡슐화가 가능하며, 의존하는 인스턴스가 쉽게

교체 될 수 있기 때문에 설계가 유연해진다.

 

상속을 무조건 사용하지 말라는 것은 아니다. 대부분의 설계에서는 상속과 합성을 적절히 섞어 사용해야한다.

Movie와 DiscountPolicy는 합성으로 연결되어있지만 DiscountPolicy, AmountDiscountPolicy, PercentDiscountPolicy는 상속관계로

되어있다. 이 처럼 중복 구현된 코드를 재사용하기 위해서 상속도 적절히 섞어서 사용할 수 있다.

 

 

 

정리

2장에서는 다른 개발자들이 작성한 라이브러리에 등장하는 템플릿 메서드 패턴을 알게되었고 항상 상속을 기피해야한다는

강압적인 생각을 가지고 있었는데, 많은 생각이 바뀌었다.

같은 관념으로 생각해왔던 메서드와 메시지의 차이 또한 폭넓게 생각하게 되었다.

항상 메서드 와 메시지를 한곳에서 해결하려했던 코드들이 많이 생각난다.

 DataBase에 치중된 코딩을 해왔던 것 같은데, 그런것들이 조금씩 깨지려하고있다. 조금 더 읽어봐야하겠지만

마지막으로 유연함에 대한 생각인데, 유연하게 만들 수록 디버깅어려워진다라고 생각하는게 나만의 생각이 아니어서 

다행이라고 생각하기도 하고 아쉽기도 했다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함