728x90

브리지(가교) 패턴은 추상과 구현을 분리하여 각각이 독립적으로 확장될 수 있도록 하는 패턴이다.

 

 

들어가기 앞서, 확실히 해야할 것이 있다. 우리는 "상속"을 할때 보통 2가지 이유로 사용한다.

 

1. 추상적 개념 구현

추상의 구현

이건 용어적으로 JAVA에는 해당하지 않을 수 있다. (interface, implements 키워드가 따로 있기 때문)

어쨌든 그림과 같이 추상적 개념(기능)을 여러 형태로 구현하기 위해 "상속"시킨다.

 

2. 기능 확장

기능 확장

기존 클래스를 활용 및 확장하여 새로운 기능을 사용자에게 제공하기 위해 상속시킨다.


1번 2번 둘다 상속을 이용하지만 1번의 경우 서브 클래스에 특화된 기능을 추가하기 어렵다.

client는 FileSystem으로만 추상적 기능을 이용하기 때문이다.

반대로 2번은 client는 서브 클래스인 TV class만 사용하기 때문에 마음껏 확장 가능하다.

1번과 2번의 차이를 알면 브리지 패턴의 필요성을 잘 알게 된다.


여러분은 게임내 다양한 요소들을 하나의 추상적 개념으로 핸들링 하기 위해 다음과 같은 class를 정의하였다.

그리고나서, 한번 배치되면 움직이지 않는 StaticObject와 물리 엔진에 따라 움직이고,

충돌하는 등의 움직일 수 있는 DynamicObject로 두 형태로 구현하였다.

이때 사용자 입력에 따라 조작이 되는 Object 즉, UI 기능을 하는 Object의 추가 요구사항이 발생하였다.

그래서 UIObject를 추가하였다.

근데 생각해보면, UIObject는 확장 기능이다.

StaticObject가 조작될 수 있고, DynamicObject가 조작될 수 있다.

StaticObject도 DynamicObject도 UIObject가 될 수 있어야 한다.

죽음의 다이아몬드 2개~

이렇게 되니 죽음의 다이아몬드가 된다... UIStaticObject와 UIDynamicObject는 하는 수 없이

StaticObject, DynamicObject를 상속받지 않고 중복 구현하여 피할 수 있긴 하지만 여전히 구리다.

그래서 다음과 같이 변경하였다.

이런 구조를 가지면 좀더 깔끔하게 해결되지만, 문제는 이제 StaticObject와 DynamicObject는

UIObject에 강한 의존을 가지게 되었다... UI기능을 하지 않아도 상속하고 있으니 실제

코드에서 처리가 복잡해질 것이다.

 

앞서 얘기한 추상부 구현과 기능 확장이 상속으로만 해결하려 하여 이러한 문제가 발생하였다.

브리지 패턴은 이것을 분리해주는 패턴이다!

브리지 패턴을 적용하여 재설계된 모습

추상 기능부와 구현부를 분리하여 GameObject와 GameObjectImpl로 나누었다.

UIObject는 GameObject에 UI 조작기능을 확장한 것으로 GameObject를 확장시켰고,

StaticObject와 DynmiacObject는 GameObject 기능의 구체적 구현이므로 GameObjectImpl를 구현하고 있다.

이제 UI 조작이 필요한 GameObject를 다룰때 사용자는 UIObject를 인스턴스화하고,

필드에 있는 _impl를 통하여 StaticObject나 DynamicObject를 GameObjectImpl을 통하여 조작하면 된다.

추상과 구현이 분리되어 있어서 각각 따로 확장할 수 있게 되었다!


브릿지 패턴의 이러한 유연한 구조에도 불구하고 역시 단점은 있다. 

1. 기본적으로 코드 복잡성을 늘린다.

2. 기능 클래스를 통해야 하는 추가 오버헤드

728x90

'프로그래밍 > C++' 카테고리의 다른 글

Visitor Pattern  (0) 2021.08.21
Observer Pattern  (0) 2021.08.13
Prototype Pattern  (0) 2021.07.13
Adaptor Pattern  (0) 2021.06.30
Factory Pattern - Simple Factory  (1) 2021.02.21
728x90

프로토타입 패턴은 생성패턴중 하나로, 인스턴스 생성시 원형이 되는 인스턴스로부터

새로운 인스턴스를 만드는 패턴이다.

원형이 되는 나루토로부터 분신을 만든다.

우리가 코딩할때 stack overflow나 블로그를 뒤져가면서 관련 코드를 찾고, 그대로 따라서 타이핑 해도 되지만,
바로 복사해서 쓰듯(ㅋㅋ), 객체의 생성때마다 매번 재생성 하고 관련 파라미터를 세팅해주는 것이 아니라,
이미 세팅된 애를 복사해서 사용할때 유용하다.

이미 세팅된 애를 복사해서 쓰기 때문에, 객체 생성과정이 매우 복잡 or 비용이 크다면 ( DB 에서 참조, File I/O 등 )
해당 비용을 줄일 수 있다!

또, 팩토리 패턴처럼 별도의 creator를 만들지 않아도 되니 서브클래스 숫자가 줄어드는 장점도 있다.


여러분은 npc를 만들고 배치 시키는 기능을 지원하기 위한 사전 작업을 하기로 했다.

오픈월드 게임을 해본 분들은 아시겠지만, 메쉬만 다르고 똑같은 액션 똑같은 행동들을
하는 npc들이 즐비해있다. 실제 게임을 만들때 그러한 npc들을 여기저기 배치해둘 것이다.
(안그럼 밋밋해보이고, 모든 npc를 개성있게 만들기에는 시간이 부족하며 가성비가 안나온다...)

순찰하는 npc

모든 npc들을 핸들링하는데 사용할 NPC 클래스를 만든다.

class NPC
{
public:
	virtual NPC* clone(void) = 0;
#ifdef _DEBUG
	virtual void info(void) = 0;
#endif
};

NPC의 종류에 따라 외부 data(xml, csv, db 등)에서 세팅되는 애들도 있고, 처음부터 값을 static하게 가지는 애들도 있고

천차만별이다. 따라서, npc들 종류에 따라 prototype들을 관리할 store를 만든다.

(GoF에서도 prototype의 관리를 위해 'manager'에 등록하여 구현할 것을 제안하고 있다.)

spawn시킬 npc는 이 store에서 얻어온다.

class NPCStore
{
public:
	static NPCStore* This(void)
	{
		if (nullptr == s_instance)
		{
			s_instance = new NPCStore();
		}

		return s_instance;
	}

	void addNPCPrototype(const std::string& npcName, NPC* npcPrototype)
	{
		_actorRegistry.insert(make_pair(npcName, npcPrototype));
	}

	NPC* getNPC(const std::string& npcName)
	{
		TAccessor npcPrototype = _actorRegistry.find(npcName);

		//1) find 실패 검사
		if (npcPrototype == _actorRegistry.end())
		{
			//1-1) 보통 assert를 띄움
			std::cout << "[NPCStore::getNPC] " << npcName.c_str() << " find 실패하였습니다." << std::endl;
			return nullptr;
		}

		//2) clone 반환
		return npcPrototype->second->clone();
	}

private:
	NPCStore(void) {}

private:
	using TNPCRegistry = std::unordered_map<std::string, NPC*>;
	using TAccessor = TNPCRegistry::iterator;
	static NPCStore*		s_instance;
	TNPCRegistry			_actorRegistry;
};
NPCStore* NPCStore::s_instance = nullptr;

store는 아무래도 전역으로 하나만 존재해야 할 것 같아서 간단한 '싱글턴'으로 구현하였다.

: NPCStore::addNPCPrototype() 메소드로 npc종류에 따른 prototype을 스토어에 등록시킨다.

: getNPC() 메소드로 npc prototype으로부터 npc 인스턴스를 만든다.

 

임의로 무적인 행인 npc와 죽일 수 있는 고정형 npc 두 종류가 있다 가정하자.

// 행인 NPC
class WalkerNPC : public NPC
{
public:
	WalkerNPC(float speed) : _walkingSpeed(speed) {}

	virtual NPC* clone(void) override
	{
		NPC* cloned = new WalkerNPC(0);
		cloned = this;
		return cloned;
	}

#ifdef _DEBUG
	virtual void info(void) override
	{
		std::cout << "[WalkerNPC] speed : " << _walkingSpeed << std::endl;
	}
#endif

private:
	float		_walkingSpeed;
};

// 고정 NPC (고정된 위치에서 특정 행동만 수행 ex: 대장장이)
class FixedNPC : public NPC
{
public:
	FixedNPC(float hp) : _hp(hp) {}

	virtual NPC* clone(void) override
	{
		NPC* cloned = new FixedNPC(0);
		cloned = this;
		return cloned;
	}

#ifdef _DEBUG
	virtual void info(void) override
	{
		std::cout << "[FixedNPC] hp : " << _hp << std::endl;
	}
#endif

private:
	float		_hp;
};

clone 메소드를 override하여 각 종류의 npc 인스턴스를 만든뒤 대입 연산자를 통해 copy 한다.

(인스턴스의 copy를 shallow copy로 할지 deep copy로 할지는 요구사항에 따라 결정하여 구현한다.)

 

이제 npc들의 '원형'들을 만들어 store에 등록해두자!

(예제라 코드로 하지만, 실제 엔진이라면 툴 차원에서 지원할 것이다.)

NPCStore* store = NPCStore::This();

//1) speed가 빠른 행인 npc
NPC* npc = new WalkerNPC(200);
store->addNPCPrototype("Fast_NPC", npc);

//2) speed가 느린 행인 npc
npc = new WalkerNPC(10);
store->addNPCPrototype("Slow_NPC", npc);

//3) hp가 높은 고정 npc
npc = new FixedNPC(100000);
store->addNPCPrototype("High_Hp_NPC", npc);

//4) hp가 낮은 고정 npc
npc = new FixedNPC(20);
store->addNPCPrototype("Low_Hp_NPC", npc);

이처럼 store 같은 'manager'가 있으면 같은 class의 인스턴스라도 다른 state( WalkerNPC의 speed, FixedNPC의 hp)를

가지는 새로운 원형으로 등록시키는게 가능하다!

 

NPC* clonedNPC;

//1) speed가 느린 행인 npc
clonedNPC = store->getNPC("Slow_NPC");
clonedNPC->info();

//2) hp가 높은 고정 npc
clonedNPC = store->getNPC("High_Hp_NPC");
clonedNPC->info();

스토어로부터 새로운 npc 인스턴스를 얻었으니 배치할 수 있다!


이처럼 유용한 패턴이지만 역시 단점도 있다.

바로 clone() 메소드이다.

clone() 메소드를 반드시 구현해야 하는데, 다음과 같은 3가지 사항에서 clone() 메소드 구현이 어려울 수 있다.

1. 이미 존재하는 class를 prototype으로 만드려 할때 어려울 수 있다.

2. 언어적으로 '복사'를 지원하지 않는다.

3. 환형참조가 있는 object

 

또한, concrete class에서만 아주 특별하게 쓰는 field( member variable )가 있고, 이 field를 외부에서

참조해야 하는경우 결국 down cast가 발생한다.

(근데 이건 subclass를 은닉시키는 모든 상황에서 발생가능한 것이므로, 그러한 상황이 나오지 않게

설계해야한다.)

728x90

'프로그래밍 > C++' 카테고리의 다른 글

Observer Pattern  (0) 2021.08.13
Bridge Pattern  (0) 2021.07.15
Adaptor Pattern  (0) 2021.06.30
Factory Pattern - Simple Factory  (1) 2021.02.21
[C++] Cyclic Reference - Weak Reference  (0) 2020.11.11

+ Recent posts