단순 책 내용 정리가 아닌 개인 생각과 중요 내용 정리본임
온라인 영화 예매 시스템
특정한 조건을 만족하는 에매자는 요금을 할인받을 수 있다 !
영화별로 하나의 할인 정책만 할당 가능
객체지향 언어에 익숙한 사람은 가장 먼저 어떤 클래스가 필요한 지 결정한 후에 해당 클래스에 어떤 속성과 메서드가 필요한지 고민한다.
하지만, 이것은 객체지향의 본질과는 거리가 멀다.
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때 얻을 수 있다.
첫째, 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
둘째, 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다.
도메인?
-> 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용
요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에
도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다 !
인스턴스 변수의 가시성은 private, 메서드의 가시성은 public
클래스를 구현할 때, 가장 중요한 점은 클래스의 경계를 구분 짓는 것이다.
-> 클래스는 외부와 내부로 구분되며 어떤 부분을 외부에 공개하고 감출지를 결정하는 것
📍 자율적인 객체
먼저 두가지 중요한 사실을 알아야한다.
두가지 사실은 서로 깊이 연관돼 있다.
데이터와 기능을 객체 내부로 함께 묶는 것 -> 캡슐화
한 걸음 더 나아가
외부에서의 접근을 통제할 수 있는 접근 제어
많은 프로그래밍 언어들은 접근 제어를 위해 public, protected, private와 같은 접근 수정자 제공
객체가 자율적인 존재로 있으려면, 최대한 외부의 간섭을 최소화 해야 한다.
캡슐화와 접근 제어는 객체를 두 부분으로 나눔
외부에서 접근 가능한 부분 --> 퍼블릭 인터페이스(public interface)
외부에서 접근 불가능하고 오직 내부에서만 접근 가능한 부분 --> 구현(implementation)
인터페이스와 구현의 분리 원칙은 훌륭한 객체지향 프로그램을 만들기 위해 따라야 하는 핵심 원칙
📍 프로그래머의 자유
프로그래머의 역할
클래스를 개발할 때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 함
객체지향 프로그램을 작성할 때는 먼저 협력의 관점에서 어떤 객체가 필요한지를 결정하고, 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성한다.
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것
다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신했다고 이야기함.
메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.
이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드
속성들은 생성자를 통해 전달받음
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에게 메시지를 전송할 뿐이다.
이 속에는 사실 상속, 다형성, 추상화가 숨겨져 있다.
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);
}
여기서는 부모 클래스인 DiscountPoLicy 안에 중복 코드를 두고 AmountDiscountPoLicy와 PercentDiscount Policy가 이 클래스를 상속받게 할 것이다.
실제 애플리케이션에서는 DiscountPolLicy의 인스턴스를 생성 할 필요가 없기 때문에
추상 클래스(abstract class)로 구현했다.
할인 조건을 만족하는 DiscountCondition이 하나라도 존재하는 경우, 추상메서드인 getDiscountAmount 메서드를 호출해 할인 요금을 계산한다.
코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있음
-> 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있음
유연하고, 쉽게 재사용할 수 있으며,
확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.
상속 - 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법
상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로
기존 클래스가 가지고 있는 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법
-> 차이에 의한 프로그래밍
상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억해야한다.
상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.
결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅
메시지와 메서드는 다른 개념
Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. --> 다형성
메시지와 메서드를 실행 시점에 바인딩하는 것 -> 지연 바인딩 또는 동적 바인딩
전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것 --> 초기 바인딩 또는 정적 바인딩
유연성이 필요한 곳에 추상화를 사용하라.
구현과 관현된 모든 것들이 트레이드오프의 대상이 될 수 있다 !
상속의 단점
해결하기 위해서는
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법 --> 합성
합성은 상속이 가지는 문제점 해결
코드 재사용을 위해서는 상속보다는 합성이 더 좋은 방법