2장에서는 객체지향 프로그래밍을 구성하는 요소와 기법을 살펴보았다.
우선, 객체지향은 객체를 지향하는 것이다.
어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민해야 한다고 말한다.
즉, 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다는 것이다.
그리고 객체는 홀로 존재하는 것이 아니다.
다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재이다.
객체를 협력적인 존재로 간주하는 것은 설계를 유연하고 확장 가능하게 한다.
문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라 한다.
아래는 책에서 사용한 영화 예약과 관련한 도메인이다.
이런 도메인을 구성하는 개념들을 프로그램의 객체와 클래스로 표현해야 한다.
일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 유사하게 짓는다.
또한, 클래스 사이의 관계도 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 만들어
프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.
책에서는 도메인 개념에 대응하는 클래스 구조를 아래와 같이 설계하였다.
객체와 관련한 중요한 두 가지 사실이 있다.
하나는 상태(State)와 행동(Behavior)을 함께 가지는 복합적인 존재라는 것이고,
다른 하나는 스스로 판단하고 행동하는 자율적인 존재라는 것이다.
객체는 상태(데이터)와 기능(행동)을 함께 묶는 캡슐화를 통해 복합적인 존재가 되며,
접근 수정자를 이용한 접근 제어 메커니즘을 통해 자율적인 존재가된다.
객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(Request)할 수 있다.
요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(Response)한다.
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것뿐이다.
메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.
이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(Method)라고 부른다.
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));
}
public void ChangeDiscountPolicy(DiscountPolicy discountPolicy)
{
this.discountPolicy = discountPolicy;
}
}
Movie 클래스의 CalculateMovieFee 메서드는
discountPolicy에 CalculateDiscountAmount 메시지를 전송해 할인 요금을 반환받는다.
이후, 기본 요금인 fee에서 반환된 할인 요금을 차감한다.
여기서 흥미로운 점은 구체적으로 어떤 할인 정책을 사용할지 결정하는 코드는
Movie 클래스 내부에 존재하지 않는다는 것이다.
도메인 정보에 따르면 할인 정책은 금액 할인 정책과 비율 할인 정책으로 구분된다.
할인 정책을 결정하는 코드가 없는데, 어떻게 할인 정책을 적용할 수 있을까?
이 질문에 답하기 위해서는 상속과 다형성에 대해 알아야 한다.
두 가지 할인 정책, 금액 할인 정책과 비율 할인 정책을
AmountDiscountPolicy와 PercentDiscountPolicy 클래스로 구현한다고 가정하자.
두 클래스는 대부분의 코드가 유사하고 할인 요금을 계산하는 방식만 조금 다를 것이다.
따라서 아래와 같이 DiscountPolicy 안에 중복 코드를 작성하고 이를 상속받도록 할 수 있다.
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
이를 이용하면 클래스 사이의 관계를 설정하는 것만으로 기존 클래스가 가지고 있는
모든 속성과 행동을 새로운 클래스에 포함시켜 사용할 수 있다.
또한 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 추가할 수 있다.
public abstract class DiscountPolicy
{
private List<DiscountCondition> conditions = new List<DiscountCondition>();
public DiscountPolicy(params DiscountCondition[] conditions)
{
this.conditions = conditions.ToList();
}
public Money CalculateDiscountAmount(Screening screening)
{
foreach (var condition in conditions)
{
if (condition.IsSatisfiedBy(screening))
{
return GetDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money GetDiscountAmount(Screening screening);
}
public class AmountDiscountPolicy : DiscountPolicy
{
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, params DiscountCondition[] conditions) : base(conditions)
{
this.discountAmount = discountAmount;
}
protected override Money GetDiscountAmount(Screening screening)
{
return discountAmount;
}
}
public class PercentDiscountPolicy : DiscountPolicy
{
private double percent;
public PercentDiscountPolicy(double percent, params DiscountCondition[] conditions) : base(conditions)
{
this.percent = percent;
}
protected override Money GetDiscountAmount(Screening screening)
{
return screening.GetMovieFee().Times(percent);
}
}
이런 상속이 가치있는 이유는 단순히 코드를 재사용할 수 있다는 것을 넘어서
부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있다는 점이다.
인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한 것이다.
상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.
결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기에
외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
따라서 Movie의 생성자에서 인자의 타입이 DiscountPolicy임에도 아래와 같이
AmountDiscountPolicy와 PercentDiscountPolicy의 인스턴스를 전달할 수 있는 것이다.
(컴파일러는 코드 상의 부모 클래스 위치에서 자식 클래스를 사용하는 것을 허용한다.)
Movie avatar = new Moive("아바타",
Duration.OfMinutes(120),
Money.Wons(10000),
new AmountDiscountPolicy(Money.Wons(1000))
);
Movie Titanic = new Moive("타이타닉",
Duration.OfMinutes(120),
Money.Wons(10000),
new PercentDiscountPolicy(0.1)
);
이를 기억하고 Movie 클래스의 코드를 다시 살펴보자.
코드상 DiscountPolicy에 CalculateDiscountAmount라는 메시지를 전송한다.
하지만 해당 메시지를 처리하기 위해 실행 시점에 실제로 실행되는 메서드는
Movie와 협력하는 객체의 실제 클래스(생성자에 주입되는 클래스)가 무엇인지에 따라 달라진다.
Movie와 협력하는 실제 클래스들(AmountDiscountPolicy와 PercentDiscountPolicy)은
상속을 통해 CaclulateDiscountAmount 메시지를 처리할 수 있는 메소드를 갖고 있기에
Movie가 보내는 메시지에 응답할 수 있다.
이처럼 동일한 메시지를 객체의 타입에 따라 다르게 응답할 수 있는 능력을 다형성이라 부르며,
상속은 이런 다형성을 구현하는 방법 중 한가지이다.
위의 그림과 마찬가지로 코드 상에서 Movie 클래스는 DiscountPolicy에 의존한다.
하지만 실행 시점에서 AmountDiscountPolicy와 PercentDiscountPolicy에 의존한다.
이와 같이 컴파일 시점의 의존성과 실행 시점의 의존성이 다를 수 있다는 것,
지연 바인딩(동적 바인딩)이 가능하다는 것이 객체지향 프로그래밍의 중요한 특징 중 하나이다.