첫째, 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
둘째, 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다. 객체를 고립된 존재로 바라보지 말고 협력에 참여하는 협력자로 바라보기 바란다. 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라. (p. 41)
문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다. 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다. 클래스 사이의 관계도 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 만들어서 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다. (p. 42)
이 원칙에 따라 영화라는 개념은
Movie
클래스로, 상영이라는 개념은Screening
클래스로 구현한다. 할인 정책은DiscountPolicy
금액 할인 정책은AmountDiscountPolicy
와 같이 도메인의 개념과 관계를 반영하도록 프로그램을 구조화 한다.
클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다. 클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다. (p. 43)
첫 번째로 알아야 할 중요한 사실은 객체가 상태와 행동을 함께 가지는 복합적인 존재라는 것이다. 객체지향에서 객체라는 단위 안에 데이터와 기능을 내부적으로 묶는 것을 캡슐화라고 부른다. 그리고 대부분의 객체지향 프로그래밍 언어들은 상태와 행동을 캡슐화하는 것에서 한 걸음 더 나아가 외부에서의 접근을 통제할 수 있는 접근 제어 메커니즘도 함께 제공한다. (p. 44)
많은 프로그래밍 언어들은 접근 제어를 위해
public
,protected
,private
과 같은 접근 수정자를 제공한다. 변경될 가능성이 있는 세부적인 구현 내용을private
영역 안에 감춤으로써 변경으로 인한 혼란을 최소화할 수 있다.
두 번째 사실은 객체가 스스로 판단하고 행동하는 자율적인 존재라는 것이다. 객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서다. 객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다. 외부에서는 객체가 어떤 상태에 놓여 있는지, 어떤 생각을 하고 있는지 알아서는 안 되며, 결정에 직접적으로 개입하려고 해서도 안 된다. (p. 44)
외부에서 접근 가능한 부분을
public interface
, 외부에서는 접근 불가능하며 오직 내부에서만 접근 가능한 부분을implementation
이라고 부른다. 인터페이스와 구현의 분리 원칙은 훌륭한 객체지향 프로그램을 만들기 위한 핵심 원칙이다.
시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration)이라고 부른다. 객체지향 프로그램을 작성할 때는 먼저 협력의 관점에서 어떤 객체가 필요한지를 결정하고, 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성한다. (p. 48)
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메세지를 전송하는 것뿐이다. 다른 객체에게 요청이 도착할 때 메세지를 수신했다고 이야기한다. 메세지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메세지를 처리할 방법을 결정한다. 이처럼 수신된 메세지를 처리하기 위한 자신만의 방법을 메서드(Method)라고 부른다. (p. 49)
Screening
이Movie
의calculateMovieFee
'메서드를 호출한다'라고 말하는 것보다Screening
이Movie
에게calculateMovieFee
'메세지를 전송한다' 라고 말하는 것이 더 적절한 표현이다. 사실Screening
은Movie
안에calculateMovieFee
메서드가 존재하고 있는지조차 알지 못한다.
상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메세지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다. (p. 61)
Movie
입장에서는 자신과 협력하는 객체가 어떤 클래스의 인스턴스인지가 중요한 것이 아니라calculateDiscountAmount
메세지를 수신할 수 있다는 사실이 중요하다. 즉, 협력 객체가calculateDiscountAmount
라는 메세지를 이해할 수만 있다면 그 객체가 어떤 클래스의 인스턴스인지는 상관하지 않는다는 것이다.
다형성을 구현하는 방법은 매우 다양하지만 메세지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다. 다시 말해 메세지와 메서드를 실행 시점에 바인딩한다는 것이다. 이를 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라고 부른다. (p. 63)
순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라고 부른다. 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것을 인터페이스 상속이라고 부른다. 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만 이것은 오해다. (p. 64)
추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다. 영화의 예매 가격을 계산하기 위한 흐름은 항상 Movie
에서 DiscountPolicy
로, 그리고 다시 DiscountCondition
을 향해 흐른다. 할인 정책이나 할인 조건의 새로운 자식 클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름을 그대로 따르게 된다. (p. 66)
상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이다. 하지만 두 가지 관점에서 설계에 안좋은 영향을 미친다. 상속의 가장 큰 문제점은 캡슐화를 위반한다는 것이다. 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다. 결과적으로 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다. 캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다. (p. 70)
상속의 두 번째 단점은 설계가 유연하지 않다는 것이다. 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다. 그러나 인터페이스를 통해 약하게 결합한다면 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만들 수 있다. 그렇다고 해서 상속을 절대 사용하지 말라는 것은 아니다. 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다. (p. 72)
코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다. 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다. 이와 같은 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 잘 보여준다.
설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다. 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다. 항상 유연성과 가독성 사이에서 고민해야 한다. 무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다.
상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다. 그러나 클래스를 상속받는 것만이 다형성을 구현할 수 있는 유일한 방법은 아니다.
합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다. 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법으로 설계가 더 유연해진다.