디자인 패턴/GoF

[디자인 패턴] 행동 패턴 (11) 템플릿 메서드 Template Method

로파이 2021. 4. 9. 15:08

템플릿 메서드 (Template Method) 패턴

 

1. 의도

 

객체 연산에 알고리즘의 뼈대만 정의하고 알고리즘을 구성하는 연산의 구현은 서브 클래스로 미룬다.

 

2. 활용

 

Application 클래스와 Document 클래스를 제공하는 응용프로그램의 프레임워크의 예를 들어보자. Application의 클래스는 Document에 관한 연산들을 선언하고 있고 Document는 실제 문서를 열고 저장하고 닫는 연산을 선언한다. Applcation 클래스는 Document 인스턴스를 소유하며 이에 대한 OpenDocument 연산의 예시를 보면,

void Application::OpenDocument (const char* name)
{
    if(!CanOpenDocument(name)
    {
    	// 이 문서를 처리할 수 없음
        return;
    }
    
    Document* doc = DoCreateDocument();
    
    if(doc)
    {
        _docs->AddDocument(doc);
        AboutToOpenDocument(doc);
        doc->Open();
        doc->DoRead();
    }
}

문서를 열기위한 절차가 수행되고 있음을 볼 수 있다. 절차는 해당 이름으로 된 먼저 문서를 열 수 있는 지 판단하고, 문서를 생성한 다음 자신이 가지고 있는 문서 리스트 _docs에 새로운 문서를 추가한다. 문서 정보를 열람하고 실제 내용을 파악하는 것은 Document 클래스의 연산을 사용하고 있다.

 

템플릿 메서드는 OpenDocument, CanOpenDocument, DocreateDocument, AboutToDocument와 같은 연산을 선언하고 각 알고리즘에 포함된 수행 절차를 서브 클래스가 새로 정의할 수 있도록 하는 것이다. 즉 Application 클래스는 위 연산에 대한 뼈대만 제공하고 실제 내용은 MyApplication에서 자세한 수행 절차를 구현하도록한다.  

 

3. 참여자

 

  • AbstractClass(Application): 서브 클래스들이 재정의를 통해 구현해야하는 기본 연산을 정의한다. 그리고 알고리즘의 뼈대를 정의하는 템플릿 메서드를 구현한다. 템플릿 메서드는 Abstract 클래스에 정의된 연산과 다른 객체의 연산도 호출할 수 있다.
  • ConcreteClass(MyApplication): 서브 클래스마다 달라지는 알고리즘 처리 단계를 수행하기 위한 기본 연산을 구현한다.

4. 협력 방법

  • ConcreteClass는 Abstract 클래스의 템플릿 메서드에 정의된 수행 절차에 포함되는 연산들을 재 정의하여 구현하도록 한다.

5. 결과

 

템플릿 메서드는 코드 재사용을 위한 기본 기술이다.

템플릿 메서드는 다음과 같은 연산을 호출한다.

  1) 구체 연산, ConcreteClass나 다른 클래스의 연산을 호출한다.

  2) AbstractClass에 정의된 연산

  3) 자신의 기본 연산

  4) 팩토리 메서드

  5) 훅 메서드: 부모 클래스에는 정의가 없으며 서브 클래스에서 재정의된 함수 

 

6. 구현

 

1. C++의 접근 제한 방법을 이용하여 protected 클래스로 선언된 함수는 파생 클래스에서 템플릿 메서드를 재정의할 때 부모 클래스의 필요한 연산을 호출 가능하게 된다.

2. 기본 연산의 수를 최소화한다. 재정의해야하는 연산을 줄이고 실체화가 필요한 부분만 가상 함수로 정의하는 것이다.

3. 이름을 짓는 규칙을 만든다. 재정의가 필요한 연산에 대한 이름을 잘 짓도록 한다. (DoRead, DoCreate... 등)

 

예제 코드

 

게임상 물리 효과를 구현하기 위해 내가 사용한 실제 코드에서 템플릿 메서드를 의도치 않게 적용하였다. 아이템 드랍이나 아이템이 끌려오는 효과 혹은 물체의 흔들림, 넉백 등등 객체의 2D 좌표를 시간에 따라 물리 법칙에 맞게 변화시키는 Effect 클래스에 대한 내용을 템플릿 메서드를 적용할 수 있다.

 

EffectInterface.h

class EffectInterface
{
public:
	EffectInterface();
	virtual ~EffectInterface() = 0;
protected:
	bool m_bEnd = false;
public:
	bool IsEnd() const { return m_bEnd; }
	virtual void Step(float dt) = 0;
};

typedef unique_ptr<EffectInterface> EffectPtr;

이펙트 효과를 위한 기본 인터페이스 (순수 추상 클래스)를 정의한다. 물리 효과의 적용 시간이 끝에 도달했는지 (IsEnd())와 매 시간 마다 델타 타임을 받아서 처리하는 로직에 대한 함수 Step를 선언한다.

 

Effect.h

class Effect : public EffectInterface
{
protected:
	Pos m_tOrigin;
	class Object* m_pSubject = nullptr;
	float m_fDuration = 0.f;
	float m_fMaxDuration = 0.f;
private:
	bool IsOver() const{ return m_fDuration >= m_fMaxDuration; }
protected:
	explicit Effect(class Object* pObj, float duration);
	Effect(const Effect& effcet) = delete;
	virtual ~Effect() = 0;
	virtual void Process(float dt) = 0;
	virtual bool Predicate() const { return false; }
public:
	virtual void Step(float dt) final;
};

실제 템플릿 메서드를 선언한 Effect 클래스는 EffectInterface를 상속 받는다. 멤버로 객체의 2D 좌표를 변화시키기위해 객체를 소유하고 적용 시간과 경과 시간을 기록하기 위한 멤버를 둔다. 

Process(dt)는 실제 물리 구현을 위한 함수를 서브 클래스에서 정의하고 Predicate는 경과 시간이외에도 다른 조건 함수를 서브 클래스에서 구현할 수 있도록 한다. 

 

템플릿 메서드인 Step(dt)는 더 이상 재정의를 허용하지 않으며 Effect 클래스에서 선언된 함수, Process()와 Predicate(), IsOver() 등을 적절히 활용하여 알고리즘의 수행 과정을 정의한다.

 

템플릿 메서드 Step(dt)의 알고리즘 뼈대는 다음과 같다.

#include "Effect.h"
#include "../Object/Object.h"

Effect::Effect(Object* pObj, float duration)
	: m_tOrigin(pObj->GetPos()), m_pSubject(pObj), m_fMaxDuration(duration)
{
}

Effect::~Effect()
{
}

void Effect::Step(float dt)
{
	if (!m_bEnd)
	{
		m_fDuration += dt;
		Process(dt);
		m_bEnd = IsOver() || Predicate();
	}
}

아직 물리 효과가 유효하다면 경과 시간을 축적하고 서브 클래스에서 재정의할 물리 법칙을 Process(dt)로 적용한다. 그리고 물리 효과가 끝났는 지 여부를 판별하여 m_bEnd를 다시 판단하고 있다.

 

BoundEffect.h

Effect 클래스의 서브 클래스로 아래 그림과 같이 아이템이 바닥과 부딪히고 여러번 튕기는 모션을 구현한다.

https://m.blog.naver.com/PostView.nhn?blogId=zinblue&logNo=220859147104&proxyReferer=http:%2F%2F211.193.127.39%2F

class BoundEffect : public Effect
{
private:
	static constexpr float m_fCoeff = 0.7f;
	int m_iBounceCount = 0;
	int m_iMaxBounceNum = 5;
	float m_fVeloY = 0.f;
	float m_fVeloX = 0.f;
	float m_fYLimit = 0.f;
protected:
	virtual void Process(float dt);
	virtual bool Predicate() const;
public:
	explicit BoundEffect(Object* pObj, float duration, int maxBounce,
							float fAngle, float fVelo, float YLimit);
	~BoundEffect();
};
#include "BoundEffect.h"
#include "../Object/Object.h"

BoundEffect::BoundEffect(Object* pObj, float duration,
						int maxBounce,
						float fAngle, float fVelo,
						float YLimit)
	: Effect(pObj, duration)
{
	m_iMaxBounceNum = maxBounce;
	m_fYLimit = YLimit;
	m_fVeloX = fVelo * cosf(fAngle);
	m_fVeloY = fVelo * sinf(fAngle);
}

BoundEffect::~BoundEffect()
{
}

void BoundEffect::Process(float dt)
{
	m_fVeloY -= GRAVITY * dt;

	float dx = m_fVeloX * dt;
	float dy = m_fVeloY * dt;

	m_tOrigin.x += dx;
	m_tOrigin.y -= dy;
	m_pSubject->SetPos(m_tOrigin);

	if (m_tOrigin.y >= m_fYLimit && m_iBounceCount < m_iMaxBounceNum)
	{
		m_fVeloX *= m_fCoeff;
		m_fVeloY *= -m_fCoeff;
		++m_iBounceCount;
	}
}

bool BoundEffect::Predicate() const
{
	return (m_iBounceCount == m_iMaxBounceNum);
}

추가적인 조건 함수로 최대 튕길 수 있는 횟수를 정의하고 튕길 때마다 적용되는 물리를 Process(dt)에 구현한다. 

 

매 프레임마다 오브젝트에 물리 효과가 적용되어 있다면 다음 클라이언트 코드로 물리 효과를 적용할 수 있다.

    if (m_pEffect)
    {
        m_pEffect->Step(dt);
        if (m_pEffect->IsEnd())
        {
            m_pEffect = nullptr;
        }
    }

물리 효과가 있다면 적용하고 끝났다면 물리 효과 객체를 소멸시킨다.