의도
- 객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보임
다른 이름
- 상태 표현 객체(Object for State)
언제 쓰는가 ?
- 객체의 행동이 상태에 따라 달라질 수 있고, 객체의 상태에 따라서 런타임에 행동이 바뀌어야 함
- 어떤 연산에 그 객체의 상태에 따라 달라지는 다중 분기 조건 처리가 너무 많이 들어있을 때.
구조
- Context : 사용자가 관심 있는 인터페이스를 정의. 객체의 현재 상태를 정의한 ConcreteState 서브클래스의 인스턴스를 유지,관리 함
- State : Context의 각 상태별로 필요한 행동을 캡슐화하여 인터페이스로 정의.
- ConcreteState 서브클래스들 : 각 서브클래스들은 Context의 상태에 따라 처리되어야 할 실제 행동을 구현.
협력 방법
- 상태에 따라 다른 요청을 받으면 Context 클래스는 현재의 ConcreteState 객체로 전달. 이 ConcreteState 객체는 State 클래스를 상속하는 서브클래스들 중 하나의 인스턴스일 것
- Context 클래스는 실제 연산을 처리할 State 객체에 자신을 매개변수로 전달함. 이로써 State 객체를 Context 클래스에 정의된 정보에 접근할 수 있음
- Context 클래스는 사용자가 사용할 수 있는 기본 인터페이스를 제공. 사용자는 상태 객체를 Context 객체와 연결. Context는 실제 상태를 가지고 사용자는 State를 몰라도 Context를 통해 요청을 보낼 수 있음
- Context 클래스 또는 ConcreteState 서브클래스들은 자기 다음의 상태가 무엇인지 알고 있음. 상태 전이 규칙을 알아야 함.
결과
- 상태에 따른 행동을 국소화하며, 서로 다른 상태에 대한 행동을 별도의 객체로 관리함
- 특정 상태에 대한 행동을 하나의 객체에 모을 수 있음.
- 새로운 상태 발견 시 새로운 서브클래스를 정의하기만 하면 됨
- 상태 전이를 명확하게 만듬
- 어떤 객체가 자신의 현재 상태를 내부 데이터로만 가지면 상태 전이가 명확한 표현을 갖지 못함
- 상태 객체는 공유될 수 있음
- 플라이급 객체로서 활용될 수 있음
구현 / 고려 사항
- 누가 상태 전이를 정할 것인가?
- Context가 할지 State 내부에서 처리할지
- 일반적으로 State 내부에서 처리하는 것이 더 유용
- 테이블 기반의 대안
- 상태 객체의 생성과 소멸
- 필요할 때만 생성, 소멸 vs 미리 만들고 이용
- 상태가 자주 바뀌지 않거나 상태가 실행되기 전까지 어떤 상태인지 모르면 생성, 소멸이 유용
- 필요할 때만 생성, 소멸 vs 미리 만들고 이용
- 동적 상속을 이용하는 방법
예제 코드 (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 |