[디자인 패턴] #10_Sandbox Pattern, 샌드박스 패턴
게임 디자인 패턴 중 "행동 패턴"에 대해 알아보겠습니다.
"게임 프로그래밍 패턴"의 12 항목, "하위 클래스 샌드박스"에 해당하는 내용입니다.
샌드박스 패턴
1. 정의
상위 클래스가 제공하는 기능들을 통해서 하위 클래스에서 행동을 정의한다.
2. 상세 내용
* 상위 클래스가 제공하는 인터페이스 없이, 하위 클래스에서 각각의 행동을 정의할 경우?
1. 중복 코드의 문제점
2. 대부분의 외부 코드와 커플링 된다.
3. Too Much Coupling 문제로 외부에서 코드를 변경할 경우, 클래스가 깨진다.
4. 모든 하위 클래스가 공통적으로 공유할 불변식을 정의하기 어렵다.
* 기본적인 구현 방법?
원시 명령을 상위 클래스의 "protected" 메서드로 정의하고, 이를 상속하는 하위 클래스에서 쉽게 접근할 수 있도록 합니다. 더불어, 원시 명령들을 사용할 공간을 제공하기 위해서 하위 클래스들이 각각의 특성에 맞게끔 "샌드박스 메서드"를 순수 가상 메서드로 만들어 "protected" 영역에 함께 둡니다. 정리하자면 아래와 같습니다!
1. 최 상위 클래스에서 "샌드박스 메서드"와 함께 원시 명령들을 protected 영역에 정의합니다.
2. 하위 클래스에서 샌드박스 메서드(eg, Activate() 메서드)를 오버라이드, 재정의 합니다.
3. 상위 클래스가 제공하는 protected 영역의 메서드들을 호출하여 샌드박스 메서드를 구현합니다.
* 커플링 문제를 최 상위 클래스에 몰아주고, 상속 구조가 얇게 퍼지도록 합니다!
3. 언제 사용할까?
1. 한 개의 클래스에 상대적으로 많은 하위 클래스가 존재할 때
2. 하위 클래스가 필요한 기능을 상위 클래스에서 모두 제공할 수 있을 때
3. 하위 클래스들 간의 중복되는 행동들이 존재할 때
4. 하위 클래스들 간의 커플링, 혹은 외부 코드들과의 커플링을 최소화할 때
예제 코드
1. 상위 클래스
class SuperClass
{
public:
virtual ~SuperClass();
protected:
virtual void Activate() = 0;
void move(double x, double y, double z);
void playSound(SoundID soundId);
void spawnParticles(ParticleSystem particle);
};
class SubClass : public SuperClass
{
protected:
virtual void Activate()
{
move(3, 4, 5);
playSound(SOUND_34);
spawnParticles(PARTICLE_20);
}
}
1. Activate() 순수 가상 메서드를 "샌드박스 메서드"로 활용, 오버라이드가 필수적!
2. 그 외 원시 명령들을 비 가상 멤버 메서드로 선언합니다.
3. 하위 클래스는 "샌드박스 메서드" 내에 원시명령들 호출합니다.
디자인 설계
1. 어떤 기능을 제공해야 하나?
1. 상위 클래스가 제공하는 기능들을 몇 안 되는 하위 클래스에서만 사용하면 의미가 없다.
2. 상태를 변경하지 않는 외부 시스템의 함수는 안전한 커플링이다.
3. 상위 클래스 제공 기능이 단순히 외부 시스템으로 호출만 넘겨준다면, 큰 의미가 없다.
2. 직접 제공할 것인가? 이를 담고 있는 객체를 통해서 제공할 것인가?
* 샌드박스 패턴의 문제점?
샌드박스 패턴의 큰 문제는 상위 클래스의 비 가상 멤버 메서드(제공 메서드)가 끊임없이 늘어난다는 점입니다.
이 메서드들 중 일부를 또 다른 클래스에 옮겨 이 문제를 완화할 수 있습니다.
* 객체에 담아서 제공하는 방법?
class SuperPower { public: virtual ~SuperPower(); protected: virtual void Activate() = 0; //... // 너무 많아지는 메서드들... void playSound(SoundId soundId, double volume); void stopSound(SoundId soundId); void setVolume(SoundId soundId); }; ***************** 객체에 담아서 제공 class SoundPlayer { public: void playSound(SoundId soundId, double volume); void stopSound(SoundId soundId); void setVolume(SoundId, soundId); }; class SuperPower { public: virtual ~SuperPower(); protected: //... SoundPlayer& getSoundPlayer() { return soundPlayer; } private: SoundPlayer soundPlayer; };
1. 상위 클래스에서 제공하는 원시 명령 메서드의 개수를 줄일 수 있습니다
2. 보조 클래스에 있는 코드를 유지보수하는 것이 더 간단합니다.
3. 상위 클래스와 외부 시스템 간의 커플링을 줄입니다.
* 그렇다면, 필요한 객체를 어떻게 얻어올 것인가?
1. 초기화 리스트 활용
class SuperClass { public: SuperClass(ParticleSystem* _particles) : particles(_particles) {} //... private: ParticleSystem* particles; };
가장 간단한 방법은 상위 클래스의 생성 시점에 "초기화 리스트"를 활용해 보조 클래스의 객체를 생성하는 방법입니다. 하지만, 하위 클래스 생성자에서 상위 클래스에 인수를 전달해야 하기 때문에, 상위 클래스의 멤버가 쉽게 노출될 위험이 존재합니다.
2. 2단계 초기화 활용class SuperClass { public: virtual ~SuperClass(); //... SuperClass* Init(ParticleSystem* _particles) { particles = _particles; } private: ParticleSystem* particles; }; class SubClass : public SuperClass { //... }; int main() { SuperClass* mySub = new SubClass(); ParticleSystem* particle = new ParticleSystem(); mySub->Init(particle); }
위와 같은 캡슐화 문제를 해결하기위해 우리는 2단계 초기화 방법을 사용합니다. 상위 클래스 생성자의 초기화 리스트를 활용하기보다, 새로운 객체를 동적 할당 받아서 반환하는 "Init()" 멤버 메서드를 통해 보조 클래스의 객체를 초기화합니다!
3. 정적 객체로 만들기, 싱글턴 패턴
class SuperClass { public: virtual ~SuperClass(); //... static void Init(ParticleSystem* _particles) { particles = _particles; } private: static ParticleSystem* particles; };
마지막으로, 보조 클래스의 기능을 모든 하위 클래스에서 공통적으로 사용한다면, 앞서 공부했던 "싱글턴 패턴"을 활용해 볼 수 있습니다! 보조 클래스의 객체를 정적 객체로 만들어 상위 클래스의 멤버로 선언합니다. 결과적으로 SuperClass::Init() 을 한 번 호출해놓으면 모든 SuperClass 인스턴스에서 같은 보조 클래스에 접근할 수 있습니다!
* 싱클턴 패턴의 자세한 내용은 아래 링크를 참조해주세요!
'언어 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴]#12_Component Pattern, 컴포넌트 패턴 (0) | 2022.09.25 |
---|---|
[디자인 패턴]#11_타입 객체, Type Object (0) | 2022.09.19 |
[디자인 패턴]#9_업데이트 패턴, Update Pattern (0) | 2022.09.04 |
[디자인 패턴]#8_게임 루프, Game Loop Pattern (0) | 2022.08.28 |
[디자인 패턴]#7_이중 버퍼, Double Buffer (0) | 2022.08.21 |