티스토리 뷰

 

Effective C++ 이펙티브 C++
스콧 마이어스 저/곽용재

 

 

6. 상속, 그리고 객체 지향 설계 (3)

 

 

항목 38 : "has-a(...는...를 가짐)" 혹은 "is-implemented-in-terms-of(...는...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자

 

합성(Composition)이란, 어떤  타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계를 일컫습니다.

 

응용 영역 : 객체 중 우리 일상생활에서 볼 수 있는 사물을 본 뜬 것들.

구현 영역 : 응용 영역에 속하지 않는 것들. 시스템 구현만을 위한 인공물.

 

연결 리스트를 재사용하는 Set 템플릿 예

 

// list를 잘못 사용한 예(is-a)

template<typename T>

class Set : public std::list<T> { ... };

list 객체는 중복 원소를 가질 수 있는 컨테이너이므로 Set 객체와 is-a 관계가 성립될 수 없습니다.

 

// list를 제대로 사용한 예(is implemented in terms of)

template<class T>

class Set

{

public:

bool member(cost T& item) const;

void insert(const T& item);

void remove(const T& item);

std::size_t size() const;

private:

std::list<T> rep;

};

 

  • 객체 합성(composition)의 의미는 public 상속이 가진 의미와 완전히 다릅니다.
  • 응용 영역에서 객체 합성의 의미는 has-a입니다. 구현 영역에서는 is-implemented-in-terms-of의 의미를 갖습니다.

 

 

항목 39 : private 상속은 심사숙고해서 구사하자

 

private 상속은 소프트웨어 설계(design) 도중에는 아무런 의미도 갖지 않으며, 단지 소프트웨어 구현(implementation) 중에만 의미를 가질 뿐입니다.

할 수 있으면 객체 합성을 사용하고, 꼭 해야 하면 private 상속을 사용하십시오.

 

타이머를 사용하는 Widget 객체 예

 

class Timer

{

public:

explicit Timer(int tickFrequency);

// 일정 시간이 경과할 때마다 자동으로 호출

virtual void onTick() const;

};

 

// 항목 18("제대로 사용하기에는 쉽게, 잘못 사용하기에는 어렵게 만들라") 위반 

class Widget: private Timer

{

private:

// Widget 사용 자료 등을 수집

virtual void onTick() const;

...

};

 

올바른 예 - public 상속에 객체 합성 조합

class Widget

{

private:

class WidgetTimer: public Timer

{

public:

virtual void onTick() const;

...

};

WidgetTimer timer;

...

};

 

public 상속에 객체 합성 조합의 이점

1. 파생은 가능하게 하되, 파생 클래스에서 onTick을 재정의할 수 없도록 설계 가능.

2. Widget의 컴파일 의존성을 최소화.

 

  • private 상속의 의미는 is-implemented-in-terms-of입니다. 대개 객체 합성과 비교해서 쓰이는 분야가 많지는 않지만, 파생 클래스 쪽에서 기본 클래스의 protected 멤버에 접근해야 할 경우 혹은 상속받은 가상 함수를 재정의해야 할 경우에는 private 상속이 나름대로 의미가 있습니다.
  • 객체 합성과 달리, private 상속은 공백 기본 클래스 최적화(EBO)를 활성화시킬 수 있습니다. 이 점은 객체 크기를 가지고 고민하는 라이브러리 개발자에게 꽤 매력적인 특징이 되기도 합니다.

 

 

항목 40 : 다중 상속은 심사숙고해서 사용하자

 

다중 상속을 올바르게 사용한 예 - 인터페이스의 public 상속과 구현의 private 상속의 조합

 

// 용도에 따라 구현될 인터페이스 

class IPerson

{

public:

virtual ~IPerson();

virtual std::string name() const = 0;

virtual std::string birthDate() const = 0;

};

 

class DatabaseID { ... };

 

// IPerson 인터페이스를 구현하는데 유용한 함수 포함

class PersonInfo

{

public:

explicit PersonInfo(DatabaseID pid);

virtual ~Person();

virtual const char *theName() const;

virtual const char *theBirthDate() const;

virtual const char *valueDelimOpen() const;

virtual const char *valueDelimClose() const;

...

};

 

class CPerson: public IPerson, private PersonInfo

{

public:

explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}

// IPerson 클래스의 순수 가상 함수에 대해 파생 클래스의 구현 제공

virtual std::string name() const

{ return PersonInfo::theName(); }

virtual std::string birthDate() const

{ return PersonInfo::theBirthDate(); }

private:

// 구분자에 관련된 가상 함수들도 상속되므로 이 함수들에 대한 재정의 버전을 만듭니다.

const char *valueDelimOpen() const { return " "; }

const char *valueDelimClose() const { return " "; }

};

 

// 유일한 데이터베이스 ID로부터 IPerson 객체를 만들어내는 팩토리 함수

std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

// 사용자로부터 데이터베이스 ID를 얻어내는 함수

DatabaseID askUserForDatabaseID();

DatabaseID id(askUserForDatabaseID());

std::tr1::shared_ptr<IPerson> pp(makePerson(id)); 

 

// IPerson 인터페이스를 지원하는 객체를 하나 만들고 pp로 가리키게 합니다.

// 이후에는 *pp의 조작을 위해 IPerson의 멤버 함수를 사용합니다.

...

 

  • 다중 상속은 단일 상속보다 확실히 복잡합니다. 새로운 모호성 문제를 일으킬 뿐만 아니라 가상 상속이 필요해질 수도 있습니다.
  • 가상 상속을 쓰면 크기 비용, 속도 비용이 늘어나며, 초기화 몇 대입 연산의 복잡도가 커집니다. 따라서 가상 기본 클래스에는 데이터를 두지 않을 것이 현실적으로 가장 실용적입니다.
  • 다중 상속을 적법하게 쓸 수 있는 경우가 있습니다. 여러 시나리오 중 하나는, 인터페이스 클래스로부터 public 상속을 시킴과 동시에 구현을 돕는 클래스로부터 private 상속을 시키는 것입니다.

 

- James Song

댓글