상속과 다형성컴파일 시간 의존성과 실행 시간 의존성차이에 의한 프로그래밍상속과 인터페이스다형성구현 상속과 인터페이스 상속추상화와 유연성추상화의 힘코드 재사용상속캡슐화를 위반한다?설계가 유연하지 않다?합성
상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
- 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.
- 2장에서 수정된 코드는 Movie클래스가 DiscountPolicy 클래스와 연결되어 있다는 점을 자세히 봐야한다.
- 영화 요금을 계산하기 위해서는 추상 클래스가 아니라 그 추상클래스의 구현체가 필요하다. 즉 구현체에 의존해야 하지만 코드 수준에서는 두 구현체 중 어떤 것도 의존하지 않고있다.
- 그렇다면 Movie의 인스턴스가 코드 작성 시점에는 그 존재조차 알지 못했던 금액할인, 퍼센트할인의 인스턴스와 실행 시점에 협력 가능한 이유는 무엇이였을까?
Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new AmountDiscountPolicy(Money.wons(800),...));
- 퍼센트로 바꾸고싶다면 변경해줘서 실행시키면 그만..!
[여기서 이야기하고싶은 부분이 그래서 뭔데!?]
- 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다는 점이다.
- 즉 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다. 또한 유연하고 쉽게 재사용 할 수 있으며 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 점이다.
- 한 가지 관과해서는 안되는 사실은 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다는 점이다. 그 이유는 코드를 이해하기 위해서는 코드 뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다.
- 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다.
- 설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하자.
- 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장성은 낮아진다는 사실 또한 기억하자
- 무조건 유연한 설계도, 읽기쉬운 코드도 정답이 아니다 이것이 객체지향이 어려운 점이면서도 매력적인 이유다!
차이에 의한 프로그래밍
클래스를 하나 추가하고 싶은데 그 클래스가 기존의 어떤 클래스와 매우 흡사하다고 가정해보자.
- 드는 생각은 그 클래스 코드를 가져와서 약간만 수정하면 새로운 클래스를 만들 수 있다면 좋을 것이다.
- 더 좋은 방법은 그 클래스 코드를 수정하지 않고 재사용하는 것일 것이다. 이를 가능하게 해주는 것이 바로 상속 !
- 상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
- 또한 상속을 이용하면 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
- 메서드를 오버라이딩해서 행동을 수정한다는 의미!
- 이처럼 부모 클래스와 다른 부분만을 추가해서 클래스를 새롭고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라 부른다.
[자식클래스와 부모클래스]
- 코드를 제공하는 클래스를 슈퍼클래스, 부모클래스, 부모, 직계조상, 직접적인 조상 이라고 부른다.
- 코드를 제공받는 클래슬르 서브클래스, 자식클래스, 자식, 직계자손, 직접적인 자손 이라고 부른다.
- 특정 클래스보다 하위에 위치한 모든 클래스를 자손이라고 부른다.
- 클래스 관계는 상대적이라는 점에 주목하자. A>B>C>D 순으로 있을 때 B는 C의 부모일 수 있지만 A의 입장에서 보면 B는 자식이다.
- 즉 어떤 클래스를 기준으로 하느냐에 따라 상속 관계에 참여하는 클래스의 역할이 달라짐
상속과 인터페이스
- 상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
- 이것은 상속을 바라보는 일반적인 인식과는 거리가 있는데, 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각하기 때문이다.
- 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하자 !
- 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다. 결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
- 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 부른다. 업캐스팅이라고 부르는 이유는 일반적으로 부모 클래스를 자식 클래스의 위에 위치시키기 때문이다.
다형성
- 메시지와 메서드는 다른 개념이라는 것을 명심하자.
- 코드 상에서 클래스에게 메시지를 전송하지만 실행 시점에 실제로 싱행되는 메서드는 객체의 실제 클래스가 무엇인지에 따라 달라진다.
- 다시 말해 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라지며 이를 다형성이라고 부른다 !
- 다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
- Moive 클래스는 추상 클래스를 의존하고 있어 컴파일 시점에는 추상 클래스를 의존하지만 실행 시점에는 구현 클래스에 의존한다.
- 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
- 따라서 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 하며 즉 인터페이스가 동일해야 한다는 뜻이다.
- 다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
- 즉 메시지와 메서드 실행 시점에 바인딩 한다는 뜻으로 이를 지연 바인딩 또는 동적 바인딩 이라고 부른다.
- 그에 반해 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩 또는 정적 바인딩 이라고 부른다.
- 객체지향이 컴파일 시점의 의존상과 실행 시점의 의존성을 분리하고 하나의 메시지를 선택적으로 서로 다른 메서드에 연결할 수 있는 이유가 바로 지연 바인딩이라는 매커니즘을 사용하기 때문이다.
구현 상속과 인터페이스 상속
- 상속은 구현 상속과 인터페이스 상속으로 분류할 수 있다.
- 구현 상속을 서브클래싱 이라고 부르고 인터페이스 상속을 서브타이핑 이라고 부른다.
- 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라고 부른다.
- 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것을 인터페이스 상속이라고 부른다.
- 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.
- 대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만 이것은 오해다.
- 인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게될 확률이 높다.
추상화와 유연성
추상화의 힘
- 위에서 살펴본 것처럼 할인 정책은 구체적인 금액 할인 정책과 비율 할인 정책을 포괄하는 추상적인 개념이였다.
- 할인 조건 역시 더 구체적인 순번 조건과 기간 조건을 포괄하는 추상적인 개념이다.
- 즉 DiscountPolicy는 Amount, Percent보다 추상적이고 DiscountCondition도 Sequence, Period보다 추상적이다.
- 프로그래밍 언어 측면에서 DiscountPolicy와 DiscountCondition이 더 추상적인 이유는 인터페이스에 초점을 맞추기 때문이다.
- 추상화의 장점 두가지
- 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 두 번째 장점은 추상화를 이용하면 설계가 좀 더 유연해진다는 것이다.
- 추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
- 추상화의 이런 특징은 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.
- 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미함!
- 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있다.
코드 재사용
- 상속은 코드를 재사용하기 위해 널리 사용하는 방법이다
- 그러나 널리 사용되는 방법이라고 해서 가장 좋은 방법은 아니다.
- 코드 재사용을 위해서는 상속보다는 합성(composition)이 더 좋은 방법이다.
- 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 뜻한다.
상속
- 상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 방법이다 하지만 두가지 관점에서 설계에 안좋은 영향을 미친다.
- 상속이 캡슐화를 위반한다.
- 설계를 유연하지 못하게 만든다.
캡슐화를 위반한다?
- 상속을 이용하기 위해서는 부모 클래스의 내부구조를 잘 알고있어야 한다.
- 즉 결과적으로 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
- 캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다. 결과적으로 상속을 과도하게 사용한 코드는 변경하기도 어려워진.
설계가 유연하지 않다?
- 상속은 부모클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.
- 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
합성
- 합성은 상속이 가지는 두가지 문제점을 해결한다.
- 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 할 수 있다.
- 또한 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
- 상속은 클래스를 통해 강하게 결합되는 데 비해 합성은 메시지를 통해 느슨하게 결합된다.
- 따라서 코드 재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋다 !
그렇다고 해서 상속을 절대 사용하지 말라는 뜻은 아니다 !
- 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다.
객체지향이란 객체를 지향하는 것이다 따라서 객체지향 패러다임의 중심에는 객체가 위치한다!
- 그러나 각 객체를 따로 떼어 놓고 이야기 하는 것은 무의미 하며 가장 중요한 것은 협력에 참여하는 객체들 사이의 상호 작용이다 !
- 객체지향 설계의 핵심은 적절한 협력을 식별하고 협력에 필요한 역할을 정의한 후에 역할을 수행할 수 있는 적절한 객체에게 적절한 책임을 할당하는 것이다.