[Effective C++] #35 Public 가상 함수의 대안, NVI, 전략 패턴
Scott Meyers의 "Effective C++"를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 6장 "상속, 그리고 객체 지향 설계", 항목 35 "가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자"에 해당하는 내용입니다.
가상 함수
class someClass
{
public:
virtual int virtualFunc() const;
...
};
가상 함수의 선언은 파생 클래스로 하여금 인터페이스와 기본 구현을 제공받도록 하고, 각 파생 클래스의 필요에 의해 재정의 할 수 있도록 해줍니다. 멤버 함수들의 인터페이스 상속과 구현 상속 관련 내용은 링크를 참조하세요.
가상 함수의 대안
이번 항목은 가상 함수 대신에 사용할 만한 객체 지향 설계 방법에 대해서 알아 보겠습니다. 이 방법들은 크게 두 가지로 나눌 수 있습니다. 먼저, 가상 함수를 private 영역에 선언하고, 이 가상 함수의 "래퍼"함수를 public 영역에 두는 방법이 있습니다. 이 방법을 NVI(Non-virtual interface)라고 합니다. 다른 하나는 "전략 패턴"이라고 하는 디자인 패턴의 의 적용입니다. 구체적으로 함수 포인터 사용, tr1::function, 그리고 전통적인 전략 패턴 적용 등이 있습니다.
NVI, 비 가상 함수 인터페이스 관용구
class GameCharacter
{
public:
virtual int healthValue () const; // 체력 게이지 관련 함수
...
};
class NewGameCharacter
{
public:
int healthValue () const
{
... // 사전 동작
int retVal = doHealthValue();
... // 사후 동작
return retVal;
}
private:
virtual int doHealthValue() const
{
// 체력 게이지 관련 알고리즘 정의
}
}
위 예제 코드는 멤버 함수를 public 영역에 가상 함수로 두는 대신, private 영역에 두고, 해당 함수를 본문에서 호출하는 비 가상 멤버 함수를 public에 두는 방법을 보여줍니다. 간단하게 말하자면, public 비 가상 멤버 함수를 통해 private 가상 함수를 간접적으로 호출하는 방법입니다. 이러한 설계 방식을 NVI(비 가상 함수 인터페이스)라고 합니다. NVI 설계 방식의 이점은 가상 함수의 호출 이전과 이후로 부가적인 동작등을 설계할 수 있다는 점입니다. 래퍼 함수 본문에 가상 함수 호출 이전에 작성한 코드와 이후에 작성한 코드들을 통해 이러한 설계 방식이 가능해집니다. 이때, 파생 클래스는 부모 클래스의 private 가상 멤버 함수의 재정의가 가능할까?라는 의문점이 생깁니다. 대답은 "가능하다"입니다. 정리하자면, 가상 함수의 구현 방법을 결정하는 것은 파생 클래스가 되겠지만, 어느 시점에 호출할지 결정하는 것은 기본 클래스의 고유 권한이 되는 것입니다.
전략 패턴 #1 함수 포인터 사용
int defaultHealthCalc (const NewGameCharacter& gc); // 비 멤버 체력 게이지 계산 함수
class NewGameCharacter
{
public:
typedef int (*HealthCalcFunc) (const NewGameCharacter&);
explicit NewGameCharacter (HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc (hcf)
{}
int healthValue () const { return healthFunc (*this);}
...
private:
HealthCalcFunc healthFunc;
}
첫 번재 전략 패턴 적용 방법은 함수 포인터를 클래스의 생성자로 넘겨 호출하는 방법입니다. 구체적으로, 체력 게이지를 계산하는 비 멤버 함수의 포인터를 캐릭터 클래스의 private 멤버로 선언하고, 캐릭터 객체를 생성하는 동시에 체력 계산 함수 포인터를 넘겨받는 방법입니다. 넘겨받은 체력 계산 함수 포인터는 클래스 내부에 별도로 선언한 "helathValue" 멤버 함수의 본문에서 호출해줍니다. 이 방법은 두 가지 이점을 갖고 있습니다. 하나는 파생 클래스의 특성에 맞게 비 가상 함수를 생성하여 호출하는 것이 가능해집니다. 마치 기본 클래스의 가상 멤버 함수를 상속받아 재정의 하는 것과 비슷하죠. 다른 하나는 다양한 체력 계산 함수들을 정의하고, 게임이 실행되는 중에도 캐릭터 클래스 내부에 "setHealthCalcFunc" 멤버 함수를 두어 상황에 맞게 교체하는 것도 가능해집니다. 반면에, 이러한 설계 방식은 단점도 존재합니다. 클래스 외부에 정의된 비 멤버 함수인만큼, 클래스의 private 멤버에 접근할 수 없습니다. 예를 들면, 체력 게이지를 계산하기 위해 특정 캐릭터의 private 멤버들의 정보들이 필요 할 수도 있기 때문에, 상황에 맞게 이 방법을 사용해야겠죠.
전략 패턴 #2 std::tr1::function 사용
int defaultHealthCalc (const NewGameCharacter& gc);
class NewGameCharacter
{
public:
typedef std::tr1::function <int (const NewGameCharacter&)> HealthCalcFunc;
explicit NewGameCharacter (HealthCalcFunc hcf = defaultHelathCalc)
: healthFunc (hcf)
{}
private:
HealthCalcFunc healthFunc;
}
두 번째 전략 패턴 적용 방법은 std::tr1::function의 사용입니다. 자세한 내용을 알아보기 전에, 간략하게 용어 정리를 먼저 해보도록 하겠습니다. "함수의 시그니처"란 무엇일까요? 함수의 시그니처란 컴파일러가 함수를 구분하기 위해 필요한 3가지 구성요소를 의미합니다 - 함수의 이름, 매개 변수, 반환 값. 그렇다면, "함수 호출성 개체"는 무엇일까요? 함수 호출 성 개체의 종류는 다음과 같습니다 - 함수 포인터, 함수 객체, 멤버 함수 포인터... 등. "std::tr1::function"은 어떤 함수가 가진 시그니처를 갖는 함수 호출 성 개체의 표현을 가능하게 해주는 템플릿입니다. 쉽게 말해서, tr1::function 타입으로 만들어진 객체는 해당 객체의 시그니처와 호환되는, 혹은 암시적으로 형 변환이 가능한 어떠한 함수 호출 성 개체도 가질 수 있게 됩니다. 따라서, std::tr1::function의 사용은 클래스가 구체적인 함수 포인터를 물고 있기 보다, 더욱 일반화된 함수 포인터를 물고 있게 해 줍니다. 더욱 융통성을 갖는 코드 작성이 가능해지죠.
전략 패턴 #3 별도의 클래스 내부에 가상 멤버 함수로 설정
class HelathCalcFunc
{
public:
virtual int calc(const NewGameCharacter* gc) const
{...}
};
HealthCalcFunc hcf; // HealthCalcFunc 타입의 객체 생성
class NewGameCharacter
{
public:
explicit NewGameCharacter (HealthCalcFunc* phcf = &hcf)
: pHealthCalcFunc (phcf)
{}
int helathVal () const
{ return pHelathCalcFunc->calc(*this); } //함수 포인터를 통해 HelathCalcFunc 클래스의 calc 멤버 함수 호출
private:
HealthCalcFunc* pHealthCalcFunc;
}
세 번째 방법은 앞선 두 가지 방법보다 직관적입니다. 별도의 체력 계산 클래스를 생성하여, 이 클래스의 포인터를 캐릭터 클래스의 private 멤버로 두는 것입니다.
가상 멤버 함수 대신 NVI 관용구 사용과 전략 패턴 설계 방법을 고려할 수 있습니다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #37 가상 함수, 기본 매개변수 (0) | 2022.02.09 |
---|---|
[Effective C++] #36 비 가상 함수의 상속, 바인딩 (0) | 2022.02.04 |
[Effective C++] #34 사용자 정의 기본 멤버 함수 (0) | 2022.01.31 |
[Effective C++] #23 비 멤버, 비 프렌드 함수 (0) | 2022.01.28 |
[Effective C++] #22 클래스 데이터 멤버, 접근 제어, 접근 지정자 (0) | 2022.01.28 |