[디자인 패턴] #11_타입 객체, Type Object
게임 디자인 패턴 중 "타입 객체"에 대해 알아보겠습니다.
"게임 프로그래밍 패턴"의 13 항목, "타입 객체"에 해당하는 내용입니다.
개념
1. 타입 객체
클래스 하나를 인스턴스 별로 다른 객체형으로 표현하며, 앞으로 추가할 새로운 클래스들을 유연하게 만들 수 있게 합니다.
2. 왜 필요한가?
#include <string>
using namespace std;
class Monster
{
public:
virtual ~Monster();
virtual string getAttack() = 0;
protected:
Monster(int initialHealth):health(initialHealth){}
private:
int health;
};
class Dragon : public Monster
{
public:
Dragon():Monster(200){}
virtual string getAttack()
{
return "용이 불을 뿜습니다!";
}
};
class Troll : public Monster
{
public:
Troll() :Monster(200) {}
virtual string getAttack()
{
return "트롤이 몽둥이로 후려칩니다!";
}
};
* 전형적인 객체지향화
대부분의 적 객체는 앞으로 "Monster" 클래스를 상속받게 됩니다. "Monster" 클래스는 인터페이스 클래스로 하위 클래스들은 "Monster" 클래스의 생성자를 호출하고, 추상 메서드를 각각의 클래스의 특성에 맞게 오버 라이딩합니다. 하지만! 머지않아 새로운 몬스터들이 추가되어 "Monster" 클래스를 상속받는 하위 클래스들이 수도 없이 많아지겠죠!! 이때 "타입 객체" 패턴을 활용할 수 있습니다.
디테일
1. 타입 사용 객체 클래스 + 타입 객체 클래스
1. 타입 사용 객체 클래스 + 타입 객체 클래스
게임의 몬스터들을 "Monster" 클래스로 묶는것과 더불어 "Breed(종족)"클래스로 한번 더 묶어보겠습니다. 하지만, 우리는 Monster -> Breed -> Dragon 순서의 상속 계층을 형성하기보다, "Monster" < -> "Breed"로 Dragon 몬스터를 "Monster" 클래스로부터 생성된 인스턴스들 중 한 개로 생각해보죠! 정리하자면, Monster 클래스의 인스턴스들은 "Dragon", "Troll", "Skull" 등 각각 다른 특성을 갖고 있으며, 공통된 특성들은 "Breed"라는 타입 객체 클래스로부터 가져오게 되는겁니다!
2. 타입 사용 객체 클래스
여기서, 타입 사용 객체 클래스는 "Monster" 클래스를 의미하며 "Monster" 클래스의 인스턴스들은 각각의 타입을 가지며, 이 "타입"은 타입 객체 클래스로 결정됩니다.
3. 타입 객체 클래스
"Breed" 클래스는 타입 객체 클래스로 타입 사용 객체 클래스의 인스턴스들의 "타입"을 결정합니다!
* 글만으로 이루 설명하기 어렵지만, 최대한 자세한 예시를 통해 알아보겠습니다.
2. 언제 사용하는가?
1. 나중에 어떤 타입이 필요할지 알 수 없다.
2. 컴파일 혹은 코드 수정 없이 새로운 타입을 추가하고 싶다.
3. 주의 사항
1. 타입 객체를 직접 관리해야 한다.
타입 객체 패턴의 요지는 수많은 하위 클래스를 생성하는 상속 계층을 "타입 객체 클래스"로 대체하는 것입니다. "Monster" 클래스의 인스턴스들을 생성할 때 알맞은 타입 객체 클래스의 참조형으로 초기화해주는 것부터 우리가 해야 할 일입니다...
2. "타입" 별 동작을 표현하기가 어렵다.
타입 객체 클래스를 통해 데이터를 정의하기 쉽지만, 동작을 정의하는 것은 어렵죠. 우리는 이때 몇 가지 동작들을 정의하는 함수들을 준비하고, 각 타입에 알맞은 동작 함수들을 함수 포인터로 저장하도록 하는 것이 가장 간단한 방법입니다.
예제 코드
1. 타입 사용 객체 클래스 + 타입 객체 클래스 정의
class Breed
{
public:
Breed(int _health, int _power /*...*/)
: health(_health), power(_power) //...
{}
Monster* createNewMonster()
{
return new Monster(*this);
}
int getHealth() { return health; }
int getPower() { return power; }
//...
private:
int health;
int power;
//...
};
class Monster
{
friend class Breed;
public:
Monster(Breed& _breed)
: breed(_breed), health(breed.getHealth()) //...
{}
private:
Breed& breed;
int health;
int power;
//...
};
// main 문...
int main()
{
Breed DragonBreed( 300, 150, ... );
// 1. 팩토리 메서드 패턴을 활용한 생성자 함수 호출
Monster* FireDragon = DragonBreed.createNewMonster();
// 2. 일반적인 동적 할당을 통한 객체 생성
Monster* FireDragon = new Monster(DragonBreed);
}
1. 타입 객체 클래스, "Breed" 클래스
간단합니다. 특정 종족을 구현하기 위해 "Breed" 클래스의 생성자의 해당 종족이 갖는 스펙을 넘겨 생성합니다.
눈여겨볼 점은 "createNewMonster" 메서드입니다. 이 메서드는 팩토리 메서드 패턴을 활용한 생성자 호출 함수입니다. 자세한 내용은 밑에서 알아보겠습니다.
2. 타입 사용 객체 클래스, "Monster" 클래스
타입 사용 객체 클래스 또한 간단합니다. 생성자의 인자로 특정 종족 객체를 넘겨받아 스펙을 설정합니다.
2. 팩토리 메서드 패턴*, 생성자 호출 함수
// 1. 생성자 호출
Monster* monster = new Monster(DragonBreed);
Monster* monster = DragonBreed.createNewMonster();
1. 2단계 초기화
Breed 객체를 전달받아 초기화를 진행하는 방법은 "메모리"를 할당한 다음에 진행됩니다. 아직 제대로 초기화되지 않은 Monster 객체가 메모리에 먼저 올라가 있는 것입니다. 쉽게 말해, Monster 클래스의 데이터 멤버들은(health, power, etc) Breed 객체의 참조형이 전달된 후에 초기화할 수 있습니다. 따라서, 완전한 초기화 작업은 메모리 할당 이 완료된 후에 진행되겠죠.
2. 팩토리 메서드 패턴의 "생성자"
2번 코드처럼 팩토리 메서드 패턴을 활용하면 Monster 객체에게 초기화 제어권을 넘겨주기 전에 힙에서 메모리를 가져올 수 있습니다. 정리하자면, Breed 클래스 안에서 Monster 객체를 생성할 수 있도록 하여, 모든 몬스터가 정해놓은 메모리 관리 루틴을 따라 생성되도록 강제할 수 있습니다!
타입 객체의 노출
class Monster
{
//...
public:
Breed& getBreed() { return breed; }
};
1. 타입 객체의 참조를 캡슐화
1. 타입 객체 패턴의 복잡성이 외부 코드에 노출되지 않습니다.
2. 타입 사용 객체에서 타입 객체로부터 동작을 선택적으로 오버라이드 할 수 있습니다!
2. 타입 클래스의 멤버 메서드를 전부 포워딩해야 한다
2. 타입 객체의 노출
1. 비교적 간단하게 외부에서 타입 객체에 접근할 수 있습니다.
2. 타입 객체가 API의 일부가 됩니다.
'언어 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴]#13_이벤트 큐, Event Queue (0) | 2022.10.02 |
---|---|
[디자인 패턴]#12_Component Pattern, 컴포넌트 패턴 (0) | 2022.09.25 |
[디자인 패턴]#10_Sandbox Pattern, 샌드박스 패턴 (0) | 2022.09.12 |
[디자인 패턴]#9_업데이트 패턴, Update Pattern (0) | 2022.09.04 |
[디자인 패턴]#8_게임 루프, Game Loop Pattern (0) | 2022.08.28 |