5. Designing with Objects
이 장의 내용은
▶ 객체 지향 프로그래밍 디자인이 무엇인가
▶ 클래스, 개체, 속성과 동작은 무엇인가
▶ 서로 다른 개체 간의 관계를 정의하는 방법은
Chapter 4, "Designing Professional C++ Programs"에서 좋은 소프트웨어 디자인에 대한 인식을 발전시켰으니, 이제 개체의 개념과 좋은 디자인의 개념을 결합할 차례이다. 코드에서 개체를 사용하는 프로그래머와 객체 지향 프로그래밍을 진정으로 이해하는 프로그래머의 차이는 개체가 서로 관련되는 방식과 프로그램의 전체 디자인으로 요약된다.
이 장은 절차적 프로그래밍(C 스타일)에 대한 간략한 설명으로 시작하여, 객체 지향 프로그래밍(OOP)에 대한 자세한 설명으로 이어진다. 몇 년 동안 개체를 사용해 왔더라도, 개체에 대해 생각하는 방법에 대한 몇 가지 새로운 아이디어를 얻기 위해 이 장을 읽고 싶을 것이다. 프로그래머가 객체 지향 프로그램을 빌드할 때 흔히 겪는 함정을 포함하여 개체 간의 다양한 관계에 대해 논의할 것이다.
절차적 프로그래밍이나 객체 지향 프로그래밍에 대해 생각할 때, 기억해야 할 가장 중요한 점은 그들은 프로그램에서 진행 중인 작업에 대한 다양한 추론 방식을 나타낸다는 것이다. 너무 자주, 프로그래머는 개체가 무엇인지 제대로 이해하는 전에 OOP의 구문과 전문 용어에 얽매인다. 이 장은 코드에 대해서는 가볍고 개념과 아이디어에 대해서는 무겁다. Chapter 8, "Gaining Proficiency with Classes and Objects", Chapter 9, "Mastering Classes and Objects" 그리고 Chapter 10, "Discovering Inheritance Techniques"에서 C++ 개체 구문에 대해 자세히 설명한다.
AM I THINKING PROCEDURALLY? (절차적으로 생각하고 있나요?)
C와 같은 절차적 언어는 코드를 (이상적으로는) 단일 작업을 수행하는 작은 조각으로 나눈다. C에서 프로시저가 없으면, 당신의 모든 코드가 main() 내부에 함께 묶일 것이다. 당신의 코드는 읽기 어려울 것이고, 당신의 동료들은 말할 것도 없이 짜증이 날 것이다.
컴퓨터는 모든 코드가 main()에 있는지 또는 설명이 포함된 이름과 주석이 있는 한 입 크기의 조각으로 나뉘어 있는지 신경쓰지 않는다. 프로시저는 프로그래머뿐만 아니라 코드를 읽고 유지 관리하는 사람들을 돕기 위해 존재하는 추상화이다. 이 개념은 프로그램에 대한 근본적인 질문인 "이 프로그램은 무엇을 하느가?"를 중심으로 구축된다. 이 질문에 영어로 대답함으로써 당신은 절차적으로 생각하고 있는 것이다. 예를 들어, 다음과 같이 대답하여 주식 선택 프로그램 디자인을 시작할 수 있다. 첫째, 프로그램은 인터넷에서 주식 시세를 얻는다. 그런 다음, 이 데이터를 특정 메트릭으로 정렬한다. 다음으로 정렬된 데이터에 대한 분석을 수행한다. 마지막으로, 매수와 매도 추천 목록을 출력한다. 코딩을 시작하면, 이 멘탈 모델을 직접 C 함수인 retrieveQuotes(), sortQuotes(), analyzeQuotes(), outputRecommendations()로 변환 할 수 있다.
참고
C는 프로시저를 "functions(함수)"라고 하지만, C는 함수형 언어가 아니다. functional이라는 용어는 procedural와 다르며, 완전히 다른 추상화를 사용하는 Lisp와 같은 언어를 나타낸다.
절차적 접근 방식은 프로그램이 특정 단계 목록을 따를 때 잘 작동하는 경향이 있다. 그러나 대규모의 최신 애플리케이션에서는 일련의 이벤트가 거의 없다. 종종 사용자는 언제든지 명령을 수행할 수 있다. 절차적 사고는 데이터 표현에 대해서는 아무것도 말하지 않는다. 이전 예에서, 주식 시세가 실제로 무엇인지에 대한 논의가 없다.
절차적 사고 방식이 프로그램에 접근하는 방식처럼 들리더라고 걱정할 필요가 없다. OOP가 소프트웨어에 대한 단순한 대안이자 보다 유연한 사고 방식이라는 것을 깨닫고 나면 자연스럽게 이해될 것이다.
THE OBJECT-ORIENTED PHILOSOPHY (객체 지향 철학)
"이 프로그램은 무엇을 합니까?"라는 질문에 기반한 절차적 접근 방식과 달리 객체 지향 접근 방식은 "내가 모델링하는 실제 객체는 무엇인가?"라는 또 다른 질문을 던진다. OOP는 프로그램을 작업이 아닌 물리적 개체의 모델로 나누어야 한다는 개념에 기반한다. 이것은 처음에는 추상적으로 보이지만, 클래스, 컴포넌트, 속성, 동작의 측면에서 물리적 개체를 고려할 때 더 명확해진다.
Classes (클래스)
클래스는 개체와 개체의 정의를 구별하는 데 도움이 된다. 오렌지를 고려하면... 일반적으로 오렌지를 나무에서 자라는 맛있는 과일로 이야기하는 것과 현재 키보드에서 주스를 흘리고 있는 것과 같은 특정 오렌지에 대해 이야기하는 것 사이에는 차이가 있다.
"오렌지는 무엇입니까?"라는 질문에 대답할 때, 오렌지로 알려진 분류에 대해 이야기하는 것이다. 모든 오렌지는 과일이다. 모든 오렌지는 나무에서 자란다. 모든 오렌지는 오렌지의 모양을 가진다. 모든 오렌지는 특정한 맛을 가지고 있다. 클래스는 단순히 개체의 분류 정의를 캡슐화한 것이다.
특정 오렌지를 설명할 때, 개체에 대해 이야기하는 것이다. 모든 개체는 특정 클래스에 속한다. 내 책상 위의 개체는 오렌지이므로, 오렌지 클래스에 속하는 것으로 알 수 있다. 그러므로 그것이 나무에서 자라는 과일인 줄 안다. 그것이 오렌지의 중간 모양이며, 맛은 "매우 맛있다"라고 말할 수 있다. 개체는 클래스의 인스턴스로, 동일한 클래스의 다른 인스턴스와 구별되는 특성을 가진 특정 항목이다.
좀 더 구체적인 예로, 이전의 주식 선택 애플리케이션을 다시 생각해 보자. OOP에서 "주식 시세"는 시세를 구성하는 추상적인 개념을 정의하기 때문에 클래스이다. "현재 Microsoft 주식 시세"와 같은 특정 시세는 클래스의 인스턴스이기 때문에 개체가 된다.
C 언어 배경에서, 클래스 및 개체는 유형 및 변수와 유사하다고 생각하면 된다. 사실 Chapter 1, "A Crash Course in C++ and the Standard Library"에서 클래스 구문이 C 구조체 구문과 유사함을 보여준다.
Components (컴포넌트)
만약 비행기와 같은 복잡한 현실 세계의 개체를 고려한다면, 그것이 더 작은 component로 구성되어 있다는 것을 쉽게 알 수 있을 것이다. 동체, 컨트롤, 랜딩 기어, 엔진과 기타 수많은 부품이 있다. 복잡한 작업을 더 작은 절차로 나누는 것이 절차적 프로그램의 기본이 되는 것처럼, 개체를 더 작은 컴포넌트로 생각하는 능력은 OOP에 필수적이다.
컴포넌트는 본질적으로 클래스와 동일하지만, 더 작고 구체적이다. 좋은 개체 지향 프로그램에는 Airplane 클래스가 있을 수 있지만, 이 클래스가 비행기를 완전히 기술한다면 이 클래스는 엄청난 크기가 될 것이다. 대신, Airplane 클래스는 더 작고 관리하기 쉬운 많은 컴포넌트를 다룰 수 있다. 이러한 각 컴포넌트에는 추가 서브 컴포넌트가 있을 수 있다. 예를 들어, 랜딩 기어는 비행기의 컴포넌트이고, 휠은 랜딩 기어의 컴포넌트이다.
Properties (속성)
Properties은 한 개체를 다른 개체와 구별하기 위한 것이다. Orange 클래스로 돌아가서, 모든 오렌지는 약간의 오렌지 색조와 특정한 맛이 있는 것으로 정의된다는 것을 기억한다. 이 두 가지 특성이 속성이다. 모든 오렌지는 값만 다를 뿐 동일한 속성을 갖는다. 내 오렌지는 "매우 맛있는" 맛이 있지만, 당신의 오렌지는 "끔찍하게 불쾌한" 맛이 있을 수 있다.
클래스 레벨에서 속성에 대해 생각할 수도 있다. 앞에서 인식했듯이, 모든 오렌지는 과일이며, 나무에서 자란다. 이것들은 과일 클래스의 속성인 반면, 오렌지의 특정 색조는 특정 과일 개체에 의해 결정된다. 클래스 속성은 클래스의 모든 개체가 공유하는 반면, 개체 속성은 클래스의 모든 개체에 있지만 값은 다르다.
주식 선택 예에서, 주식 시세에는 회사 이름, 시세 기호, 현재 가격과 기타 통계를 비롯한 여러 개체 속성이 있다.
속성은 개체를 설명하는 특성이다. 그들은 "무엇이 이 개체를 다르게 만드는가?"라는 질문에 답한다.
Behaviors (동작)
Behaviors는 "이 개체는 무엇을 하는가?" 또는 "이 개체에 대해 무엇을 할 수 있는가?" 라는 두 가지 질문 중 하나에 답한다. 오렌지의 경우, 많은 것을 하지는 않지만, 우리는 오렌지에 대해 무언가를 할 수 있다. 한 가지 행동은 먹을 수 있다는 것이다. 속성과 마찬가지로 클래스 레벨이나 개체 레벨의 동작을 생각할 수 있다. 모든 오렌지는 거의 같은 방법으로 먹을 수 있다. 그러나, 완전히 둥근 오렌지의 동작이 더 평평한 오렌지의 동작과 다를 수 있는 경사면을 굴러서 내려가는 것과 같은 다른 동작에서는 다를 수 있다.
주식 선택 예는 좀 더 실용적인 동작을 제공한다. 절차적으로 생각할 때, 내 프로그램의 기능 중 하나로 주식 시세 분석이 필요하다고 결정했다. OOP에서 생각하면, 주식 시세 개체가 자체적으로 분석할 수 있다고 결정할 수 있다. 분석은 주식 시세 개체의 동작이 된다.
개체 지향 프로그램에서, 대부분의 기능 코드는 프로시저에서 클래스로 이동된다. 특정 동작이 있는 클래스를 만들고 상호 작용 방식을 정의함으로써, OOP는 작동하는 데이터에 코드를 첨부하기 위한 훨씬 더 풍부한 메커니즘을 제공한다. 클래스의 동작은 소위 클래스 메서드로 구현된다.
Bringing It All Together (모든 것을 하나로 모으기)
이러한 개념을 사용하여, 주식 선택 프로그램을 다시 살펴보고 개체 지향 방식으로 재설계할 수 있다.
논의한 바와 같이, "주식 시세"는 시작하기에 좋은 클래스가 될 것이다. 시세의 목록을 얻으려면, 프로그램에 종종 collection이라고 하는 주식 시세 그룹의 개념이 필요하다. 따라서 더 나은 디자인은 딘일 "주식 시세"를 나타내는 더 작은 컴포넌트로 구성된 "주식 시세 컬렉션"을 나타내는 클래스를 갖는 것이다.
속성으로 이동하면, 컬렉션 클래스에는 하나 이상의 속성(수신된 실제 시세 목록)이 있다. 또한 가장 최근 검색한 정확한 날짜 및 시간과 같은 추가 속성이 있을 수 있다. 동작에 관해서는, "주식 시세 컬렉션"이 시세를 얻기 위해서 서버와 통신하고, 정렬된 시세 목록을 제공할 수 있다. 이것은 "시세 검색"와 "시세 정렬" 동작이다.
주식 시세 클래스에는 앞에서 설명한 이름, 기호, 현재 가격 등의 속성이 있다. 또한 분석 동작을 가질 수 있다. 주식을 사고 파는 것과 같은 다른 행동을 고려할 수도 있다.
컴포넌트 간의 관계를 보여주는 다이어그램을 만드는 것이 종종 유용하다. Figure 5-1은 UML 클래스 다이어그램 구문(Appendix D)을 사용하여 StockQuoteCollection에 0개 이상(0..*)의 StockQuote 개체가 포함되어 있고, StockQuote 개체는 단일(1) StockQuoteCollection에 속함을 나타낸다.
Figure 5-2는 오렌지 클래스에 대해 가능한 UML 다이어 그램을 보여준다.
LIVING IN A WORLD OF CLASSES (클래스의 세계에서 살기)
절차적 사고 프로세스에서 개체 지향 패러다임으로 전환하는 프로그래머는 종종 클래스로 속성과 동작을 결합하는 것에 대한 깨달음을 경험하게 된다. 어떤 프로그래머는 작업 중인 프로그램의 디자인을 다시 검토하여, 클래스로 특정 부분을 다시 작성하는 자신을 발견하게 된다. 다른 프로그래머들은 모든 코드를 버리고 완전한 개체 지향 애플리케이션으로 프로젝트를 다시 시작하려는 유혹을 받을 수 있다.
클래스를 사용하여 소프트웨어를 개발하는 두 가지 주요 접근 방식이 있다. 어떤 프로그래머에게, 클래스는 단순히 데이터와 기능의 멋진 캡술화를 나타낸다. 이 프로그래머는 더 읽기 쉽고 유지 관리하기 쉽운 코드를 만들기 위해 프로그램 전체에 클래스를 전파한다. 이 접근 방식을 사용하는 프로그래머는 심장 박동기를 이식하는 외과 의사와 같이 격리된 코드 조각을 잘라내어 클래스로 대체한다.
이 접근 방식에는 본질적으로 잘못된 것이 없다. 이 프로그래머들은 클래스를 많은 상황에서 유익한 도구로 바라본다. "클래스 처럼 느껴지는" 프로 그램의 특정 부분은 주식 시세와 같다. 이들은 실세계 용어로 분리되고 설명될 수 있는 부분이다.
다른 프로그래머는 OOP 패러다임을 완전히 채택하고, 모든 것을 클래스로 변경한다. 일부 클래스는 오렌지나 주식 시세와 같은 실세계 사물에 해당하는 반면, 분류기(sorter)나 실행 취소(undo) 클래스와 같은 보다 추상적인 개념을 캡슐화하는 클래스도 있다.
아마도 이상적인 접근 방식은 이러한 극단 사이의 어딘가에 있을 것이다. 첫 번째 개체 지향 프로그램은 실제로 몇 개의 클래스가 흩어져 있는 전통적인 절차적 프로그램이었을 수도 있다. 또는 int를 나타내는 클래스에서 기본 애플리케이션을 나타내는 클래스에 이르기까지 모든 것을 클래스로 만들 수도 있다. 시간이 지남에 따라 행복한 환경를 찾을 수 있다.
Over-Classification (과잉 분류)
창의적인 개체 지향 시스템을 디자인하는 것과 모든 작은 것을 하나의 클래스로 만들어 팀의 다른 모든 사람들을 짜증나게 하는 것 사이에는 종종 미세한 차이가 있다. Freud(프로이드)가 말했듯이, 때때로 변수는 단지 변수일 뿐이다. 좋아, 그것은 그가 말한 것을 의역한 것이다.
아마도 당신은 차기 베스트셀러 tic-tac-toe 게임을 디자인하고 있을 것이다. 당신은 이것에 대한 모든 것을 OOP 할 것이다. 그래서 클래스와 개체를 스케치하기 위해 한 잔의 커피와 메모장을 가지고 앉는다. 이와 같은 게임에는 게임 플레이를 감독하고 승자를 감지할 수 있는 클래스가 종종 있다. 게임 보드를 나타내기 위해 마커와 마커 위치를 추적하는 Grid 클래스를 구상할 수 있다. 사실, 그리드의 컴포넌트는 X나 O를 나타내는 Piece 클래스가 될 수 있다.
잠깐, 물러서서... 이 디자인은 X나 O를 나타내는 클래스를 가질 것을 제안한다. 그것은 아마도 클래스 과잉이다. 결국, char은 X나 O를 똑같이 나타낼 수 없을까? 더 좋은 점은, 왜 Grid는 열거형의 2차원 배열을 사용할 수 없는가? Piece 클래스가 코드를 복잡하게 만드는가? 제안된 Piece 클래스를 나타내는 다음 테이블을 살펴본다:
CLASS | ASSOCIATED COMPONENTS | PROPERTIES | BEHAVIORS |
---|---|---|---|
Piece | None | X or O | None |
테이블이 약간 빈약하여, 여기에 있는 것이 본격적인 클래스가 되기에는 너무 세분화될 수 있음을 강력하게 암시한다.
반면에, 미래 지향적인 프로그래머는 Piece가 현재로서는 매우 빈약한 클래스이지만, 클래스로 만들면 실제 불이익 없이 미래에 확장할 수 있다고 주장할 수 있다. 아마도 나중에 Piece의 색상이나 Piece가 가장 최근에 이동되었는지 여부와 같은 추가 속성이 추가될 수 있다.
또 다른 해결책은 Piece를 사용하는 대신 격자 사각형의 state를 생각하는 것이다. 사각형의 상태는 Empty, X, O 중에 하나가 될 수 있다. 미래를 대비한 디자인을 만들기 위해, 구체적인 파생 클래스 StateEmpty, StateX나 StateO를 사용하여 추상 기본 클래스 State를 디자인할 수 있다. 이 디자인을 사용하면 나중에 기본 클래스나 개별 클래스에 추가 속성을 추가할 수 있다.
분명히, 정답은 없다. 중요한 점은 애플리케이션을 디자인할 때 이러한 문제를 고려해야 한다는 것이다. 클래스는 프로그래머가 코드를 관리하는 데 도움을 주기위해 존재한다는 것을 기억한다. 코드를 "좀 더 개체 지향적"으로 만드는 것 외에 다른 이유 없이 클래스를 사용한다면, 무언가 잘못된 것이다.
Overly General Classes (지나치게 일반적인 클래스)
아마도 클래스가 되어서는 안 되는 클래스보다 더 짜증나는 것은 너무 일반적인 클래스일 것이다. 모든 OOP 학생들은 "오렌지"(의심의 여지가 없이 클래스인 것)와 같은 예제부터 시작한다. 실제 코딩에서 클래스는 상당히 추상적일 수 있다. 많은 OOP 프로그램이 "애플리케이션 클래스"를 가지지만, 애플리케이션은 실제로 물질적 형태로 상상할 수 있는 것이 아니다. 그러나 애플리케이션 자체에 특정 속성과 동작이 있기 때문에, 애플리케이션을 클래스로 나타내는 것이 유용할 수 있다.
지나치게 일반적인 클래스는 특정한 것을 전혀 나타내지 못하는 클래스이다. 프로그래머는 유연하거나 재사용 가능한 클래스를 만들려고 시도할 수 있지만, 혼란스러운 클래스로 끝난다. 예를 들어, 미디어를 구성하고 표시하는 프로그램을 상상해 본다. 사진의 카탈로그를 만들고, 디지털 뮤직과 영화 컬렉션을 정리하고, 개인 일기장으로 사용할 수 있다. 지나치게 일반적인 접근 방식은 이러한 모든 것을 "media(미디어)" 개체로 생각하고 지원되는 모든 형식을 수용할 수 있는 단일 클래스를 빌드하는 것이다. 이 단일 클래스에는 미디어 유형에 따라 이미지, 노래, 영화나 일기 항목의 로우 비트가 포함된 "data(데이터)"라는 속성이 있을 수 있다. 클래스는 이미지를 적절하게 그리거나, 노래를 재생하거나, 영화를 재생하거나, 편집을 위해 일기를 불러오는 "perfoem(수행)"이라는 동작이 있을 수 있다.
이 단일 클래스가 너무 일반적이라는 단서는 속성과 행동의 이름에 있다. data라는 단어는 그 자체로 의미가 거의 없다. 이 클래스가 세 가지 매우 다른 용도로 지나치게 확장되었기 때문에, 여기에서는 일반적인 용어를 사용해야 한다. 마찬가지로, perform은 다양한 유형의 미디어에 대해 매우 다른 작업을 수행한다. 확실히, 이 클래스는 너무 많은 일을 하려고 한다.
그럼에도 불구하고, 미디어를 구성하는 프로그램을 디자인할 때, 애플리케이션에 Media 클래스가 반드시 있을 것이다. 이 Media 클래스에는 이름, 미리 보기, 해당 미디어 파일에 대한 링크 등과 같이 모든 유형의 미디어에 있는 공통 속성이 포함된다. 이 Media 클래스에는 특정 미디어 처리에 대한 세부 정보가 포함되어서는 안 된다. 이미지를 표시하거나 노래나 영화를 재생하는 코드를 포함해서는 안 된다. 대신 Picture 클래스와 Movie 클래스 같은 다른 클래스가 디자인에 있어야 한다. 그런 다음 이러한 특정 클래스에는 사진 표시나 영화 재생 같은 실제 미디어 관련 기능이 포함된다. 분명히, 이러한 미디어 관련 클래스는 Media 클래스와 어떻게든 관련이 있으며, 이것이 바로 다음 섹션의 주제인 클래스 간의 관계를 표현하는 방법이다.
CLASS RELATIONSHIPS (클래스 관계)
프로그래머는 서로 다른 클래스가 공통적인 특성을 가지고 있거나, 어떻게든 서로 관련이 있는 것처럼 보이는 경우를 확실히 접하게 될 것이다. 개체 지향 언어는 클래스 간의 이러한 관계를 처리하기 위한 여러 메커니즘을 제공한다. 까다로운 부분은 실제로 관계가 무엇인지 이해하는 것이다. 클래스 관계에는 두 가지 주요 유형이 있는데, 하나는 has-a 관계이고 다른 하나는 is-a 관계이다.
The Has-a Relationship
has-a 관계에 참여하는 클래스는 A에 B가 있거나 A가 B를 포함하는 패턴을 따른다. 이러한 유형의 관계에서는, 한 클래스를 다른 클래스의 일부로 상상할 수 있다. 앞에서 정의한 컴포넌트는 일반적으로 다른 클래스로 구성된 클래스를 기술하기 때문에 has-a 관계를 나타낸다.
이것의 실제 예는 동물원과 원숭이의 관계일 수 있다. 동물원에 원숭이가 있다거나 동물원이 원숭이를 포함한다고 말할 수 있다. 코드로 동물원을 시뮬레이션하면, 원숭이 컴포넌트가 가진 동물원 클래스가 있을 것이다.
종종 사용자 인터페이스 시나리오에 대해 생각하는 것이 클래스 관계를 이해하는 데 도움이 된다. 모든 UI가 OOP로 구현되는 것은 아니지만(요즘은 대부분 그렇지만), 화면의 시각적 요소가 클래스로 잘 변환되기 때문이다. has-a 관계에 대한 한 가지 UI 비유는 버튼이 포함하는 윈도우이다. 버튼과 윈도우는 분명히 별개의 두 클래스이지만, 어떤 면에서는 분명히 관련되어 있다. 버튼이 윈도우 안에 있기 때문에 윈도우에 버튼이 있다고 말한다.
Figure 5-3은 실제 세계와 사용자 인터페이스의 관계를 보여준다.
다음과 같은 두 가지 유형의 관계가 있다:
- Aggregation (집계): 집계를 사용하여, 집계된 개체(컴포넌트)는 집계자가 파괴된 후에도 계속 생존할 수 있다. 예를 들어 동물원 개체에 많은 동물 개체가 포함되어 있다고 가정한다. 동물원 개체가 파산했기 때문에 파괴되었을 때, 동물 개체들은 (이상적으로) 파괴되지 않고 다른 동물원으로 옮겨진다.
- Composition (합성): 합성을 사용하면, 다른 개체로 합성된 개체가 파괴되면 다른 개체도 파괴된다. 예를 들어, 버튼이 포함된 윈도우 개체가 소멸되면, 버튼 개체도 함께 소멸된다.
The Is-a Relationship (Inheritance)
is-a 관계는 개체 지향 프로그래밍의 기본 개념으로 파생(deriving), 서브클래싱(subclassing), 확장(extending)과 상속(inheriting)을 포함한 많은 이름을 가지고 있다. 클래스는 실제 세계에 속성과 동작이 있는 개체가 포함되어 있다는 사실을 모델링한다. 상속은 이러한 개체가 계층 구조로 구성되는 경향이 있다는 사실을 모델링한다. 이러한 계층은 is-a 관계를 나타낸다.
기본적으로 상속은 A가 B이거나, A가 실제로 B와 매우 비슷한 패턴을 따른다. 이는 까다로울 수 있다. 간단한 경우를 고수하기 위해, 동물원을 다시 방문하지만 원숭이 외에 다른 동물이 있다고 가정한다. 그 문장만으로 이미 원숭이가 동물이라는 구축된 관계를 가질 수 있다. 마찬가지로 기린도 동물, 캥거루도 동물, 그리고 펭귄도 동물이다. 그래서 어쩌라고? 글쎄, 상속의 마법은 원숭이, 기린, 캥거루, 펭귄이 특정한 공통점이 있다는 것을 깨달을 때 온다. 이러한 공통점은 일반적으로 동물들의 특징이다.
이것이 프로그래머에게 의미하는 바는 모든 동물과 관련된 모든 속성(크기, 위치, 식단 등)과 동작(이동, 먹기, 수면)을 캡술화하는 Animal 클래스를 정의할 수 있다는 것이다. 동물의 모든 특성을 원숭이가 포함하고 있기 때문에, 원숭이와 같은 특정 동물은 Animal의 파생 클래스가 된다. 기억할 것은, 원숭이는 동물에 더해 그것과 구별하게 만드는 몇 가지 추가적인 특성들이 있다. Figure 5-4는 동물에 대한 상속 다이어그램을 보여준다.
원숭이와 기린이 다른 유형의 동물인 것처럼, 사용자 인터페이스에는 종종 다른 유형의 버튼이 있다. 예를 들어 체크박스는 버튼이다. 버튼이 단순히 클릭하여 작업을 수행할 수 있는 UI 요소라고 가정하면, Checkbox는 박스가 선택되었는지 여부에 관계없이 상태를 추가하여 Button 클래스를 확장한다.
is-a 관계의 클래스를 연관시킬 때, 한 가지 목표는 공통 기능을 다른 클래스로 확장하는 base class에 포함시키는 것이다. 파생된 모든 클래스에 유사하거나 정확히 동일한 코드가 있는 경우, 해당 코드의 일부나 전부를 base class로 이동할 수 있는 방법을 고려한다. 그렇게 하면, 필요한 모든 변경 사항이 한 곳에서만 발생하고, 향후 파생 클래스는 "무료"로 공유 기능을 사용할 수 있다.
Inheritance Techniques (상속 기법)
앞의 예제에서는 상속에 사용되는 몇 가지 기술을 형식화하지 않고 다룬다. 클래스를 파생할 때, 프로그래머가 base class나 superclass라고도 하는 parent class와 클래스를 구별할 수 있는 몇 가지 방법이 있다. 파생 클래스는 이러한 기술 중 하나 이상을 사용할 수 있으며, "A is B that is..(A는 ..인 B 이다)"라는 문장을 완성함으로써 인식된다.
Adding Functionality (기능 추가)
파생 클래스는 추가 기능을 추가하여 부모 클래스를 보강할 수 있다. 예를 들어, 원숭이는 나무에서 스윙할 수 있는 동물이다. Monky 클래스는 Animal의 모든 메서드를 가지고 있을 뿐만 아니라, Monky 클래스에만 해당하는 swingFromTrees() 메서드도 가지고 있다.
Replacing Functionality (기능 교체)
파생 클래스는 부모 클래스의 메서드를 완전히 대체하거나 재정의할 수 있다. 예를 들어, 대부분의 동물은 걸어서 이동하므로, Animal 클래스에 걷기를 시뮬레이션하는 move() 메서드를 제공할 수 있다. 그렇다면 캥거루는 걷는 대신 깡충깡충 뛰면서 이동하는 동물이다. Animal 기본 클래스의 다른 모든 속성과 메서드는 여전히 적용되지만, Kangaroo 파생 클래스는 단순히 move() 메서드가 작동하는 방식을 변경한다. 물론, 기본 클래스의 모든 기능을 대체하는 경우, 기본 클래스가 abstract 기본 클래스가 아닌 한 상속이 결국 올바른 작업이 아니라는 것일 수 있다. 추상 기본 클래스는 각 파생 클래스가 추상 기본 클래스에 구현되지 않은 모든 메서드를 구현하도록 한다. 추상 기본 클래스의 인스턴스를 만들 수 없다. 추상 기본 클래스는 Chapter 10에서 설명한다.
Adding Properties (속성 추가)
파생 클래스는 기본 클래스에서 상속된 속성에 새 속성을 추가할 수도 있다. 예를 들어, 펭귄은 동물의 모든 속성을 가지고 있지만, 부리 크기 속성도 가지고 있다.
Replacing Properties (속성 교체)
C++는 메서드를 재정의할 수 있는 방법과 유사하게 속성 재정의 방법을 제공한다. 그러나 그렇게 하는 것은, 기본 클래스에서 속성을 숨기기 때문에 적절하지 않다. 즉, 기본 클래스는 특정 이름의 속성에 대해 특정 값을 가질 수 있는 반면, 파생 클래스는 이름은 같지만 다른 속성에 대해 가진 다른 값을 가질 수 있다. 숨김은 Chapter 10에서 자세히 설명한다. 속성을 대체한다는 개념을 속성에 대해 서로 다른 값을 갖는 파생 클래스의 개념과 혼동하지 않는 것이 중요하다. 예를 들어, 모든 동물은 그들이 무엇을 먹는지 나타낸는 식습관 속성을 가지고 있다. 원숭이는 바나나를 먹고 펭귄은 물고기를 먹지만, 둘 다 어느 것도 식습관 속성을 대체하지 못한다. 단순히 속성에 부여된 가치가 다를 뿐이다.
Polymorphism (다형성)
다형성은 속성과 메소드의 표준 세트를 준수하는 개체를 상호 교환하여 사용할 수 있다는 개념이다. 클래스 정의는 개체와 개체와 상호작용하는 코드 사이의 계약과 같다. 정의에 따라 모든 Monky 개체는 Monky 클래스의 속성과 메서드를 지원해야 한다.
이 개념은 기본 클래스로도 확장된다. 모든 원숭이는 동물이기 때문에, 모든 Monky 개체는 Animal 클래스의 속성과 메서드도 지원한다. 다형성은 상속이 제공하는 것을 진정으로 활용하기 때문에 개체 지향 프로그래밍의 아름다운 부분이다. 동물원 시뮬레이션에서는, 동물원에 있는 모든 동물을 프로그래밍 방식으로 반복하면서 각 동물을 한 번씩 움직일 수 있다. 모든 동물은 Animal 클래스의 멤버이기 때문에, 모두 이동 방법을 알고 있다. 일부 동물은 이동 메소드를 재정의했지만, 이것이 가장 좋은 부분이다. 코드는 단순히 그것이 어떤 종류의 동물인지 알지 못하거나 신경 쓰지 않고 각 동물들에게 이동하도록 지시한다. 각자가 아는 방법 대로 움직인다.
The Fine Line Between Has-a and Is-a (Has-a와 Is-a 사이의 미세한 경계)
현실 세계에서 개체 사이의 has-a와 is-a 관계를 분류하는 것은 매우 쉽다. 오렌지에 과일이 있다고 주장하는 사람은 아무도 없다. 오렌지는 과일이다(An orange is a fruit). 코드에서는 상황이 그렇게 명확하지 않을 때가 있다.
키를 값으로 효울적으로 매핑하는 데이터 구조인, 연관 배열을 나타내는 가상(hypothetical) 클래스를 고려해본다. 예를 들어, 보험 회사는 AssociativeArray 클래스를 사용하여 회원 ID를 이름에 매핑하여, ID가 주어지면 해당 회원 이름을 쉽게 찾을 수 있도록 할 수 있다. 회원 ID는 키(key)이고, 회원 이름은 값(value)이다.
표준 연관 배열의 구현에서는, 키는 단일 값과 연관되어 있다. ID 14534가 회원 이름 "Kleper, Scott"에 매핑되는 경우, 회원 이름 "Kleper, Marni"에는 역시 매핑할 수 없다. 대부분 구현에서, 이미 값을 가지고 있는 키에 두 번째 값을 추가하려고 시도하면, 첫 번째 값이 사라지고 만다. 다시 말해, ID 14534가 "Kleper, Scott"에 매칭된 다음에 ID 14534를 "Kleper, Marni"에 할당했다면, Scott은 사실상 무보험 상태(알 수 없는 상태)가 된다. 이는 가상의 inset() 메서드에 대한 두 번의 호출과 연관 배열의 결과 내용을 보여주는 다음 순서로 설명될 수 있다:
연관 배열과 비슷하지만, 주어진 키에 대해 여러 값을 허용하는 데이터 구조의 용도를 상상하는 것은 어렵지 않다. 보험의 예에서 가족은 동일한 ID에 해당하는 여러 이름을 가질 수 있다. 그러한 데이터 구조는 연관 배열과 유사하기 때문에 그 기능을 어떻게든 활용하는 것이 좋을 것이다. 연관 배열은 키로 하나의 값만 가질 수 있지만, 그 값은 무엇이든 될 수 있다. 문자열 대신, 값은 키에 대한 여러 값을 포함하는 컬렉션(예: vector)일 수 있다. 기존 ID에 대해 새 회원을 추가할 때마다 컬렉션에 이름을 추가한다. 이것은 다음 순서와 같이 작동한다:
문자열 대신 컬렉션을 사용하는 것은 지루하고 반복적인 코드가 많이 필요하다. 아마도 MultiAssociativeArray라고 하는 별도의 클래스에서 이 다중 값 기능을 래핑하는 것이 좋을 것이다. MultiAssociativeArray 클래스는 배후에서 각 값을 단일 문자열 대신 문자열의 컬렉션으로 저장한다는 점을 제외하고는 AssociativeArray 처럼 작동할 것이다. 분명히, MultiAssociativeArray는 여전히 연관 배열을 사용하여 데이터를 저장하기 때문에 AssociativeArray와 어떻게 든 관련이 있다. 불분명한 것은 그것이 is-a 관계를 구성하는지 아니면 has-a 관계를 구성하는지 여부이다.
is-a 관계로 시작하기 위해, MultiAssociativeArray가 AssociativeArray의 파생 클래스라고 가정하자. 배열에 항목을 추가하는 메서드를 재정의하여, 컬렉션을 만들고 새 요소를 추가하거나 기존 컬렉션을 검색하여 거기에 새 요소를 추가해야 한다. 또한 값을 검색하는 메서드를 재정의해야 한다. 그러나 복잡한 문제가 있다. 재정의된 get() 메서드는 컬레션이 아닌 단일 값을 반환해야 한다. MultiAssociativeArray에서 반환할 값은 무엇인가요? 한 가지 옵션은 지정된 키와 연결된 첫 번째 값을 반환하는 것이다. 키와 연결된 모든 값을 검색하기 위해 getAll() 메서드를 추가할 수 있다. 이는 완벽하고 합리적인 디자인처럼 보인다. 기본 클래스의 모든 메서드를 재정의하더라도, 파생 클래스 내에서 원래 메서드를 사용하여 기본 클래스의 메서드를 계속 사용할 수 있다. Figure 5-5는 이 접근 방식을 UML 클래스 다이어그램으로 보여준다.
이제 이것을 has-a 관계로 간주해 본다. MultiAssociativeArray는 자체 클래스이지만, AssociativeArray 개체를 포함한다. 아마도 AssociativeArray와 유사한 인터페이스를 가질 수 있지만, 동일할 필요는 없다. 뒤에서 사용자가 MultiAssociativeArray에 무언가를 추가하면, 실제로 컬렉션에 래핑되어 AssociativeArray 개체에 배치된다. 이 역시 Fugure 5-6과 같이 완벽하게 합리적으로 보인다.
그렇다면, 어떤 솔루션이 옳은 것일까? 명확한 답은 없지만, 프로덕션 용도로 MultiAssociativeArray 클래스를 작성한 내 친구는 이를 has-a 관계로 보았다. 주된 이유는 연관 배열 기능을 유지하는 것에 대해 걱정하지 않고 노출된 인터페이스를 수정할 수 있다는 것이다. 예를 들어, Figure 5-6에서 get() 메서드는 getAll()로 변경되어, MultiAssociativeArray의 특정 키를 대한 모든 값을 가져올 것을 분명하게 한다. 또한 has-a 관계를 사용하면, 연관 배열 기능이 유입되는 것에 대해 걱정할 필요가 없다. 예를 들어, 연관 배열 클래스가 값의 총 갯수를 가져오는 메서드를 지원하는 경우, MultiAssociativeArray가 이 메서드를 재정의할 것을 알지 못하는 한 컬렉션 수를 보고한다.
즉, MultiAssociativeArray는 실제로 몇 가지 새로운 기능이 있는 AssociativeArray이며, is-a 관계여야 한다는 설득력 있는 주장을 할 수 있다. 요점은 때로는 두 관계 사이에 미세한 경계가 있다는 것이다. 그리고 클래스가 어떻게 사용될 것인지, 구축하려는 것이 다른 클래스의 일부 기능을 활용하는지, 실제로 수정된 클래스인지, 새로운 기능인지 고려해야 한다.
다음 테이블은 MultiAssociativeArray 클래스에 대해 두 접근 방식 중 하나를 선택하는 것에 대한 찬성과 반대 주장을 나타낸다:
IS-A | HAS-A | |
찬성 이유 | 기본적으로, 다른 특성을 가진 동일한 추상화이다. AssociativeArray와 (거의) 동일한 메서드를 제공한다. |
MultiAssociativeArray는 AssociativeArray가 어떤 메소드를 가지고 있는지 걱정할 필요 없이 유용한 메소드를 가질 수 있다. 구현은 노출된 메소드를 변경하지 않고 AssociativeArray 이외의 다른 것으로 변경될 수 있다. |
반대 이유 | 정의에 의하여 연관 배열에는 키당 하나의 값이 있다. MultiAssociativeArray가 연관 배열이라고 말하는 것은 신성 모독이다! MultiAssociativeArray는 AssociativeArray의 두 메서드를 모두 재정의한다. 이는 디자인에 문제가 있다는 강력한 신호이다. 알 수 없거나 부적절한 AssociativeArray의 속성이나 메서드가 MultiAssociativeArray로 "유입"될 수 있다. |
어떤 의미에서, MultiAssociativeArray는 새로운 방법을 제시하여 바퀴를 재발명한다. AssociativeArray의 일부 추가 속성과 메서드가 유용했을 수 있다. |
이 경우 is-a 관계를 사용하는 것에 반대하는 이유는 꽤 강력하다. 또한, Liskov 대체 원칙(LSP: Liskov substitution principle)은 is-a와 has-a 관계 사이에서 결정을 내리는 데 도움이 될 수 있다. 이 원칙에 따르면 동작을 변경하지 않고 기본 클래스 대신 파생 클래스를 사용할 수 있어야 한다. 이 예제를 적용하면, 이전에 AssociativeArray를 사용하던 곳에서 MultiAssociativeArray 사용을 시작할 수 없기 때문에 이것이 has-a 관계가 되어야 한다고 명시되어 있다. 예를 들어, AssociativeArray의 insert() 메서드는 배열에 이미 있는 동일한 키의 이전 값을 제거하지만, MultiAssociativeArray는 이러한 값을 제거하지 않는다.
이 섹션에서 자세히 설명한 두 가지 솔루션만이 실제로 가능한 솔루션은 아니다. 다른 옵션으로는 AssociativeArray가 MultiAssociativeArray에서 상속하고, AssociativeArray가 MultiAssociativeArray를 포함할 수 있으며, AssociativeArray와 MultiAssociativeArray가 모두 공통 기본 클래스에서 상속될 수 있다. 특정 디자인에 대해 생각해낼 수 있는 다양한 솔루션이 있는 경우도 있다.
만약 두 가지 유형의 관계 중 하나를 선택해야 하는 경우, is-a 관계보다 has-a 관계를 선택하는 것을 수년간의 경험으로 추천한다. AssociativeArray와 MultiAssociativeArray는 is-a 관계와 has-a 관계의 차이점을 보여주기 위해 여기에서 사용된다. 자체 코드에서는, 직접 코드를 작성하는 대신 표준 연관 배열 클래스 중 하나를 사용하는 것이 좋다. C++ Standard Library는 AssociativeArray 대신 사용해야 하는 map 클래스와 MultiAssociativeArray 대신 사용해야 하는 multimap 클래스를 제공한다. 이러한 표준 클래스는 Chapter 18, "Standard Library Containers"에서 설명한다.
The Not-a Relationship (비 관계)
어떤 유형의 관계 클래스가 있는지 고려할 때, 실제로 관계가 있는지 여부를 고려해야 한다. 개체 지향 디자인에 대한 열정이 불필요한 클래스/파생-클래스 관계로 변질되지 않도록 해야한다.
한 가지 함정은 사물이 실제 세계에서는 분명히 관련되어 있지만, 코드에서는 실제 관계가 없을 때 발생한다. 개체 지향 계층 구조는 인위적인 관계가 아닌 기능적 관계를 모델링해야 한다. Figure 5-7은 온톨로지나 계층 구조로 의미가 있는 관계이지만, 코드에서 의미 있는 관계를 나타내지 않을 것 같은 관계를 보여준다.
불필요한 상속을 피하는 가장 좋은 방법은 먼저 디자인을 스케치하는 것이다. 모든 클래스와 파생 클래스에 대해, 클래스에 넣으려고 계획한 속성과 메서드를 기록한다. 클래스에 특정 속성이나 자체 메소드가 없는 경우, 또는 앞서 언급한 기본 추상 클래스로 작업할 때를 제외하고 모든 속성과 메서드가 파생 클래스에 의해 완전히 재정의되는 경우, 디자인을 다시 생각해야 한다.
Hierarchies (계층 구조)
클래스 A가 B의 기본 클래스가 될 수 있는 것처럼, B도 C의 기본 클래스가 될 수 있다. 개체 지향 계층 구조는 이와 같은 다단계(multilevel) 관계를 모델링할 수 있다. 더 많은 동물이 있는 동물원 시뮬레이션은 Figure 5-8과 같이 모든 동물을 공통적으로 Animal 클래스의 파생 클래스로 디자인할 수 있다.
이러한 파생 클래스들을 각각 코딩할 때, 클래스의 많은 부분이 유사하다는 것을 알 수 있다. 이런 일이 발생하면, 공통 부모를 넣는 것을 고려해야 한다. Lion과 Panther가 모두 같은 방식으로 움직이고 같은 식단을 가지고 있다는 사실을 깨닫는 것은 가능한 BigCat 클래스가 필요하다는 것을 의미할 수 있다. Animal 클래스를 더 세분화하여 WaterAnimal과 Marsupial을 포함할 수 있다. Figure 5-9는 이러한 공통점을 활용하는 보다 계층적인 디자인을 보여준다.
이 계층 구조를 보고 있는 생물학자는 실망할 수 있다. 펭귄은 실제로 돌고래와 같은 가족이 아니다. 그러나 코드에서 실제 세계의 관계와 공유된 기능의 관계가 균형을 맞춰야 한다는 좋은 점을 강조한다. 실제 세계에서는 두 가지가 밀접하게 관련되어 있을 수 있지만, 실제로 기능을 공유하지 않기 때문에 코드에서는 관계가 아닐 수 있다. 동물은 포유류와 물고기로 쉽게 나눌 수 있지만, 그것은 기본 클래스에 대한 어떤 공통점도 고려하지 않을 것이다.
또 다른 중요한 점은 계층 구조를 구성하는 다른 방법이 있을 수 있다는 것이다. 앞의 디자인은 주로 동물들이 움직이는 방식으로 구성된다. 대신 동물들의 식단이나 키에 따라 구성된다면, 계층 구조는 매우 달라질 수 있다. 결국 중요한 것은 클래스를 어떻게 사용할 것인가이다. 요두 사항에 따라 클래스 계층 구조의 디자인이 결정된다.
좋은 개체 지향 계층 구조는 다음을 달성한다:
- 클래스를 기능적으로 의미있는 관계로 구성
- 기본 클래스에 공통 기능을 팩터링하여 코드 재사용을 지원
- 부모가 기본 추상 클래스가 아닌 한, 부모의 기능을 대부분 재정의하는 파생 클래스를 피한다.
Multiple Inheritance (다중 상속)
지금까지의 모든 예제는 단일 상속 체인이었다. 즉, 주어진 클래스는 기껏해야 하나의 직계 부모 클래스를 가진다. 꼭 그럴 필요는 없다. 다중 상속을 통해, 클래스는 둘 이상의 기본 클래스를 가질 수 있다.
Figure 5-10은 다중 상속 디자인을 보여준다. Animal이라는 기본 클래스가 여전히 있으며, 크기에 따라 더 나뉜다. 별도의 계층 구조는 식단으로 분류되고, 세 번째는 움직임을 처리한다. 각 종류의 동물은 서로 다른 선으로 표시된 것처럼 이 세가지 클래스 모두에서 파생된 클래스이다.
사용자 인터페이스 컨텍스트에서 사용자가 클릭할 수 있는 이미지를 상상해 본다. 이 클래스는 버튼과 이미지 둘 다인 것 같으므로, 구현은 Figure 5-11과 같이 Image 클래스와 Button 클래스 모두 상속에 포함할 수 있다.
다중 상속은 특정 경우에 유용할 수 있지만, 항상 염두에 두어야 하는 여러 가지 단점도 있다. 많은 프로그래머는 다중 상속을 싫어한다. C++는 이러한 관계를 명시적으로 지원하지만, Java 언어는 다중 인터페이스(추상 기본 클래스)에서 상속하는 것을 제외하고는 이러한 관계를 모두 제거한다. 다중 상속 비평가들이 지적하는 몇 가지 이유가 있다.
첫 번째, 다중 상속을 시각화하는 것은 복잡하다. Figure 5-10에서 볼 수 있듯이 여러 계층 구조와 교차선이 있는 경우에는 간단한 클래스 다이어그램도 복잡해질 수 있다. 클래스 계층 구조는 프로그래머가 코드 간의 관계를 더 쉽게 이해할 수 있도록 해주는 것을 가정한다. 다중 상속을 사용하면, 클래스는 서로 관련이 없는 여러 부모를 가질 수 있다. 개체에 코드를 제공하는 클래스가 너무 많으면, 실제로 무슨 일이 일어나고 있는지 추적할 수 있을까?
두 번째, 다중 상속은 다른 방법으로 깨끗한(clean) 계층 구조를 파괴할 수 있다. 동물 예제에서, 다중 상속 접근 방식으로 전환하면 동물을 기술하는 코드가 이제 세 개의 계층 구조로 분리되기 때문에 Animal 기본 클래스의 의미가 적다는 것을 의미한다. Figure 5-10에 설명된 디자인은 세 가지 깨끗한(clean) 계층 구조를 보여주지만, 어떻게 지저분해질 수 있는지 상상하는 것은 어렵지 않다. 예를 들어, 모든 Jumper가 같은 방식으로 움직일 뿐만 아니라 같은 것을 먹는다는 것을 알게 된다면 어떻게 될까요? 별도의 계층 구조가 있기 때문에, 다른 파생 클래스를 추가하지 않고 이동과 식단 개념을 결합할 수 있는 방법이 없다.
세 번째, 다중 상속의 구현은 복잡하다. 두 개의 기본 클래스가 동일한 메서드를 다른 방식으로 구현하면 어떻게 될까요? 공통 기본 클래스의 파생 클래스인 두 개의 기본 클래스를 가질 수 있나요? 코드에서 이러한 복잡한 관계를 구조화하는 것은 작성자와 독자 모두에게 어렵기 때문에, 이러한 가능성은 구현을 복잡하게 만든다.
다른 언어들이 다중 상속을 생략할 수 있는 이유는 대게 피할 수 있기 때문이다. 계층 구조를 다시 생각해보면, 프로젝트 디자인을 제어할 수 있는 경우 다중 상속 도입을 피할 수 있다.
Mixin Classes (믹스인 클래스)
Mixin 클래스는 클래스 사이의 또 다른 유형의 관계를 나타낸다. C++에서 Mixin 클래스를 구현하는 한 가지 방법은 구문상으로 다중 상속과 같다. 하지만 의미는 완전히 다르다. Mixin 클래스는 "이 클래스는 또 무엇을 할 수 있을까?"라는 질문에 답한다. 그리고 대답은 종종 "-able(~가능한)"로 끝난다. Mixin 클래스는 완전한 is-a 관계를 커밋(적용)하지 않고 클래스에 기능을 추가할 수 있는 방법이다. 그것을 공유 관계라고 생각할 수 있다.
동물원의 예로 돌아가서, 일부 동물들은 "pettable(쓰다듬을 수 있는)"이라는 개념으로 소개하고 싶을 수 있다. 즉, 동물원을 찾는 방문객들이 아마도 물리거나 상해를 입지 않고, 쓰다듬을 수 있는 동물이 있다. 쓰다듬을 수 있는 모든 동물이 "be pet(애완 동물이 되다)"이라는 동작을 지원하기 원할 수 있다. 쓰다듬을 수 있는 동물은 공통점이 없고 여러분이 디자인한 기존 계층 구조를 깨고 싶지 않기 때문에, Pettable은 훌륭한 Mixin 클래스를 만든다.
MIxin 클래스는 사용자 인터페이스에서 자주 사용된다. PictureButton 클래스가 Image와 Button이라고 말하는 대신, Clickable(클릭 가능한) Image라고 말할 수 있다. 바탕 화면의 폴더 아이콘은 Draggable(드래그 가능한)와 Clickable(클릭 가능한)이 가능한 Image일 수 있다. 소프트웨어 개발자는 재미있는 형용사를 많이 만드는 경향이 있다.
Mixin 클래스와 기본 클래스의 차이는 코드 차이보다 클래스에 대해 생각하는 방식과 관련이 더 있다. 일반적으로 Mixin 클래스는 범위가 매우 제한적이기 때문에 다중 상속보다 이해하기 쉽다. Pettable Mixin 클래스는 기존 클래스에 하나의 동작을 추가한다. Clickable Mixin 클래스는 "mouse down"과 "mouse up" 동작을 추가할 수 있다. 또한, Mixin 클래스에는 큰 계층 구조가 거의 없으므로, 기능의 교차 혼합(contamination)이 없다. Chapter 32, "Incorporating Design Techniques and Frameworks"에서 Mixin 클래스에 대해 더 자세히 설명한다.
SUMMARY
이 챕터에서는 많은 코드에 방해되지 않고 개체 지향 프로그램을 디자인하는 것에 대한 진가를 얻었다. 배운 개념은 거의 모든 개체 지향 언어에 적용할 수 있다. 그 중 일부는 당신에게 리뷰였을 수도 있고, 익숙한 개념을 공식화하는 새로운 방법일 수도 있다. 당신의 팀에게 줄곧 설교해 온 개념에 찬성하여, 오래된 문제나 새로운 주장에 대한 몇 가지 새로운 접근 방식을 당신은 아마도 선택했을 것이다. 코드에서 클래스를 사용한 적이 없거나 드물게 사용했더라도, 이제 많은 경험이 있는 C++ 프로그래머보다 개체 지향 프로그램을 디자인하는 방법에 대해 더 많이 알게 되었다.
잘 연결된 클래스가 코드 재사용에 기여하고 혼란을 줄이는 데 기여할 뿐만 아니라 팀에서 작업하기 때문에 클래스 사이의 관계는 연구하는 데 중요하다. 의미 있는 방식으로 관련된 클래스는 읽고 유지하기가 더 쉽다. 프로그램을 디자인할 때 "Class Relationships" 섹션을 참조로 사용할 수 있다.
다음 챕터에서는 재사용을 염두에 두고 코드를 디자인하는 방법을 설명하면서 디자인 테마를 계속 진행할 것이다.
EXERCISES
다음 연습문제를 풀면 이 챕터에서 설명한 내용을 연습할 수 있다. 모든 연습문제에 대한 솔루션은 책의 웹사이트(www.wiley.com/go/proc++5e)에서 코드를 다운로드하여 사용할 수 있다. 그러나, 연습문제를 풀지 못하는 경우, 웹 사이트에서 솔루션을 보기 전에 이 챕터의 일부를 다시 읽고 스스로 답을 찾아보기 바란다.
이 챕터의 연습문제에는 정답이 없다. 이 챕터의 과정에서 배웠듯이, 특정 문제에는 종종 서로 다른 절충안(trade-off)을 가진 여러 디자인 솔루션이 있다. 이 연습문제와 함께 제공되는 솔루션에는 하나의 가능한 디자인을 설명하고 있지만, 그렇다고 당신이 생각한 솔루션이 해당 디자인과 일치해야 하는 것은 아니다.
Exercise 5-1: 자동차 경주 게임을 작성한다고 가정해 본다. 자동차 자체에 대한 일종의 모델이 필요할 것이다. 이 연습문제에서는 한 가지 유형의 자동차만 있다고 가정한다. 해당 차동차의 각 인스턴스는 엔진의 현재 출력, 현재 연료 사용량, 타이어 공기압, 주행등이 켜져 있는지 여부, 앞유리 와이퍼가 동작하고 있는지 여부와 기타 등등 여러 속성을 추적해야 한다. 게임을 통해 플레이어는 다른 엔진, 다른 타이어, 맞춤형 주행등과 앞유리 와이퍼 등으로 자동차를 구성할 수 있다. 그러한 자동차를 어떻게 모델링할 것이고, 그 이유는 무엇인가?
Exercise 5-2: Exercise 5-1의 자동차 경주 게임을 계속 진행하면서, 사람이 운전하는 자동차뿐만 아니라 인공 지능(AI)이 운전하는 자동차에 대한 지원도 포함하려고 한다. 당신은 게임에서 이것을 어떻게 모델링할 것인가?
Exercise 5-3: 인적 자원(HR) 애플리케이션의 일부로 다음 세 가지 클래스가 있다고 가정한다:
- Employee: 사원 ID, 급여, 근무하기 시작한 날짜등을 추적
- Person: 이름과 주소 추적
- Manager: 어떤 직원이 팀에 있는지 추적
Figure 5-12의 상위 레벨의 클래스 다이어그램에 대해 어떻게 생각하는가? 변경 사항이 존재하는가? 다이어 그램은 Exercise 5-4의 주제이므로 다른 클래스의 속성이나 동작을 보여주지 않는다.
Exercise 5-4: Exercise 5-3의 최종 클래스 다이어그램에서 시작한다. 클래스 다이어그램에 몇 가지 동작과 속성을 추가한다. 마지막으로 관리자가 팀의 직원을 관리한다는 사실을 모델링한다.
'Programming Language > Professional C++' 카테고리의 다른 글
Professional C++ - 7. Memory Management (0) | 2022.12.19 |
---|---|
Professional C++ - 6. Designing for Reuse (0) | 2022.11.03 |
Professional C++ (0) | 2022.09.08 |
Professional C++ - 4. Designing Professional C++ Programs (0) | 2022.09.08 |
Professional C++ - 3. Coding with Style (0) | 2022.07.21 |