의도

  • 객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보임

 

다른 이름

  • 상태 표현 객체(Object for State)

 

언제 쓰는가 ?

  • 객체의 행동이 상태에 따라 달라질 수 있고, 객체의 상태에 따라서 런타임에 행동이 바뀌어야 함
  • 어떤 연산에 그 객체의 상태에 따라 달라지는 다중 분기 조건 처리가 너무 많이 들어있을 때.

 

구조

GoF의 디자인 패턴 중 P.397

  • Context : 사용자가 관심 있는 인터페이스를 정의. 객체의 현재 상태를 정의한 ConcreteState 서브클래스의 인스턴스를 유지,관리 함
  • State : Context의 각 상태별로 필요한 행동을 캡슐화하여 인터페이스로 정의.
  • ConcreteState 서브클래스들 : 각 서브클래스들은 Context의 상태에 따라 처리되어야 할 실제 행동을 구현.

 

협력 방법

  • 상태에 따라 다른 요청을 받으면 Context 클래스는 현재의 ConcreteState 객체로 전달. 이 ConcreteState 객체는 State 클래스를 상속하는 서브클래스들 중 하나의 인스턴스일 것
  • Context 클래스는 실제 연산을 처리할 State 객체에 자신을 매개변수로 전달함. 이로써 State 객체를 Context 클래스에 정의된 정보에 접근할 수 있음
  • Context 클래스는 사용자가 사용할 수 있는 기본 인터페이스를 제공. 사용자는 상태 객체를 Context 객체와 연결. Context는 실제 상태를 가지고 사용자는 State를 몰라도 Context를 통해 요청을 보낼 수 있음 
  • Context 클래스 또는 ConcreteState 서브클래스들은 자기 다음의 상태가 무엇인지 알고 있음. 상태 전이 규칙을 알아야 함.

 

결과

  1. 상태에 따른 행동을 국소화하며, 서로 다른 상태에 대한 행동을 별도의 객체로 관리함
    1. 특정 상태에 대한 행동을 하나의 객체에 모을 수 있음.
    2. 새로운 상태 발견 시 새로운 서브클래스를 정의하기만 하면 됨
  2. 상태 전이를 명확하게 만듬
    1. 어떤 객체가 자신의 현재 상태를 내부 데이터로만 가지면 상태 전이가 명확한 표현을 갖지 못함
  3. 상태 객체는 공유될 수 있음
    1. 플라이급 객체로서 활용될 수 있음

 

구현 / 고려 사항

  1. 누가 상태 전이를 정할 것인가?
    1. Context가 할지 State 내부에서 처리할지
    2. 일반적으로 State 내부에서 처리하는 것이 더 유용
  2. 테이블 기반의 대안
  3. 상태 객체의 생성과 소멸
    1. 필요할 때만 생성, 소멸 vs 미리 만들고 이용
      1. 상태가 자주 바뀌지 않거나 상태가 실행되기 전까지 어떤 상태인지 모르면 생성, 소멸이 유용
  4. 동적 상속을 이용하는 방법

 

예제 코드 (1)

  • Character가 마우스 좌클릭을 하면 Idle, Attack, Ult 상태를 조건에 맞게 왔다갔다 변경할 수 있게 끔 예제를 짜봤습니다.
  • Attack은 최대 3회 연속으로 이어질 수 있고 Attack이 총 10번 발동되면 Ult 게이지가 쌓여 Ult 상태가 됩니다.
#include "State1.h"

int main()
{
	Character* character = new Character;

	for (int i = 0; i < 20; i++)
	{
		character->MouseLeftClick();
	}

	return 0;
}

  • 캐릭터가 20번 마우스 좌클릭을 했다고 가정하에 예제 구성했습니다.
  • 초기 Idle 상태에서 Attack상태로 변경 후 3번 Attack하고 다시 Idle 상태로 돌아가고 Attack이 총 10번 누적되면 Ult상태로 진입하여 궁극기를 쓸 수 있게 됩니다.
#pragma once
#include <map>
#include <string>

class Character;

class CharacterState
{
public:
	CharacterState(Character* character) : character(character) {}
	virtual void Activate() = 0;
	void ChangeState(CharacterState* state);

protected:
	Character* character;
};

class Character
{
	friend class CharacterState;

public:
	Character();
	virtual ~Character();

public:
	void MouseLeftClick();

	CharacterState* GetState(std::string state_name);

private:
	void ChangeState(CharacterState* state);

public:
	size_t attack_count = 0;
	size_t ult_gauge = 0;

private:
	std::map<std::string, CharacterState*> StateMap;

	CharacterState* state = nullptr;
};

class IdleState : public CharacterState
{
public:
	IdleState(Character* character)
		: CharacterState(character) {}

	virtual void Activate();
};

class AttackState : public CharacterState
{
public:
	AttackState(Character* character)
		: CharacterState(character) {}

	virtual void Activate();
};

class UltState : public CharacterState
{
public:
	UltState(Character* character) 
		: CharacterState(character) {}

	virtual void Activate();
};
  • 추상 클래스 CharacterState는 Activate를 오버라이드하여 각 서브 클래스마다 행동을 정의합니다.
  • CharacterState는 character의 state를 변경할 수 있는 메서드 ChangeState 메서드가 있는데 CharacterState와 Character는 friend로 선언되어 있기 때문에 CharacterState에서 Character의 ChangeState 메서드(private)에 접근할 수 있게 됩니다.
  • Character의 상태는 생성자에서 map 컨테이너를 통해 초기화되며 key는 string입니다. 필요할 때 만드는 방식이 아닌 미리 만들어놓고 map 컨테이너에 저장해놓고 필요할 때 검색하여 사용하는 방법입니다.
#include "State1.h"
#include "iostream"

Character::Character()
{
	StateMap["Idle"] = new IdleState(this);
	StateMap["Attack"] = new AttackState(this);
	StateMap["Ult"] = new UltState(this);

	state = StateMap["Idle"];
}

Character::~Character()
{
	for (auto& ele : StateMap)
	{
		delete ele.second;
	}
}

void Character::MouseLeftClick()
{
	state->Activate();
}

void Character::ChangeState(CharacterState* state)
{
	this->state = state;
}

CharacterState* Character::GetState(std::string state_name)
{
	return StateMap[state_name];
}

void IdleState::Activate()
{
	std::cout << "Idle 상태에서 Attack 상태로 전이됩니다" << std::endl;
	ChangeState(character->GetState("Attack"));
}

void AttackState::Activate()
{
	if (character->ult_gauge >= 10)
	{
		character->attack_count = 0;
		std::cout << "Attack 상태에서 Ult 상태로 전이됩니다" << std::endl;
		ChangeState(character->GetState("Ult"));
	}
	else if (character->attack_count >= 3)
	{
		character->attack_count = 0;
		std::cout << "Attack 상태에서 Idle 상태로 전이됩니다" << std::endl;
		ChangeState(character->GetState("Idle"));
	}
	else
	{
		character->attack_count++;
		character->ult_gauge++;
		std::cout << "Attack 상태가 지속됩니다" << std::endl;
	}
}

void UltState::Activate()
{
	character->ult_gauge = 0;
	std::cout << "궁극기를 발동합니다" << std::endl;
	ChangeState(character->GetState("Idle"));
}

void CharacterState::ChangeState(CharacterState* state)
{
	character->ChangeState(state);
}
  • 우리의 예제는 CharacterState가 가지는 Activate 메서드나 Character의 MouseLeftClick을 통해 한 가지 동작만 할 수 있지만 Character가 오른쪽 마우스 클릭도 할 수 있고 방향키도 누를 수 있기 때문에 사실 CharacterState도 Activate메서드 뿐만 아니라 다양한 인터페이스를 준비했어야 합니다. 예제에선 좌클릭을 통해 idle, attack, ult 의 상태 변환만 처리합니다.
  • 위에서 언급한대로 CharacterState의 ChangeState는 private으로 감춰진 character의 메서드 ChangeState를 호출할 수 있습니다.
  • 예제의 편의를 위해 Character의 attack_count나 ult_gauge 같은 경우 public으로 두어 CharacterState의 서브클래스들에서 접근하기 용이하기 구성해뒀습니다.
  • 책에선 각각의 State에 대해 싱글턴으로 구성하여 재사용하는 방법을 제공했지만 우리는 간단히 Character가 각각의인스턴스를 map 컨테이너에 보관합니다.

 

예제 코드 (2)

  • 위 예제와 동작은 완전히 같으나 state 객체들을 관리하는 방법을 다르게 하여 기존 예제를 변경해봤습니다.
#include "State2.h"

int main()
{
	Character2* character = new Character2;

	for (int i = 0; i < 20; i++)
	{
		character->MouseLeftClick();
	}

	return 0;
}

  • 각각의 state에 대해서 CharacterState2 내의 static 멤버변수로 관리하게 되며 초기화는 프로그램이 시작되며 시켜주어야 합니다.(State2.cpp의 전역 공간에서 new로 초기화를 진행합니다.)
//State2.h

#pragma once
#include <map>
#include <string>

class Character2;
class IdleState2;
class AttackState2;
class UltState2;

class CharacterState2
{
public:
	static IdleState2* idle_state;
	static AttackState2* attack_state;
	static UltState2* ult_state;

public:
	virtual void Activate(Character2* character2) = 0;
	void ChangeState(Character2* character2, CharacterState2* state);
};

class Character2
{
	friend class CharacterState2;

public:
	Character2();

public:
	void MouseLeftClick();

private:
	void ChangeState(CharacterState2* state);

public:
	size_t attack_count = 0;
	size_t ult_gauge = 0;

private:
	CharacterState2* state = nullptr;
};

class IdleState2 : public CharacterState2
{
public:
	virtual void Activate(Character2* character2) override;
};

class AttackState2 : public CharacterState2
{
public:
	virtual void Activate(Character2* character2) override;
};

class UltState2 : public CharacterState2
{
public:
	virtual void Activate(Character2* character2) override;
};
  • 1번 예제와 다른 점은 Activate 메서드나 ChangeState 메서드가 Character2*의 매개변수를 더 받는 점이나, Character2마다 개별의 State를 소유하는 것이 아닌 공유객체를 가리키고 있다는 점입니다.(플라이급)
//State2.cpp

#include "State2.h"
#include "iostream"

IdleState2* CharacterState2::idle_state = new IdleState2;
AttackState2* CharacterState2::attack_state = new AttackState2;
UltState2* CharacterState2::ult_state = new UltState2;

Character2::Character2()
{
	state = CharacterState2::idle_state;
}

void Character2::MouseLeftClick()
{
	state->Activate(this);
}

void Character2::ChangeState(CharacterState2* state)
{
	this->state = state;
}

void IdleState2::Activate(Character2* character2)
{
	std::cout << "Idle 상태에서 Attack 상태로 전이됩니다" << std::endl;
	ChangeState(character2, CharacterState2::attack_state);
}

void AttackState2::Activate(Character2* character2)
{
	if (character2->ult_gauge >= 10)
	{
		character2->attack_count = 0;
		std::cout << "Attack 상태에서 Ult 상태로 전이됩니다" << std::endl;
		ChangeState(character2, CharacterState2::ult_state);
	}
	else if (character2->attack_count >= 3)
	{
		character2->attack_count = 0;
		std::cout << "Attack 상태에서 Idle 상태로 전이됩니다" << std::endl;
		ChangeState(character2, CharacterState2::idle_state);
	}
	else
	{
		character2->attack_count++;
		character2->ult_gauge++;
		std::cout << "Attack 상태가 지속됩니다" << std::endl;
	}
}

void UltState2::Activate(Character2* character2)
{
	character2->ult_gauge = 0;
	std::cout << "궁극기를 발동합니다" << std::endl;
	ChangeState(character2, CharacterState2::idle_state);
}

void CharacterState2::ChangeState(Character2* character2, CharacterState2* state)
{
	character2->ChangeState(state);
}
  • 앞선 예제와 다르게 각각의 state 객체에 대해서 Character로 부터 얻어오는 것이 아닌 CharacterState 네임스페이스 내에 있는 static 멤버변수로부터 얻어오게 됩니다.

'디자인 패턴 > 행위' 카테고리의 다른 글

템플릿 메서드(Template Method)  (2) 2024.01.15
전략(Strategy)  (0) 2024.01.15
감시자(Observer)  (0) 2024.01.12
메멘토(Memento)  (1) 2024.01.11
중재자(Mediator)  (2) 2024.01.11

+ Recent posts