- Item 33: 유도된 클래스가 없는 상태의 클래스로 추상화 하라.
당신이 동물의 역할을 하는 소프트웨어 프로젝트를 진행한다고 가정해라. 이 소프트웨어에서는 대부분의 동물들이 같게 취급될 수 있다. 그렇지만 두 종류의 동물들 -lizard(도마뱀) 와 chicken(닭)- 은 특별한 핸들링(handling)을 원한다. 그러한 경우에, 명백한 방법은 다음과 같이 관계를 만들어 버리는 것이다.
Animal 클래스는 주어진 모든 생명체들이 공유하고 있는 부분이다. 그리고 Lizard과 Chicken 클래스는 Animal에서 도마뱀과 닭만으로 특화된 클래스이다.
여기 이들 클래스들의 대강의 모습을 그린다.
~cpp
class Animal {
public:
Animal& operator=(const Animal& rhs);
...
};
class Lizard: public Animal {
public:
Lizard& operator=(const Lizard& rhs);
...
};
class Chicken: public Animal {
public:
Chicken& operator=(const Chicken& rhs);
...
};
보다시피 오직 할당(assignment) 연산자만 보인다. 그렇지만 이걸로 유지하는 것은 충분하다. 다음 코드를 생각해 보자.
~cpp
Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;
여기에는 두가지의 문제가 있다.
첫번째로 마지막 줄에 있는 할당 연산자는 Animal 클래스의 것을 부르는데, 객체 형이 Lizad형이라도 Animal 클래스의 연산자로 진행된다. 결과적으로, 오직 liz1의 Animal 부분만이 수정된다. 이것은 부분적인 할당(assignment)이다. liz1에 Animal 멤버의 할당은 li2로부터 얻은 값을 가진다. 그렇지만 liz1의 Lizard 부분의 데이터는 변화하지 않는다.
두번째 문제는 진짜 프로그래머들이 이와 같은 코드를 쓴다는 것이다. 특별히 C++로 전향한 C프로그래머들에 경험에서 보면, 포인터를 통한 객체의 할당은 그리 흔하지 않은것도 아니다. 그러한 경우는 이성적인 생각으로 취한 할당같이 보인다. Item 32의 촛점중, 상속 관계 상에서 우리의 클래스는 정확히 사용하기 쉽고, 부정확하게 사용하기 어렵게 해야 한다고 언급했다.
문제에 대한 한가지 접근으로 할당(assignment)연산자를 가상(virtual)로 선언하는 방법이 있다. 만약
Animal::operator= 가 가상(virtual)이면, 위의 경우에 할당 연산자는 정확한 Lizard 할당 연산자를 호출하려고 시도할 것이다. 그렇지만 만약 우리가 가상으로 할당 연산자를 선언했을때 다음을 봐라.
~cpp
class Animal {
public:
virtual Animal& operator=(const Animal& rhs);
...
};
class Lizard: public Animal {
public:
virtual Lizard& operator=(const Animal& rhs);
...
};
class Chicken: public Animal {
public:
virtual Chicken& operator= (const Animal& rhs);
...
};
언어상에 최근에 변화와 관계있는 의무로 우리는 할당 연산자에 대한 반환값의 최적화를 진행할수 있다. 그래서 각 반환 참조에 정확한 클래스로 교체 할수 있다 하지만 C++의 규칙은 모든 클래스 내부에, 가상 함수에 대하여 동일한 parameter 형을 규정 지을수 있다. 이것의 의미는 Lizard와 Chicken에 대한 할당 연산자가 반드시 할당시 right-hand 부분에 Animal의 어떠한(any) 한종류의 객체에 대한 준비를 해야만 한다는 것이다. 이는 우리에게 다음과 같은 코드가 합법임을 의미하는 것이다.
~cpp
Lizard liz;
Chicken chick;
Animal *pAnimal1 = & liz;
Animal *pAnimal2 = & chick;
...
*pAnimal1 = *pAnimal2; // chicken을 lizard에 할당한단 말이다.!
이것은 mix-type의 할당이다.:Lizard는 오른쪽의 Chicken의 왼쪽에 있는 입장이다. Mixed-type 할당은 C++에서 평범한 문제는 아니다. 왜냐하면 언어의 strong typing은 보통 그것이 규정에서 어긋나게 하기 때문이다. 하지만, animal의 할당 연산자를 가상으로 하는 것에 의해, 닫혀진 Mix-type 연산자의 문이 열려 버린다.
이것은 어려운 면이다. 우리는 same-type 할당만 포인터로 허용하게 만들어야 할것이다. 그에 반하여 같은 포인터를 통한 Mix-type 할당은 거부되도록 만들어야 한다. 다른 말로 하자면 우리는 이런건 통과지만
~cpp
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2; // 통과를 원해요! 같은 형이므로
그렇지만
~cpp
Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chick;
...
*pAnimal1 = *pAnimal2; // 안되길 원해요~
이러한 경우에 형을 가리는 것은 오직 실행 시간 중에 할수 있다. 왜냐하면 어떤때는, *pAnimal2를 *pAnimal1에 유효한 할당임을 알아 내야하고, 어떤때는 아닌걸 증명해야 하기 때문이다. 그래서 우리는 형 기반(type-based)의 실행 시간 에러의 거친 세계로 들어가게 된다. 특별하게, 만약 mixed-type 할당을 만나면, operator= 내부에 에러 하나를 발생하는 것이 필요하다. 그렇지만 만약 type이 같으면 우리는 일반적인 생각에 따라서 할당을 수행하기를 원한다.
우린 dynamic_cast(Item 2참고)를 이러한 행동에 적용해서 사용할수 있다. 여기 Lizard의 할당 연산자에 대한 방법이 있다.
~cpp
Lizard& Lizard::operator=(const Animal& rhs)
{
// lizard가 진짜 rhs가 맞는가?
const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
*this에 rhs_liz의 일반적인 할당과정 수행을 한다.
}
이러한 함수는 *this가 오직 rhs가 Lizard일때만 할당을 허용한다. 만약 이것이 통과되지 못한다면, bad_cast 예외가 dynamic_cast에서 발생되어서 전달되어 진다. (정확히 표준 C++ 라이브러리에 있는 std::bad_cast 형의 객체가 날아간다. 이름 공간이 std에 관한것은 표준 라이브러리를 살피고 Item 35에도 설명되어 있다.)
예외 관한 주의가 필요 없는 보통의 경우에, 이 함수는 복잡하고, 비용 부담이 필요할 것으로 보인다.
~cpp
Lizard liz1, liz2;
...
liz1 = liz2; // dynamic_cast가 필요로 하지 않다. 이건 반드시 옳다.
우리는 Lizard에 좀더 적당한 할당 연산자를 더해서 dynamic_cast로 인해 발행하는 비용과 복잡성을 제가 할수 있다.
~cpp
class Lizard: public Animal {
public:
virtual Lizard& operator=(const Animal& rhs);
Lizard& operator=(const Lizard& rhs); // 더한 부분
...
};
Lizard liz1, liz2;
...
liz1 = liz2; // const Lizard&를 인자로 하는 operator= 호출
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2; // const Ainmal&인자로 가지는 operator= 연산자 호출
const Animal&을 인자로 하는 연산자의 구현은 구현은 간단한다.
~cpp
Lizard& Lizard::operator=(const Animal& rhs)
{
return operator=(dynamic_cast<const Lizard&>(rhs));
}
이 함수는 rhs를 Lizard로 형변환 시킨다. 만약 형변환이 성공된다면 할당 연산자가 성공적으로 호출 될것이다. 반대라면 언급했던 bad_cast 예외가 발생된다.
솔직히, dynamic_cast를 사용해서 실행 시간에 검사하는건 좀 짜증난다. 한가지, 몇몇 컴파일러는 아직 dynamic_cast에 대한 지원이 부족해서, 이를 사용하면, 이론적으로는 이식성이 보장되지만 실제로는 그렇지 않다. 더 중요하게 Lizard와 Chicken의 클라이언트들은 bad_cast 예외에 대한 준비와, 할당을 수행할때의 각 코딩시에 민감하게 처리하지 못한다. 내 경험에 비추어 볼때 많은 프로그래머들이 그런 방법을 취하지 않고 있다. 만약 그들이 하지 않는다면, 할당이 일어나는 수많은 곳에서 정확하지 않은 처리상태로, 명료성을 보장 받을수 없다.
가상 할당 연산자를 이용하는 것 역시 불충분한 상태가 주어진다. 그것은 클라이언트가 문제있는 할당을 하는 것을 방지하는 방법을 찾도록 노력하는데 힘을쓰게 만든다. 만약 그러한 할당이 컴파일 중에 거부된다면, 우리는 잘못이 일어날 것에 대해 걱정할 필요가 없게된다.
가장 쉬운 방법은 Animal 내부의 operator=를 사역(private)로 묶어서 할당 자체를 하지 못하게 만들어 버리는 것이다. 그런 방법은 도마뱀이 도마벰에게, 닭이 닭에게만 할당을 할수 있지만, 부분적이고 Mix-type 할당을 방지한다.
~cpp
class Animal {
private:
Animal& operator=(const Animal& rhs); // 사역(private)이다.
...
};
class Lizard: public Animal {
public:
Lizard& operator=(const Lizard& rhs);
...
};
class Chicken: public Animal {
public:
Chicken& operator=(const Chicken& rhs);
...
};
Lizard liz1, liz2;
...
liz1 = liz2; // 옳바르다.
Chicken chick1, chick2;
...
chick1 = chick2; // 역시 옳바르다.
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &chick1;
...
*pAnimal1 = *pAnimal2; // 에러! Animal::operator=가 사역이라 에러난다.
불행히 Animal은 concrete 클래스이다.(가상이 아니란 소리) 그리고 이러한 접근은 Animal 객체 간에 할당을 못하는 것을 의미한다.
~cpp
Animal animal1, animal2;
...
animal1 = animal2; // 에러! Animal::operator=가 사역(private)이다.
게다가 Lizard와 Chicken에 구현된 할당 연산자도 정확히 구현되기 불가능하다. 왜냐하면, 유도된 클래스에서 할당 연산자는 그들의 기초 클래스의 할당 연산자의 호출을 책임진다.
~cpp
Lizard& Lizard::operator=(const Lizard& rhs)
{
if (this == &rhs) return *this;
Animal::operator=(rhs); // 에러! 사역(private) 인자의 함수를 호출하는 시도를 한다.
// 하지만 Lizard::operator=는 반드시 이 함수를 Animal 부분
// 의 할당을 위해 호출해야 한다.
...
}
이러한 문제를
Animal::operator=를 보호(protected)영역으로 설정해서 해결할수 있다. 하지만 Animal 포인터를 통하여 Lizard와 Chicken객체의 부분적인 할당을 막는 것에 비하여, Animal 객체 간의 할당 문제는 난제이다. 추상 클래스로서 Animal 은 초기화 할수 없다. 그래서 Animal 간의 할당은 허용될 필요가 없다. 물론 새로운 문제를 수반한다. 왜냐하면 우리의 기본적인 디자인에서 이 시스템에서는 Animal을 객체로서 필요가 있어서 지원한 것 이기 때문이다. 이러한 쉬운 방법은 어려운 부분이 둘러싸고 있는 형국이다. 대신에 Animal 을 추상화 시키는 클래스를 새로 만들어 버리는 건 어떨까?
AbstractAnimal 같이 말이다. 대신에 이제 이들을 Animal, Lizard, Chicken 객체가 상속을 받고 객체로서 활용 되는 것이다. 그렇게 되면 우리는
AbstractAnimal 이라는 추상 클래스에서 Concrete 클래스를 유도한다. 이러한 계층도를 준비하면 다음과 같다.
그리고 다음과 같이 정의 된다.
~cpp
class AbstractAnimal {
protected:
AbstractAnimal& operator=(const AbstractAnimal& rhs);
public:
virtual ~AbstractAnimal() = 0; // 이에 관해서는 설명에 자세히
...
};
class Animal: public AbstractAnimal {
public:
Animal& operator=(const Animal& rhs);
...
};
class Lizard: public AbstractAnimal {
public:
Lizard& operator=(const Lizard& rhs);
...
};
class Chicken: public AbstractAnimal {
public:
Chicken& operator=(const Chicken& rhs);
...
};
이 디자인은 당신이 필요한 모든것을 제공한다. 동종의 할당에 관해서
동물,
도마뱀,
닭의 할당 연산을 허용한다.;부분적인 할당과 타종간의 할당을 금지한다는 것;그리고 유도된 클래스의 할당은 아마 기본 클레스 내의 할당 연산자가 맡을 것이다. 게다가 Animal, Lizard, Chicken클래스의 역할이 기록된 코드들을 수정을 필요로 하지 않는다. 왜냐하면, 이들 클래스는 소계된
AbstractAnimal 클래스로 기존의 역할들을 대신 받을수 있다. 물론, 그러한 코드들은 재 컴파일이 되어야 한다. 그렇지만 컴파일러가 할당의 명시성을 보장해 주어서 보장되는 보안성에 비하여 작은 비용이다.
모든 일에 대하여
AbstractTnimal은 반드시 추상적이어야 하나? 그것은 반드시 최소 하나의 순수 가상 함수를 가지고 있어야 한다. 대부분의 경우에 알맞는 함수를 고르기에는 별 문제가 없다. 그렇지만 희귀한 경우에 당신은 아마
AbstractAnimal 같은 클래스를 만들어야 하는 상황에 직면할지도 모른다. 그러한 경우에, 적당한 방법은 순수 가상 함수로 파괴자를 만들어 버리는 것이다.;위에서 보는것과 같이 말이다.포인터를 통한 다형성을 지원하기 위하여 기본 클래스는 가상 파괴자를 어떤 방법으로든 필요로 한다. 그래서 순수 가상함수를 만들기 위한 비용만이 해당 클래스 정의부 바깥에서 이루어질 구현에서 지불된다. (에를들어서 p195를 보아라)
(순수 가상 함수에 대한 구현의 개념이 당신을 혼란스럽게 한다면, 그거에 신경을 끊으면 된다. 순수 가상 함수의 선언은 구현이 없는것을 의미 하지만은 않다. 그것은 의미한다.
- 현재의 클래스를 추상화 한다. 그리고
- 현재 클래스에서 유도된 어떠한 concrete 클래스라도 반드시 "보통" 가상 함수로 함수를 선언해야 한다. (다시 말해 "=0"을 제외하고)
맞다, 대부분의 순수 가상 함수는 결코 구현되지 않는다. 그렇지만 순수 가상 파괴자는 특별한 경우이다. 그들은
반드시 구현되어야 한다. 왜냐하면 그들은 유도된 클래스들이 파괴될때 마다 불리기 때문이다. 게다가 그들은 종종 유용한 작동을 하는데, 자원의 해제 같은거(Item 9참고)나 로그 메세지를 남기는것 따위 말이다. 순수 가상 함수의 구현은 일반적으로 아마 특별하지 않은 경우이다. 하지만 순수 가상 파괴자는 그렇지 하다 그것은 명령에 가깝다.)
당신은 아마도 데이터 멤버를 가지는 Animal 클래스 같이, Concrete 기초 클래스를 기반으로 전체하고 기초 클래스의 포인터를 통해서 할당에 대한 논의라는걸 주목할 것이다. 그렇다면, 만약 아무런 데이터가 없다면, 의도에 부합하는, 문제가 안될것이 없고, 좀더 생각해 보면, 그것은 자료가 없는 concrete 클래스가 두번 상속 되어도 안전할꺼라고 의견을 펼지 모른다.
두가지 경우에 한가지는 당신의 데이터가 없는 concrete로 적용한다.:이건 미래에 데이터를 가질지도, 안가질지도 모른다. 만약 미래에 데이터를 가진다면, 당신이 하는 모든 것은 데이터 멤버가 추가도리때까지 문제를 미루어 두는 것이다. 이런 경우 당신은 잠깐의 편함과 오랜 시간의 고뇌를 맞바꾸는 것이다. (Item 32참고) 대안으로, 만약 기초 클래스가 정말 어떠한 데이터도 가지고 있지 않다면, 처음에 추상화 클래스와 아주 비슷한 이야기가 된다. concrete 기본 클래스는 데이터 없이 사용되는건 무엇인가?
AbstractAnimal 같은 추상 기본 클래스를 Animal 같은 concrete 기본 클래스로의 교체는 operator= 의 동작을 도 쉽게 이해할수 있는 장점을 가져다 준다. 또한 당신이 배열을 다형하게 다루는 기회 역시 줄여준다.(배열과 클래스에 대한 관계는 Item 3을 참고하라.) 그렇지만, 기술적으로 가장 두드러지는 이점은 디자인 레벨에서 나타난다. 왜냐하면 당신은 유용한 추상의 존제를 명시적으로 인지 할수 있기 때문이다. 그것은 유용한 개념의 존재를 의식하지 않을지라도, 당신으로 하여금 새로운 유용한 개념을 위한 추상 클래스 생성하게 한다.
만약 당신이 C1,C2 두개의 concrete 클래스를 가지고 있고, C2는 C1을 public 상속 했을때 당신은 아마도 두 클래스의 관계를 A라는 새로운 클래스를 생성해서, 다시 구조를 재편하는 과정을 부담없이 할수 있다.
이러한 변환 과정에서 처음의 값은 추상 추상 클래스 A 를 확인하게 만든다. C1과 C2는 아마 보통 몇가지를 가지고 있다.:그것은 그들이 public 상속이 되는 이유이다. 이 변환으로 당신은 반드시 그 가지고있는 어떻것을 확인해야 한다. 게다가 C++에서 클래스로 모호한 부분에 대하여 명확하게 해주어야 한다. 그것은 보통 추상화(abstraction)가 추구해야 하는 것이고 잘 정의된 멤버 함수와 확실한 문법으로 구현된다.
이 모든것이 어떤 잘못된 생각으로 인도한다. 결국, 모든 클래스는 어떠한 종류의 추상화를 표현한다. 그래서 우리는 계층 관계에서 모든 개념을 위해서 두가지의 클래스를 생성할수가 없게 되지 않을가? 하나는 추상화로(추상화를 표현하는 부분 작성) 하나는 concrete로(객체 생성 부분 작성)? 아니다. 만약 당신이 그렇게 하면 당신은 굉장히 많은 클래스로 계층 관계를 만들 것이다. 그리고 컴파일에도 많은 비용을 소요한다. 그것은 객체 지향 디자인의 잘못된 목표이다.
그 목표는 유용한 추상화와 추상화를 추상 클래스로 존재하게 강제해 버리 도록 구분한다. 그렇지만 당신은 어떻게 유용한 추상화를 분간 할것인가? 누가 그 추상화가 미래에도 유용한지 알려주는가? 누가 어디로 부터 상속되는지 예상할수 있는가?
자, 나는 어떻게 미래에 상속 관계에 대한 사용을 예측할수 없다. 그러나 나는 이거 하나는 알고 있다.:하나의 목적에서 추상화에 대한 필요는 비슷할 것이라는것, 그러나 더 많은 목적으로 추상화에 대한 필요성은 보통 중요하다. 즉, 유용한 추상화는 하나 이상의 용도(목적)에서 필요성이 있다. 그것은 그렇나 추상화가 그들에 목적에 부합하는 클래스와 올바르게 쓰인다는 것과, 유도된 클래스에서도 또한 유용하다는 걸 의미한다. (작성자주:의역)
이것은 정확하게 concrete 기본 클래스가 유용한 추상 기본 클래스로 변환되는 이유가 된다.:그것은 오직 존재하는 concrete 클래스가 기본 클래스 같을때, 다시 말하면 클래스가 새로운 목적으로 재사용 될때, 새로운 추상 클래스의 도입을 강요한다. 그러한 추상화는 유용하다. 왜냐하면 그들 자신이 보이는 것과 같이 구현될 필요성을 가지고 있기 때문이다.
처음에 요구되는 개념은, 우리는 추상 클래스(개념을 위한)와 concrete 클래스(객체가 개념에 반응하기 위한) 양쪽다 정당화 시킬수 없다. 하지만 두번째로 필요한 개념은, 우리가 이 두 클래스의 생성을 정당화 할수 있다. 내가 간단하게 언급한 변환은 이러한 과정(process)를 공정화(mechanize) 하는 것이다. 그리고 비록 디자이너와 프로그래머들이 유용한 개념을 항상 의식을 가지고 생각하지 않을지라도, 그들에게 생각하는 유용한 추상화 과정을 명시적으로 보이도록 강제한다.
믿음직한 예제를 생각해 보자. 당신이 네트웍 상에서 어떤 프로토콜을 이용해서 정보를 패킷 단위로 나누어 컴퓨터 사이에 이동시키는 어플리케이션을 작성한다고 하자.(모호 그래서 생략:by breaking it into packets) packet을 표현하는 클래스나 클래스들에 관하여 생각해야 할것이다. 그러한 클래스들은 이 어플리케이션을 대하여 잘알고 있어야 한다고 전제 할것이다.
일단, 오직 한종류의 프로토콜을 통하여 오직 한종류의 패킷만을 전송한다고 가정하자. 아마도 다른 프로토콜과 패킷의 존재를 알고 있을 것이다. 그렇지만 당신은 그들을 지원하지 않고, 미래에도 이들을 지원할 계획이 없을 것이다. 당신은 패킷에 대한 추상 클래스(패킷을 표현하는 개념을 위한)를 사용할 패킷의 concrete 클래스와 같이 만들것인가? 만약 그렇게 한다면 당신이 패킷의 기본 클래스의 변화 없이 차후에 새로운 패킷 형태를 추가하는 것을 바랄수 있다. 그것은 새로운 형태의 패킷이 추가될경우 패킷과 관련하여 재컴파일할 수고를 덜어 줄것이다. 그렇지만 그런 디자인은 두개의 클래스를 요구한다. 그리고 지금 당신에게는 하나만이 필요하다.(당신이 사용하는 패킷만을 표현하는 클래스) 없을지도 모르는 미래를 위한 확장을 위해서 당신의 디자인을 복잡하게 하는 것을 해야 할까?
여기에는 정확한 답을 내릴수 없다. 그렇지만 경험상으로 그것은 우리가 완전히 이해하기 힘든 개념을 잘 구현한 훌륭한 클래스의 디자인에는 결코 가까워 질수 없을 것으로 보인다. 만약 당신이 패킷을 위해서 추상 클래스를 만들었다면, 오직 단일 패킷 형태로 제한하는 디자인 이후에 어떻게 옳바른 디자인을 할수 있겠는가? 기억해 봐라, 만약 당신이 추상 클래스로 디자인해서 미래에 이를 상속한 클래스들로 디자인상 별 변화 없이 제작될수 있다는 면, 이런 추상 클래스가 주는 장점을 얻는다. (만약 변화가 필요하다면 모든 클라이언트에게 재 컴파일을 요구해야 한다. 그리고 아무것도 얻지 못한다.)
당신이 하려는 훌륭한 추상 패킷 클래스 디자인은 당신이 다양한 목적에 수많은 다른 패킷을 훌륭하게 만들어 보지 않고서는 할수 없다. 이번 경우에서 이런 제한된 경험을 제시하는 것은 나의 충고가 패킷에 대한 정의가 아니라, 추후 오직 concrete 패킷 클래스로 부터 상속의 필요성이 있을때에, 패킷의 추가를 용이하게 하기 위한 것이다.
내가 여기에 제시한 변환은 추상 클래스의 필요성을 확인하기 위한 하나의 방법이지 유일한 방법은 아니다. 추상 클래스의 지원이 요구되는 수많은 경우들이 있다.;객체 지향에 분석은 책들을 만들 만큼 다양하다. 추상 클래스에 관한 소개는 이 경우 만이 아니라 자신 스스로 다른 concrete 클래스에 대한 상속 관계를 설계하면서 깨달아라. 그렇지만, 보통 두개의 concrete 클래스 public 상속으로 연결 지어 놓는것은 새로운 추상 클래스의 필요성을 의미한다.
종종 다음의 문제로, 심사 숙고해서 만들어논 평화로운 이론을 가혹한 현실이 망친다. 서드 파티 C++ 라이브러리는 폭팔적으로 증가하고, 당신은 읽을수 밖에 없는 라이브러리 상의 concrete클래스로 부터 상속받은 concrete 클래스의 생성을 원할때 어떻게 할것인가?
당신은 새로운 추상 클래스를 넣기위해, 라이브러리를 수정할수 없다. 그래서 당신의 선택은 제한과 별로 매력적이지 않는 방법일 것이다.
- 당신의 concrete 클래스를 존재하고 있는 concrete 클래스로 부터 유도하라 그리고 할당에 관련한(assignment-related) 문제들인, 이 Item의 시작 부분에 대하여 시험해 봐라. 또한 Item 3에 언급된 배열에 관한 문제가 있는지도 점검해 봐야만 한다.
- 당신이 필요로 하는 것에 가장 가까운 추상 클래스를 상속 계층 높은 부분에서 찾아봐라, 그리고 나서 그 클래스에서 상속하라. 물론 정확하지 않은 클래스 일지도 모른다. 그렇더라도, 아마 당신은 확장하고자 하는 기능을 가지는 concrete 클래스의 구현의 노력 해야 할것이다.
- 당신의 새로운 클래스를 당신이 상속 받고자 하는것과 비슷한 클래스 라이브리의 한부분에 구현해라. 예를 들어서 당신이 데이터 멤버로서 라이브러리 클래스의 객체를 가지고 싶을때, 당신의 새로운 클래스에 라이브러리 클래스의 인터페이스를 재정의 해라.
~cpp
class Window { // 이것은 클래스 라이브러리
public:
virtual void resize(int newWidth, int newHeight);
virtual void repaint() const;
int width() const;
int height() const;
};
class SpecialWindow { // 이것은 Window로 부터 상속되기 원하는 클래스
public:
...
// 비 가상 함수는 넘기는 것으로 구현한다.
int width() const { return w.width(); }
int height() const { return w.height(); }
// "상속 받은" 가상 함수의 새로운 구현
virtual void resize(int newWidth, int newHeight);
virtual void repaint() const;
private:
Window w;
};
이러한 전략은 당신이 의존하고 있는 라이브러리 벤더의 클래스가 업데이트 될때 마다 당신의 클래스를 업데이트를 할 준비가 되어 있는걸 요구한다. 또한 라이브러리 클래스상에서 가상 함수의 재정의 능력을 제거를 요구하기도 한다. 왜냐하면 당신은 상속 받기 전까지 가상 함수를 재정의 할수가 없다.
- 당신이 가진 것을 이용해 만들어라. 라이브러리에서 concrete 클래스의 사용하고, 당신의 소프트웨어에서 수정하라 그러면 , 그 클래스는 충분하다. 당신이 클래스에 더하고자 하는 기능을 제공하는 non-member 함수를 작성하라. 하지만 할수 없다. 그와 같이 하면 소프트웨어의 결과는 명료하지 못하고, 효율적이지 못하고, 유지 보스하기 힘들고, 확장하기 힘들다. 하지만 최소한 그런 일을 할수 있게 하라.
이러한 선택은 특별히 매력적이지는 않다 왜냐하면 당신은 어떤 공학적 판단에 적용해야 하고, 최대한 모호하지 않도록 하는 독을 선택해야 한다. 그리 재미있는건 아니다. 그러나 삶은 때로 그와 같다. 미래의 당신 자신을 위해서 더 쉽게 만들어라, 당신이 원하는 디자인의 라이브러리의 벤더들에게 불평해라.
아직 일반적인 규칙이 남아 있다.:non-leaf 클래스가 추상화 되어야 한다. 당신은 아마도 외부의 라이브러리를 사용할때, 묶어 줄만한 규칙이 필요할 것이다. 하지만 우리가 다루어야 하는 코드에서, 외부 라이브러리와 가까워 진다는 것은 신뢰성, 내구성, 이해력, 확장성에서 것과 떨어지는 것을 야기한다.