오브젝트 2장 객체지향 프로그래밍
01. 영화 예매 시스템 #
- 영화 / 상영
- 할인
- 할인 조건 - 할인이 가능한지
- 할인 정책 - 적용되는 비용
02. 객체지향 프로그래밍을 향해 #
협력, 객체, 클래스 #
- 대부분의 사람은 클래스를 결정한 후 클래스에 어떤 속성과 메서드가 필요한지 고민한다.
- 이는 객체지향의 본질과는 거리가 멀다.
- 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
- 어떤 클래스가 필요한지 고민하기 전에 어떤 객체들이 필요한지 고민하라
- 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이다.
- 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다.
- 객체를 중심에 두는 접근방법은 설계를 단순하고 깔끔하게 만든다.
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐라
- 객체는 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재다.
- 객체를 협력 공동체의 일원으로 바라보는 것은 설계를 유연하고 확장 가능하게 만든다.
- 객체 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하라
도메인의 구조를 따르는 프로그램 구조 #
- 도메인
- 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
- 객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.
- 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에 도메인을 구성하는 개념들이 객체와 클래스로 매끄럽게 연결될 수 있다.
클래스 구현하기 #
https://github.com/soso01/object/tree/main/chapter2
- 클래스를 구현하거나 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다.
- 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 결정하는 것
- 구분해야 하는 이유는?
- 경계의 명확성이 객체의 자율성을 보장한다.
- 프로그래머에게 구현의 자유를 제공한다.
자율적인 객체 #
- 중요한 두가지 사실
- 객체는 상태와 행동을 함께 가지는 복합적인 존재임
- 객체는 스스로 판단하고 행동하는 자율적인 존재
- 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현할 수 있게 했다.
- 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 부른다.
- 객체지향 언어들은 캡슐화에서 더 나아가 외부에서의 접근을 통제할 수 있는 접근 제어 메커니즘도 함께 제공한다.
- private, protected, public … - 접근 수정자
- 객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서이다.
- 객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다.
- 외부에서는 객체에게 원하는 것을 요청하고 객체가 스스로 최선의 방법을 결정할 수 있을 것이라는 점을 믿고 기다려야 한다.
- 캡슐화와 접근 제어는 두 부분으로 나뉜다.
- 퍼블릭 인터페이스 - 외부에서 접근 가능한 부분
- 구현 - 내부에서만 접근가능한 부분
- 인터페이스와 구현의 분리는 훌륭한 객체지향 프로그램을 만들기 위해 따라야 하는 핵심 원칙이다.
프로그래머의 자유 #
- 프로그래머의 역할을 클래스 작성자와, 클라이언트 프로그래머로 구분하자.
- 클래스 작성자는 새로운 타입을 프로그램에 추가한다.
- 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
- 접근제어를 통해 클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다.
- 이를 구현은닉이라고 부른다.
- 구현은닉은 클라이언트 프로그래머에게도 유용하다.
- 클라이언트 프로그래머는 내부의 구현을 무시한 채 인터페이스만 알고 있어도 클래스를 사용할 수 있기 때문에 머릿속에 담아둬야 하는 지식의 양을 줄일 수 있다.
- 따라서 클래스를 개발할 때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.
- 설계가 필요한 이유는 변경을 관리하기 위해서다.
- 객체의 변경을 관리할 수 있는 가장 대표적인 것이 접근 제어다.
- 변경될 가능성이 있는 세부적인 구현 내용을 private 영역에 감춤으로써 변경으로 인한 혼란을 최소화 할 수 있다.
협력하는 객체들의 공동체 #
- 가격에 대한 타입을 number 대신 Money라는 새 객체로 정의하는 것은 유용하다.
- number타입은 Money타입처럼 저장하는 값이 금액과 관련되어 있다는 의미를 전달할 수 없다.
- 또한 금액과 관련된 로직이 서로 다른 곳에 중복되어 구현되는 것을 막을 수 없다.
- 객체지향의 장점은 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있는 것이다.
- 의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현하라.
협력에 대한 짧은 이야기 #
- 객체는 외부에 공개되어있는 퍼블릭 인터페이스를 통해 내부 상태에 접근하도록 허용한다.
- 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청할 수 있고, 요청 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.
- 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것이다.
- 다른 객체에 요청이 도착할 때 해당 객체가 메시지를 수신한다.
- 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드라 부른다.
- 메시지와 메서드를 구분하는 것은 매우 중요하다.
- 이 구분은 객체지향 패러다임이 유연하고 확장가능하며, 재사용 가능한 설계를 할 수 있도록 크게 기여함.
- 메시지와 메서드의 구분에서부터 다형성의 개념이 출발한다.
03. 할인 요금 구하기 #
https://github.com/soso01/object/tree/main/chapter2
04. 상속과 다형성 #
컴파일 시간 의존성과 실행 시간 의존성 #
- 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다.
- 다시 말해, 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다.
- 쉽게 재사용할 수 있으며, 확 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.
- 예제에서 코드상 Movie는 DiscountPolicy라는 추상클래스에 의존성을 갖지만, 실행 시점에서의 의존성의 DiscountPolicy의 상속클래스인 AmountDiscountPolicy에 의존한다.
- 사실은 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다.
- 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문
- 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다.
- 이와 같은 의존성의 양면성은 설계가 트레이드오프의 산물임을 보여준다.
- 설계를가 유연해지면 코드를 이해하고 디버깅하기 점점 더 어려워지고, 유연성을 억제하면 코드를 이해하기 쉽지만, 재사용성과 확장 가능성은 낮아진다.
차이에 의한 프로그래밍 #
- 상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
- 부모 클래스의 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.
상속과 인터페이스 #
- 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
- 자식클래스는 부모 클래스의 인터페이스를 포함하므로, 결과적으로 부모클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
- 외부 클래스의 입장에선 객체가 어떤 클래스의 인스턴스인지 중요하지 않다.
- 메시지를 수신할 수 있으면 무엇이든 상관없음.
다형성 #
- 다형성 - 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 수신하는 객체의 클래스가 무엇이냐에 따라 달라지는 것
- 다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
- 상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다.
05. 추상화와 유연성 #
추상화의 힘 #
- 추상화를 사용할 경우의 두 가지 장점
- 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.
- 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다.
- 설계가 좀 더 유연해진다.
- 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.
- 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
유연한 설계 #
export class Movie {
...
calculateMovieFee = (screening: Screening) => {
if (this.discountPolicy === null) return this.fee;
return this.fee.minus(
this.discountPolicy.calculateDiscountAmount(screening)
);
};
}
- 위 방식의 문제점은 할인 정책이 없는 경우를 예외 케이스로 취급하기 때문에 지금까지 일관성 있었던 협력방식이 무너진다.
- 할인 금액을 결정하는 책임이 DiscountPolicy가 아닌, Movie쪽에 있다.
- 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다.
- 항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택하라.
export class NoneDiscountPolicy extends DiscountPolicy {
protected override getDiscountAmount = () => Money.ZERO;
}
- 위와 같이 새 클래스를 추가해서 일관성을 지킬 수 있다.
- 추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결함되는 것을 방지하기 때문이다.
코드 재사용 #
- 코드 재사용을 위해서는 상속보다 합성이 더 좋은 방법이다.
- 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.
상속 #
- 두가지 관점에서 상속은 설계에 좋지 않다.
- 캡슐화를 위반함
- 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
- 결과적으로 부모의 구현이 자식에 노출되기 때문에 캡슐화가 약해진다.
- 캡슐화의 약화는 자식과 부모를 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식도 함게 변경될 확률을 높인다.
- 설계를 유연하지 못하게 만듦
- 상속은 부모와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.
- 따라서 실행 시점에 객체의 종류를 바꾸는 것이 불가능하다.
- 캡슐화를 위반함
합성 #
- 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법이 합성이다.
- 합성은 상속이 가지는 두가지 문제를 모두 해결한다.
- 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다.
- 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
- 그렇다고 상속을 쓰면 안되는 것은 아님.
- 대부분의 설계에서 상속과 합성을 같이 사용한다.