본문 바로가기
그룹 스터디 공부(IT 서적)/오브젝트

02 객체지향 프로그래밍

by hanyugyeong 2023. 7. 23.
반응형
SMALL

01 영화 예매 시스템

시스템을 구현하기 위한 요구사항은 다음과 같다.

  • 시간대 별로 영화를 예매한다.
  • 요금할인은 할인 정책(discount policy)과 할인 조건(discount condition) 2가지다.

할인 정책은 금액과 비율로 나눠지고 할인 조건은 기간과 순서로 나눠진다.

02 객체지향 프로그래밍을 향해

진정한 객체지향 패러다임의 전환은 객체에 초점을 맞춰야 얻는다.
첫째, 어떤 객체들이 필요한지 고민한다. 객체 중심의 접근 방법은 설계를 단순하고 깔끔하게 만든다
둘째, 객체를 협력하는 공동체의 일원으로 봐야한다. 훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 클래스를 낳는다.

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

도메인(domain) 이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 뜻한다. 객체지향 패러다임이 강력한 이유는 요구사항과 프로그램을 객체라는 동일한 관점에서 바라보기에 도메인 개념들을 객체와 클래스로 매끄럽게 연결할 수 있다.


일반적으로 클래스의 이름은 도메인 개념의 이름과 동일하거나 유사해야 한다.

클래스 구현하기

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

인스턴스 변수의 가시성은 private이고 메서드의 가시성은 public이다. 이는 클래스의 경계를 구분 짓는 것으로 설계 핵심은 어떤 것을 외부에 공개하고 숨길지 결정하는 것이다.

내외부를 구분하는 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이고, 프로그래머에게 구현의 자유를 제공한다.

자율적인 객체

두 가지를 알고 넘어가자.
첫째, 객체가 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재.
둘째, 객체가 스스로 판단하고 행동하는 자율적인 존재
객체지향 언어들은 접근 제어(access control) 메커니즘도 제공한다. 접근 제어를 위해 public, protected, private 같은 접근 수정자(accesee modifier) 를 제공한다. 접근을 통제하는 이유는 자율적인 존재로 만들기 위해서다.

캡슐화와 접근 제어는 객체의 두 부분으로 나뉜다. 외부에서 접근 가능한 부분을 퍼블릭 인터페이스(public interface) 라 부르고, 외부에서는 접근이 불가능하며 내부에서만 접근이 가능한 것을 구현(implementation) 이라고 부른다. 이는 인터페이스와 구현의 분리(separation of interface and implementation) 로 설계의 핵심 원칙이다.

프로그래머의 자유

프로그래머의 역할을 클래스 작성자클라이언트 프로그래머로 구분하는 것이 유용하다.

클래스 작성자: 새로운 데이터 타입을 프로그램에 추가
클라이언트 프로그래머: 클래스 작성자가 추가한 데이터 타입을 사용한다.

클라이언트 프로그래머의 목표: 필요한 클래스들을 엮어서 애플리케이션을 빠르고 안정적으로 구축하는 것
클래스 작성자: 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 꽁꽁 숨겨야 한다.

구현 은닉: 클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다.
클라이언트는 데이터 타입의 내부 구현을 무시하고 인터페이스로 사용만 한다.

협력하는 객체들의 공동체

Screening의 reserve 메서드는 영화를 예매한 후 예매 정보를 담고 있는 Reservation의 인스턴스를 생성해서 반환한다.

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

Money는 금액과 관련된 다양한 계산을 구현하는 간단한 클래스다.

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 static Money wons(double amount) {
        return new Money(BigDecimal.valueOf(amount));
    }

    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 isGreaterThanOrEqual(Money other) {
        return amount.compareTo(other.amount) >= 0;
    }

    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof Money)) {
            return false;
        }

        Money other = (Money)object;
        return Objects.equals(amount.doubleValue(), other.amount.doubleValue());
    }

    public int hashCode() {
        return Objects.hashCode(amount);
    }

    public String toString() {
        return amount.toString() + "원";
    }
}

Long 타입은 변수의 크기나 연산자의 종류와 관련된 구현 관점의 제약은 표현할 수 있지만 Money 타입처럼 저장하는 값이 금액과 관련돼 있다는 의미를 전달할 수는 없다.
또한 금액과 관련된 로직이 서로 다른 곳에 중복되어 구현되는것을 막을 수 없다.

객체지향의 장점: 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있다는 것이다.

Reservation 클래스는 고객(customer), 상영 정보(Screening), 예매 요금(fee), 인원 수(audienceCount)를 속성으로 포함한다.

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

시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration)이라고 부른다.

협력에 관한 짧은 이야기

협력(Collaboration) 이란 기능을 구현하기 위해 객체들이 상호작용하는 것을 뜻한다.

객체는 다른 객체의 공개된 인터페이스를 통해 요청(request) 할 수 있고, 요청을 받는 객체는 자율적으로 처리 후 응답(response) 한다.

이처럼 상호작용하는 유일한 방법은 메시지를 전송(send a message) 하는 것이고, 다른 객체에게 요청이 도착하면 메시지를 수신(receive a message) 했다고 한다. 메시지를 수신한 객체는 스스로 처리할 방법을 정하는데 이를 메서드(method) 라고 부른다.

메시지와 메서드를 구분하는 게 중요하다. 여기서 다형성(polymorphism) 의 개념이 출발한다

03 할인 요금을 구해보자

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

예매 요금을 계산하는 협력을 살펴보자. Movie는 제목(title)과 상영시간(runningTime), 기본요금(fee), 할인 정책(discountPolicy)을 속성으로 가진다.

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

위 코드에는 어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하지 않는다.

하나는 일정한 금액을 할인해 주는 금액 할인 정책이고 또 다른 하나는 일정한 비율에 따라 할인 요금을 결정하는 비율 할인 정책이다.

이 코드에는 객체지향에서 중요하다고 여겨지는 두 가지 개념이 숨겨져 있다. 하나는 상속이고 다른 하나는 다형성 이다. 그리고 그 기반에는 추상화 라는 원리가 숨겨져 있다.

할인 정책과 할인 조건

할인 정책은 금액 할인 정책과 비율 할인 정책으로 구분된다.
금액 할인 정책: AmountDiscountPolicy
비율 할인 정책: PercentDiscountPolicy
공통 코드: DiscountPolicy

부모 클래스인 DiscountPolicy 안에 중복 코드를 두고 AmountDiscountPolicy와 PercentDiscountPolicy가 이 클래스를 상속받게 할 것이다.

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

calculateDiscountAmount 메서드는 전체 할인 정책에 대해 차례대로 DiscountCondition의 isSatisfiedBy 메서드를 호출한다. isSatisfiedBy 메서드는 인자로 전달된 screening이 할인 조건을 만족시킬 경우에는 true를 만족시키지 못할 경우에는 false를 반환한다.

할인 조건을 만족하는 DiscountCondition이 하나라더도 존재하는 경우에는 추상 메서드인 getDiscountAmount 메서드를 호출해 할인 요금을 계산한다. 만족하는 할인 조건이 하나도 존재하지 않는다면 Screening의 getMovieFee매서드를 호출해 원래의 영화 가격을 반환한다.

DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름은 정의하지만 실제로 요금을 계산하는 부분은 추상 메서드인 getDiscountAmount 메서드에게 위임한다. 실제로는 DiscountPolicy를 상속받은 자식 클래스에서 오버라이딩한 메서드가 실행될 것이다.
이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다.

DiscountCondition은 자바의 인터페이스를 이용해 선언돼 있다. isSatisfiedBy 오퍼레이션은 인자로 전달된 screening이 할인이 가능한 경우 true 할인이 불가능한 경우에는 false를 반환한다.

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

영화 예매 시스템에는 순번 조건과 기간 조건의 두 가지 할인 조건이 존재한다.
두 가지 할인 조건은 각각 SequenceCondition과 PeriodCondition이라는 클래스로 구현할 것이다.

SequenceCondition은 할인 여부를 판단하기 위해 사용할 순번(sequence)을 인스턴스 변수로 포함한다. isSatisfiedBy 메서드는 파라미터로 전달된 Screening의 상영 순번과 일치할 경우 할인 가능한 것으로 판단해서 true, 그렇지 않을 경우에는 false를 반환한다.

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은 상영 시작 시간이 특정한 기간 안에 포함되는지 여부를 판단해 할인 여부를 결정한다.
조건에 사용할 요일(dayOfWeek)와 시작 시간(startTime),종료 시간(endTime)을 인스턴스 변수로 포함한다.

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

AmountDiscountPolicy는 DiscountPolicy의 자식 클래스로서 할인 조건을 만족할 경우 일정한 금액을 할인해주는 금액 할인 정책을 구현한다. 이 클래슨는 DiscountPolicy의 getDiscountAmount 메서드를 오버라이딩한다. 할인 요금은 인스턴스 변수인 discountAmount에 저장한다.

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 역시 DiscountPolicy의 자식 클래스로서 getDiscountAmount 메서드를 오버라이딩한다. AmountDiscountPolicy와 다른 점이라면 고정 금액이 아닌 일정 비율을 차감한다.

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

오버라이딩과 오버로딩

오버라이딩: 부모 클래스에 정의된 같은 이름, 같은 파라미터 목록을 가진 메서드를 자식 클래스에서 재정의하는 경우를 가리킨다. 자식 클래스의 메서드는 오버라이딩한 부모 클래스의 메서드를 가리기 때문에 외부에서는 부모 클래스의 메서드가 보이지 않는다.

오버로딩: 메서드의 이름은 같지만 제공되는 파라미터의 목록이 다르다. 오버로딩한 메서드는 원래의 메서드를 가리지 않기 때문에 이 메서드들은 사이 좋게 공존한다.

할인 정책 구성하기

Movie의 생성자는 오직 하나의 DiscountPolicy 인스턴스만 받을 수 있도록 선언돼 있다.

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의 생성자는 여러 개의 DiscountCondition 인스턴스를 허용한다.

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }
}

이처럼 생성자의 파라미터 목록을 이용해 초기화에 필요한 정보를 전달하도록 강제하면 올바른 상태를 가진 객체의 생성을 보장할 수 있다.

다음은 표의 '아바타'에 대한 할인 정책과 할인 조건을 설정한 것이다.할인 정책으로 금액 할인 정책이 적용되고, 두 개의 순서 조건과 두 개의 기간 조건을 이용해 할인 여부를 판단한다는 것을 알 수 있다.

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.TUESDAY,LocalTime.of(10,0),LocalTime.of(20,59))
                ));
Movie titanic = new Movie("타이타닉",
            Duration.ofMinutes(180),
            Money.wons(11000),
            new PercentDiscountPolicy(0.1,
                new PeriodCondition(DayOfWeek.TUESDAY,LocalTime.of(14,0),LocalTime.of(16,59)),
                new SequenceCondition(2),
                new PeriodCondition(DayOfWeek.TUESDAY,LocalTime.of(10,0),LocalTime.of(13,59))));

04 상속과 다형성

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

Movie는 DiscountPolicy와 연결돼 있으며, AmountDiscountPolicy와 PercentDiscountPolicy는 추상 클래스인 DiscountPolicy를 상속받는다.

이처럼, 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.

코드 상에서 Movie는 DiscountPolicy에 의존한다. 코드에서는 Movie가 AmountDiscountPolicy나 PercentDiscountPolicy에 의존하는 곳을 찾을 수는 없다. 그러나 실행 시점에는 Movie의 인스턴스는 AmountDiscountPolicy나 PercentDiscountPolicy의 인스턴스에 의존하게 된다.

코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다. 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다. 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다.

이런 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 보여준다.

  • 상황에 따라 유연성과 가독성 사이에서 고민해야 한다.

차이에 의한 프로그래밍

상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.

상속과 인터페이스

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

인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하라. 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.

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

Movie는 협력 객체가 calculateDiscountAmount라는 메시지를 이해할 수만 있다면 그 객체가 어떤 클래스의 인스턴스인지는 상관하지 않는다는 것이다. 따라서 calculateDiscountAmount 메시지를 수신할 수 있는 AmountDiscountPolicy와 PercentDiscountPolicy 모두 DiscountPolicy를 대신해서 Movie와 협력할 수 있다.

자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다.

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

다형성

메시지와 메서드는 다른 개념이라고 언급했었다.

코드상에서 Movie는 DiscountPolicy에게 메시지를 전송하지만 실행 시점에 실제로 실행되는 메서드는 수신하는 객체의 클래스에 따라 달라진다. 인터페이스가 동일할 때 수신하는 메시지에 따라 다르게 응답할 수 있는 능력을 다형성 이라 한다.

다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.

프로그램을 작성할 때 Movie 클래스는 추상 클래스인 DiscountPolicy에 의존한다.
따라서 컴파일 시간 의존성은 Movie에서 DiscountPolicy로 향한다.

실행 시점에 Movie의 인스턴스와 실제로 상호작용하는 객체는 AmountDiscountPolicy 또는 PercentDiscountPolicy의 인스턴스다.
따라서 실행 시간 의존성은 Movie에서 AmountDiscountPolicy나 PercentDiscountPolicy로 향한다.

다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.

다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
메시지와 메서드를 실행 시점에 바인딩한다는 것이다. 이를 지연 바인딩 또는 동적 바인딩 이라고 부른다.

객체지향이 컴파일 시점의 의존성과 실행 시점의 의존성을 분리하고, 하나의 메시지를 선택적으로 서로 다른 메서드에 연결할 수 있는 이유가 바로 지연 바인딩이라는 메커니즘을 사용하기 때문이다.

상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.

05 추상화와 유연성

할인 정책은 구체적인 금액 할인 정책과 비율 할인 정책을 포괄하는 추상적인 개념이다.
할인 조건 역시 구체적인 순번 조건과 기간 조건을 포괄하는 추상적인 개념이다.

DiscountPolicy는 AmountDiscountPolicy와 PercentDiscountPolicy보다 추상적이고
DiscountCondition은 SequenceCondition과 PeriodCondition보다 추상적이다.

추상화를 사용할 경우의 두 가지 장점

1) 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다는 것이다.
-> 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다.
영화의 예매 가격을 계산하기 위한 흐름은 항상 Movie에서 DiscountPolicy로,그리고 다시 DiscountCondition을 향해 흐른다.
재사용 가능한 설계의 기본을 이루는 디자인 패턴 이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 매커니즘을 활용하고 있기 때문이다.

2) 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다. 설계를 유연하게 만들 수 있다.

유연한 설계

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

할인 정책의 경우에는 할인할 금액을 계산하는 책임이 DiscountPolicy의 자식 클래스에 있었지만 할인 정책이 없는 경우에는 할인 금액이 0원이라는 사실을 결정하는 책임이 DiscountPolicy가 아닌 Movie쪽에 있기 때문이다.

이 경우에 일관성을 지킬 수 있는 방법은 0원이라는 할인 요금을 계산할 책임을 그대로 DiscountPolicy 계층에 유지시키는 것이다.

public class NoneDiscountPolicy implements DiscountPolicy {
    @Override
    public Money calculateDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

유연성이 필요한 곳에 추상화를 사용하라

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

부모 클래스인 DiscountPolicy에서 할인 조건이 없을 경우에는 getDiscountAmount() 메서드를 호출하지 않는다.

이 문제를 해결하는 방법은 DiscountPolicy를 인터페이스로 바꾸고 NoneDiscountPolicy가 DiscountPolicy의 getDiscountAmount() 메서드가 아닌 calculateDiscountAmount() 오퍼레이션을 오버라이딩하도록 변경하는 것이다.

public interface DiscountPolicy {
    Money calculateDiscountAmount(Screening screening);
}

원래의 DiscountPolicy 클래스를 DefaultDiscountPolicy로 변경

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

NoneDiscountPolicy

public class NoneDiscountPolicy implements DiscountPolicy {
    @Override
    public Money calculateDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

코드 재사용

코드 재사용을 위해서는 상속보다는 합성이 더 좋은 방법이라는 이야기를 많이 들었을 것이다.
합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.

합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.

상속

상속의 문제점은 2가지로 분류된다.

첫째, 캡슐화를 위반

둘째, 설계가 유연하지 못하다.

자식이 부모의 세부사항을 알아야 해서 캡슐화를 위반한다고 하는 것이고, 상속을 거듭하여 계층이 쌓일수록 부모를 수정할 수 없게 된다. 또한 컴파일 시점에 의존성이 결정된다.

합성은 인터페이스의 메시지를 통해서만 재사용되기에 캡슐화에 문제가 없고 실행 시점에 객체를 선택할 수 있어서 설계에 유연성이 생긴다. 상속의 문제점 2가지를 모두 해결하는 셈이다.

하지만 다형성을 위해 인터페이스를 재사용하기 위해서는 상속과 합성을 함께 사용할 수밖에 없다.

반응형
LIST

'그룹 스터디 공부(IT 서적) > 오브젝트' 카테고리의 다른 글

05 책임 할당하기_02  (0) 2023.08.09
05 책임 할당하기_01  (0) 2023.08.07
04 설계 품질과 트레이드 오프  (0) 2023.07.31
03 역할,책임,협력  (0) 2023.07.24
01.객체, 설계  (0) 2023.07.23