기간 조건: 영화 상영 시작 시간을 이용해 할인 여부 결정, 영화 상영 시간이 특정 기간에 포함되면 요금을 할인
할인 정책(할인 방식)
금액 할인 정책
비율 할인 정책
영화 별로 하나의 할인 정책만 적용 가능하고 그 하나의 할인 정책에 대해서 다수의 할인 조건을 섞을 수 있다.
2. 객체지향 프로그래밍을 향해
협력, 객체, 클래스
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때 가능하다
이를 위해서는 두 가지에 집중해야 한다.
클래스가 필요한지 고민하지 말고 어떤 객체들이 필요한지 먼저 고민하라
어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다.
객체를 협력하는 공동체의 일원으로 봐야한다.
객체들 간의 관계를 고민하고 파악하라
객체를 고립된 존재로 바라보지 말고 협력에 참여하는 협력자로 바라봐야한다.
객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류, 이 타입을 기반으로 클래스를 구현하라
도메인의 구조를 따르는 프로그램 구조
도메인: 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
객체지향 패러다임이 강력한 이유: 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문
요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
일반적으로 클래스의 이름은 대응된 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.
클래스 구현하기
Screening 클래스
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이라는 것이다.
클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분짓는 것이다.
클래스는 외부와 내부로 구분, 클래스 설계 시 중요한 것은 어떤 부분을 외부에 공개하고 어떤 부분을 내부에 감출 지를 결정하는 것이다.
클래스의 외부와 내부를 구분해야 하는 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문, 그리고 더 중요한 것은 프로그래머에게 구현의 자유를 제공하기 때문
자율적인 객체
데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 한다.
대부분의 객체지향 프로그래밍 언어는 상태와 행동을 캡슐화하는 것에 한 걸음 더 나아가, 외부에서의 접근을 통제할 수 있는 접근 제어 메커니즘도 함께 제공한다.(접근 수정자)
객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.
퍼블릭 인터페이스: 외부에서 접근 가능한 부분
구현: 외부에서 접근 불가능, 오직 내부에서만 접근 가능한 부분
인터페이스와 구현의 분리 원칙은 객체지향 프로그래밍의 핵심 원칙
프로그래머의 자유
프로그래머의 역할을 클래스 작성자와, 클라이언트 프로그래머로 구분하는 것이 유용하다.
클래스 작성자는 프로그램에 새로운 데이터 타입을 추가하고 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
클래스 작성자: 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 꽁꽁 숨긴다.
클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지 -> 클라이언트 프로그래머에 대한 영향을 걱정하지 않고 내부 구현을 마음대로 변경
이를 구현 은닉이라고 부른다.
인터페이스와 구현을 깔끔하게 분리하기 위해 노력하라
객체의 변경을 관리할 수 있는 기법 중에서 가장 대표적인 것이 접근 제어이다.
협력하는 객체들의 공동체
영화 예매 기능 구현
Screening의 reserve 메서드가 영화를 예매한 후 예매 정보를 담고있는 Reservation의 인스턴스를 생성해서 반환한다.
public class Screening {
public Reservation reserve(Customer customer, int audienceCount){
return new Reservation
(customer, this, calculateFee(audienceCount), audienceCount);
}
}
Screening의 reserve 메서드는 calculateFee라는 private 메서드를 호출해서 요금을 계산한다.
calculateFee 메서드는 요금 계산을 위해 다시 Movie의 calculateMovieFee 메서드를 호출한다.
public class Screening {
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 Money wons(long amount){
return new Money(BigDecial.valueOf(amount));
}
public Money wons(double amount){
return new Money(BigDecial.valueOf(amount));
}
Money(BigDecimal amount){
this.amount = amount;
}
publc Money plus(Money amount){
return new Money(this.amount.add(amount.amount));
}
publc Money plus(Money amount){
return new Money(this.amount.substract(amount.amount));
}
public Money times(double percent) {
return new Money(this.amount.multiply(BigDecial.valueOf(percent)));
}
}
요금을 Long으로 다루지 않고 Money라는 새로운 데이터 타입으로 다루는 것은 객체지향의 장점을 드러낸다.
바로 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있다는 것이다.
의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 이용해서 해당 개념을 구현하라.
public class Reservation {
private Customer customer;
private Screening screening;
private Money fee;
public Reservation(Customer customer, Screening screening,
Money fee, int audienceCount){
this.customer = customer;
this.screening = screening;
this.fee = fee;
this.audienceCount = audienceCount;
}
}
영화를 예매하기 위해 Screening, Movie, Reservation 인스턴스들은 서로의 메서드를 호출하며 상호작용한다.
이러한 상호 작용을 협력이라고 부른다.
협력에 관한 짧은 이야기
객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청할 수 있다.
요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것뿐이다.
다른 객체에게 요청이 도착할 대 해당 객체가 메시지를 수신했다고 한다.
수신된 메시지를 처리하기 위한 객체 자신만의 방법을 메서드라고 부른다.
메시지와 메서드를 구분하는 것은 매우 중요하다.
메시지와 메서드의 구분에서부터 다형성의 개념이 출발한다.
3. 할인 요금 구하기
할인 요금 계산을 위한 협력 시작하기
예매 요금을 계산하는 협력을 살펴보자
Movie 클래스
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 Movie getFee(){
return fee;
}
public calculateMovieFee(Screening screening){
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
코드 어디에도 할인 정책을 특정하는 코드는 존재하지 않는다.
단지 discountPolicy에게 메시지를 전송할 뿐이다.
이 코드에는 상속과 다형성 개념이 적용되어 있다. 그리고 그 기반에는 추상화라는 원리가 숨겨져 있다.
할인 정책과 할인 조건
할인 정책은 금액 할인 정책과 비율 할인 정책으로 구분
두 가지 할인 정책을 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스로 구현하는데 두 클래스는 대부분의 코드가 유사하고 할인 요금 계산 방식만 다르기 때문에 두 클래스 사이의 중복 코드를 제거하기 위해 공통 코드를 보관할 장소가 필요
DiscountPolicy 클래스를 부모 클래스로 하여 이 클래스 안에 중복 코드를 두고 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스가 이 클래스를 상속받게 한다.
실제 애플리케이션에서는 DiscountPolicy의 인스턴스를 생성할 필요가 없기 때문에 DiscountPolicy 클래스를 추상 클래스로 구현한다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountContition ... 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 getDiscountAmount(Screening screening);
}
DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름은 정의하지만 실제로 요금을 계산하는 부분은 추상 메서드인 getDiscountAmount 메서드에게 위임한다.
결국 실제로는 DiscountPolicy 클래스를 상속받은 자식 클래스에서 오버라이딩한 메서드가 실행될 것이다.
이처럼 부모 클래스에 기본적인 알고리즘 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 템플릿 메서드 패턴이라고 한다.
할인 조건 클래스인 DiscountCondition 클래스는 인터페이스를 이용해 선언(구현)했다
SequenceCondition과 PeriodCondition은 할인 조건을 만족하는지 확인하는 기능을 한다는 것만 같고, 공통적으로 갖는 부분이 없기 때문에 인터페이스를 사용하는 것이 낫다.
public interface DiscountCondition {
boolean isSatisfied(Screening screening);
}
DiscountCondition 인터페이스를 구현한 SequenceContidion 클래스와 PeriodCondition 클래스
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence){
this.sequence = sequence;
}
@Override
pubilc boolean isSatisfied(Screening screenig){
return screening.isSequence(sequence);
}
}
public class AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount,
DiscountContion ... conditions){
super(conditons);
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)
}
}
할인 정책 구성하기
생성자 파리미터 목록을 이용하여 초기화에 필요한 정보를 전달하도록 강제하면 올바른 상태를 가진 객체의 생성을 보장할 수 있다.
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 abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountContition ... 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(20,59))
));
4. 상속과 다형성
의존성의 개념을 살펴보고 상속과 다형성을 이용해 특정 조건을 선택적으로 실행하는 방법을 알아보자
컴파일 시간 의존성과 실행 시간 의존성
Movie 클래스가 DiscountPolicy 클래스와 연결돼있다.
근데 문제는 영화 요금을 계산하기 위해서는 추상 클래스인 DiscountPolicy가 아니라 AmountDiscountPolicy와 PercentDiscountPolicy 인스턴스가 필요하다는 것이다.
Movie의 인스턴스는 실행 시에 AmountDiscountPolicy나 PercentDiscountPolicy 인스턴스에 의존해야 한다.
그러나 코드 수준에서 Movie 클래스는 이 두 클래스 중 어떤 것에도 의존하지 않는다. 오직 추상 클래스인 DiscountPolicy에만 의존하고 있다.
Movie의 인스턴스가 코드 작성 시점에서는 그 존재조차 몰랐던 AmountDiscountPolicy와 PercentDiscountPolicy의 인스턴스와 협력이 가능한 이유는 무엇인가?
Movie의 인스턴스를 생성하는 코드를 살펴봐야 한다.
Movie avatar = new Movie("아바타",
Duration.ofMinute(120),
Money.won(10000),
new AmountDiscountPolicy(Money.wons(800), ...)
);
이런 식으로 Movie 클래스의 인스턴스를 생성하도록 하면, 실행 시에 Movie의 인스턴스가 AmountDiscouontPolicy 클래스의 인스턴스에 의존하게 될 것이다.
영화 요금을 계산하기 위해 비율 할인 정책을 적용하고 싶다면 AmountDiscountPolicy 대신 PercentDiscountPolicy의 인스턴스를 전달하면 된다.
Movie avatar = new Movie("아바타",
Duration.ofMinute(120),
Money.won(10000),
new PercentDiscountPolicy(0.1, ...)
);
코드 상에서는 Movie가 DiscountPolicy에 의존한다.
하지만 실행 시점에서는 AmountDiscountPolicy나 PercentDiscountPolicy의 인스턴스에 의존하게 된다. => 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다.
클래스 사이의 의존성과 객체 사이의 의존성은 다를 수 있다.
코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다.
코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더욱 유연해지고 확장 가능해진다.
의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 잘 보여준다.
설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다.
항상 유연성과 가독성 사이에서 고민해야 한다.
클래스로의 의존성이 어떻게 실행 시점에는 인스턴스에 대한 의존성으로 바뀔 수 있는 것인지 알기 위해 상속을 살펴보자
차이에 의한 프로그래밍
상속은 기존 클래스와 매우 흡사한 클래스를 만들 때 기존 클래스의 코드를 가져와 약간만 추가하거나 수정해서 새로운 클래스를 만들 수 있도록 해준다.
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다
상속을 이용하면 클래스 사이에 관계를 성정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.
상속과 인터페이스
상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다.
상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.
자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다.
컴파일러는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용한다.
자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅이라고 부른다.
다형성
메시지와 메서드는 다른 개념이다.
코드 상에서 Movie 클래스는 DiscountPolicy 클래스에게 메시지를 전송하지만 실행 시점에 실제로 실행되는 메서드는 Movie와 실제 협력하는 실제 클래스가 무엇인지에 따라 달라진다.
Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 실제 객체의 타입이 무엇이냐에 따라 달라진다. => 이를 다형성이라고 부른다.
다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
다형성은 컴파일 시간 의존성과 실행 시간 의존성을 다르게 만들 수 있는 객체지향의 특성을 이용해 서로 다른 메서드를 실행할 수 있게 한다.
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미하기 때문에 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. 즉, 인터페이스가 동일해야 한다.
지연 바인딩, 동적 바인딩: 다형성을 구현하는 방법으로, 메시지와 메서드를 실행 시점에 바인딩하는 것
전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩 또는 정적 바인딩이라고 부른다.
상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있기 때문에 다형성을 이야기할 때 상속이 많이 언급되지만 클래스 상속만이 다형성을 구현할 수 있는 유일한 방법은 아니다.
구현 상속과 인터페이스 상속
상속을 구현 상속과 인터페이스 상속으로 분류할 수 있는데, 구현 상속을 서브클래싱이라고 부르고 인터페이스 상속을 서브타이핑이라고 부른다.
구현 상속은 순수하게 코드를 재사용할 목적으로 상속을 사용하는 것이고 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것을 인터페이스 상속이라고 부른다.
상속은 구현 상속이 아닌 인터페이스 상속을 위해 사용해야 한다.
인터페이스와 다형성
구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때가 있는데 이를 위해 C#과 자바에서는 인터페이스라는 프로그래밍 요소를 제공한다.
C++의 경우에는 추상 기반 클래스를 통해 자바의 인터페이스 개념을 구현할 수 있다.
자바의 인터페이스는 구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스를 선언한 것이다.
할인 조건은 구현을 공유할 필요가 없기 때문에 자바의 인터페이스를 이용해 타입 계층을 구현했다.
DiscountCondition을 실체화하는 클래스들은 동일한 인터페이스를 공유하며 DiscountCondition을 대신해서 사용될 수 있으며, 업캐스팅이 적용되고 협력은 다형적이다.
5. 추상화와 유연성
추상화의 힘
프로그래밍 언어 측면에서 DiscountPoliciy와 DiscountCondition이 더 추상적인 이유는 인터페이스에 초점을 맞추기 때문이다.
둘 다 같은 계층에 속하는 클래스들이 공통으로 가질 수 있는 인터페이스를 정의하며 구현의 일부(추상 클래스의 경우) 또는 전체(인터페이스의 경우)를 자식 클래스가 결정할 수 있도록 결정권을 위임한다.
추상화를 사용할 경우의 두 가지 장점
추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
추상화를 이용하면 설계가 좀 더 유연해진다.
유연한 설계
추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다. => 유연한 설계가 가능하다.
할인 금액이 적용되지 않는는, 즉 할인 금액이 0원인 경우를 위한 할인 정책 클래스를 추가 구현해보자
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening){
return Money.ZERO;
}
}
Movie statWars = new Movie("스타워즈",
Duration.ofMinutes(210),
Money.wons(10000),
new NoneDiscountPolicy());
중요한 것은 기존의 Movie와 DiscouontPolicy는 수정하지 않고 NoneDiscountPolicy라는 새로운 클래스를 추가하는 것만으로 애플리케이션의 기능을 확장했다는 것이다.
추상화를 중심으로 코드의 구조를 설계하면 유연하고 확장 가능한 설계를 만들 수 있다.
추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결함되는 것을 방지하기 때문이다.
추상 클래스와 인터페이스 트레이드오프
NoneDiscountPolicy 클래스의 코드를 자세히 살펴보면 getDiscountAmount() 메서드가 어떤 값을 반환하더라도 상관이 없다는 사실을 알 수 있다.
부모 클래스인 추상 클래스 DiscountPolicy에서 할인 조건이 없는 경우에는 getDiscountAmount() 메서드를 호출하지 않기 때문이다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountContition ... 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 getDiscountAmount(Screening screening);
}
이것은 부모 클래스인 DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킨다.
이 문제를 해결하는 방법은 DiscountPoliciy를 인터페이스로 바꾸고 NoneDiscountPolicy가 DiscountPolicy의 getDiscountAmount() 메서드가 아닌 calculateDiscountAmount() 오퍼레이션을 오버라이딩하도록 변경하는 것이다.
public interface DiscountPolicy {
public Money calculateDiscountAmount(Screening screening);
}
원래의 DiscountPolicy 클래스의 이름을 DefaultDiscountPolicy로 변경하고 DiscountPolicy 인터페이스를 구현하도록 선언
public abstract class DefaultDiscountPolicy implements DiscountPolicy{
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountContition ... 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 getDiscountAmount(Screening screening);
}
NoneDiscountPolicy가 DiscountPolicy 인터페이스를 구현하도록 선언
public class NoneDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening){
return Money.ZERO;
}
}
DiscountPolicy를 추상 클래스로 설계하는 것과 인터페이스로 설계하는 것 중 이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋은 설계라 할 수 있지만 현실적으로 NoneDiscountPolicy만을 위해 인퍼테이스를 추가하는 것은 과하다고 생각할 수도 있다.
구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다.
코드 재사용
상속은 코드를 재사용하기 위해 널리 사용되는 방법이지만 가장 좋은 방법이라고 할 수는 없다.
코드 재사용을 위해서는 상속보다는 합성이 더 좋은 방법이다.
합성은 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.
상속을 사용해 클래스를 추가하는 것은 합성을 사용하는 방법과 기능적인 관점에서 완벽히 동일한데 왜 상속대신 합성을 코드 재사용 시 선호하는 것일까?
상속
상속의 두 가지 단점
상속은 캡슐화를 위반한다.
상속은 설계를 유연하지 못하게 만든다.
상속을 이용하기 위해서는 부모 클래스의 구조를 잘 알고 있어야 하기 때문에 결과적으로 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경된 확률을 높인다.
상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다. => 실행 시점에 객체의 종류를 변경하는 것이 불가능하다. 변경하고자 하는 객체를 생성해서 상태를 복사하는 것이 최선이다.
인스턴스 변수로 연결하는 방법을 사용하면 실행 시점에 객체의 종류를 간단하게 변경할 수 있다.
public class Movie {
private DiscountPolicy discountPolicy;
public void changeDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy = discountPolicy;
}
}
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(20,59))
));
avatar.changeDiscountPolicy(new PercentDiscountPolicy(0.1, ...));
Movie가 DiscountPolicy를 포함하는 방법 역시 코드를 재사용하는 방법이다.
합성
인스턴스 변수로 연결하는 방법이 상속과 다른 점은 상속은 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합시키는 데 반해 Movie가 DiscountPolicy의 인터페이스를 통해 약하게 결합된다는 것이다.
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.
합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
구현을 효과적으로 캡슐화할 수 있고, 의존하는 인스턴스를 비교적 쉽게 교체할 수 있기 때문에 설계를 유연하게 만든다.
상속은 클래스를 통해 강하게 결합되는데 반해 합성은 메시지를 통해 느슨하게 결합된다.
코드 재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋은 방법이다.
다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수 밖에 없다.