- 조합된 객체는 간단하고 독립적인 개체들로 이루어지는 경향이 있고 이런 개체들은 새롭게 조합하고, 재배치하기 쉽다. 간단한 객체는 이해하기 쉽고 재사용하고 테스트하기 용이하다.
- 각각을 제대로 사용하는 것은 경험과 판단력의 문제이다. 경험을 쌓는 가장 좋은 방법은 직접 실수를 통해 배우는 것이다. 디자인 기술을 향상시키고자 한다면 기술을 적극적으로 사용해 보고 실수를 받아들일 줄 알아야 하낟. 잘못된 디자인 결정을 버리고 가차 없이 새로운 방식으로 리팩터링다.
리팩터링을 통해 상속을 조합으로 변경하기
- 객체지향 조합을 이용하면 간단하고 독립적인 객체를 보다 크고 복잡한 것으로 통합할 수 있다.
- 조합에서 좀 더 큰 객체지는 자신의 부분들을 가지고 있다. -> has-a 관계
-
Bicycle 클래스 리팩터링
-
도메인 모델 분석 -> 자전거가 어떻게 조합되어야 하는지 고민
-
자전거는 부품들을 가지고있다. -> Parts 추상 클래스 (부품들의 모음) 도출
- 각각 로드 자전거와 산악 자전거에 필요한 부품을 Parts 클래스를 상속 받아서 구현함. -> RoadBikeParts, MountainBikeParts
- Bicycle과 Parts를 has-a 관계로 표현
-
-
-
RoadBikeParts, MountainBikeParts 리팩터링
- 각각 자전거에 필요한 부품 목록을 Parts 추상 클래스를 상속 받아 구현 -> 조합으로 리팩터링 하기
- 부품 목록 (Parts)은 개별 부품들을 갖고 있다. -> Part(부품) 클래스 도출
// 상속 -> 조합으로 리팩터링 class Bicycle { #size; #parts; constructor(size, parts) { this.#parts = parts; this.#size = size; } spares() { this.#parts.spares(); } } class Parts { #parts; constructor(parts) { this.#parts = this.#parts; } spares() { this.#parts.filter((part) => part.needSpare()); } } class Part { #name; #description; #needSpare; constructor(name, description, needSpare = true) { this.#name = name; this.#description = description; this.#needSpare = needSpare; } } // 사용 const chain = new Part('chain', '10-speed'); const roadTire = new Part('tireSize', '23'); const tape = new Part('tapeColor', 'red'); const mountainTire = new Part('tireSize', '2.1'); const rearShock = new Part('rearShock', 'Fox'); const frontShock = new Part('frontShock', 'Manitou', false); const roadBike = new Bicycle('L', new Parts([chain, roadTire, tape])); const mountainBike = new Bicycle( 'L', new Parts([chain, mountainTire, frontShock, rearShock]) );
팩토리 패턴
- 우리 애플리케이션에서 누군가는 Part들을 만드는 법을 알고 있어야 함. -> 책임을 격리시켜야 한다.
- 위 코드에서 자전거를 만들기 위해서 4개의 부품이 필요하다는 지식을 애플리케이션 여러 곳에 흩뿌려질 수 있다.
-
팩토리
- 다른 객체를 생산하는 객체
- 객체의 생성 과정을 팩토리 안에 고립시킨다. -> 객체 생성 과정을 객체 안으로 캡슐화
- 각 자전거에 필요한 부품 리스트를 갖는 부품 config와 팩토리 패턴으로 변경 점을 하나의 객체안으로 캡슐화할 수 있음.
-
집합과 조합
-
위임 : 한 객체가 전달바4ㄷ은 메시지를 단순히 다른 객체에게 전달하는 것
- 위임은 의존성을 만들어낸다.
- 메시지를 수신한 객체는 메시지를 인지해야 하고 또한 이 메시지를 어디로 보내야 할지 알아야 한다.
-
조합 = 가지고 있는 관계
-
집합 : 일반적인 의미의 조합
- 일반적으로 두 객체 사이의 가지고 있는 관계를 의미
- 포함된 객체가 독립적으로 존재할 수 있는 조합
-
조합 : 엄밀한 의미의 조합
- 포함된 객체가 포함하는 객체로부터 독립적으로 존재하지 못하는 방식으로 서로 가지고 있는 관계
- 예 :) 식사 - 에피타이저 -> 식사가 끝나면 에피타이저도 같이 사라짐.
-
-
상속과 조합 중 선택하기
-
상속의 이점과 비용
-
이점
- 제대로 구조화된 상속관계인 경우 확장에 열려있고, 동시에 수정에는 닫혀있다.
- 기본 상속구조에서 새로운 클래스를 추가하려 할 때 기존 코드를 전혀 수정하지 않아도 된다.
- 클래스간의 관계가 is-a 관계일 때 상속은 코드를 정리하는 가장 최적의 방법이다.
-
비용
- 상속이 어울리지 않는 문제를 해결하는 데 상속을 사용할 수 있다.
- 상속이 문제를 해결하기 위한 적절한 방법일지라도 다른 프로그래머가 우리가 작성한 코드를 우리가 바라지 않는 방식으로 사용할 수 있다.
-
상속은 그 본성 때문에 매우 강한 의존성을 함께 만들어 낸다.
- 자동화된 메시지 전달 방식에 의존하고 있기 때문
- 디자인 자체가 하위클래스를 상위클래스로부터 떨어질 수 없게 묶어 놓고 있다.
-
-
조합의 이점과 비용
- 조합된 객체는 클래스의 상속 관계에 의존하지 않는다. 수신한 메시지를 직접 전달한다.
-
이점
- 명확한 책임과 명료한 인터페이스를 갖는 작은 객체를 여럿 만들게 되는 경향이 있다. -> 수정사항이 발생했을 때 코드를 이해하기 쉽고 어떤 일이 벌어질지 예상하기 좋다.
- 상속구조로부터 독립적 -> 상속 구조의 위쪽에서 발생한 변화로부터 영향을 덜 받는다.
- 조합된 객체는 자신의 부분들을 인터페이스를 통해 관리하기 때문에 한 부분을 새로 추가하는 것이 보다 쉽다.
- 조합에 관여하는 객체들은 본질적으로 크기가 작고, 구조적으로 독립되어 있고 잘 정의된 인터페이스를 가지고 있다 -> 객체들은 자연스럽게 추가하고 제거될 수 있으며, 객체들을 서로 대체할 수 있는 요소로 만들어준다.
-
비용
- 개별 객체들은 충분히 투명하더라도 전체는 그렇지 않을 수 있다.
- 조합된 객체는 누구에게 어떤 메시지를 전달해야 할지 명확하게 알고 있어야 한다.
- 조합을 사용하면 여러 부분으로 이루어진 객체를 훌륭하게 조립할 수 있지만 매우 비슷한 부분들을 정리해야 하는 상황에서는 별 도움을 주지 못한다.
올바른 디자인 기술 선택하기
-
is-a 관계에서 상속 사용하기
- 일반 - 특수의 상속 관계가 뚜렷한 것은 고전적 상속으로 구조화하기 좋은 대상이다.
-
behaves-like-a 관계에서 오리 타입 사요하기
- 어떤 문제는 여러 개의 객체가 같은 역할을 수행해야 하는 상황을 만든다. -> 예 : 스케줄 가능성, 준비 가능성, 프린트 가능성, 저장 가능성...
-
코드 속에 숨어 있는 역할을 알아볼 수 있는 방법
- 객체가 역할을 수행하고 있지만 그 역할이 객체의 핵심적인 책임이 아닌 경우 -> 객체가 ~인 것 처럼 행동한다.
- 코드의 여러 곳에서 특정 역할을 수행하려고 하는 경우 -> 역할을 부여하는 객체의 관점에서 생각해 보기
- has-a 관계에서 조합 사용하기