스테이트 패턴이란

 

State : 상태

상태패턴이라고도 불림

 

상태에 따라 객체의 행동을 바꾸는 패턴

상태들을 클래스로 만들고 해당 클래스에서는 해당 상태에서 할 수 있는 행위들을 메서드로 정의

Context가 현재 상태를 확인하고 해당 상태에 동작해야 할 상태클래스에 행동을 위임

 

객체의 클래스가 바뀌는것과 같은 결과를 만들어냄

 

스태이트 패턴을 사용하는 상황

- 객체의 행동이 상태에 따라 달라지는 경우

- 런타임에 객체의 상태에 따라 행동이 바뀌어야 하는 경우

- 객체의 상태에 따라 달라지는 분기문이 많은 경우

- 상태 간 전환이 명시적인 경우

 

 

 

스테이트 패턴의 구조

 

 

1) Context

- 여러 상태를 가질수 있는 객체

- State를 이용하는 역할

- 현재 시스템의 상태를 나타내는 state변수를 갖는다.

- 각 Concrete State 클래스에서 상태 변경을 요청할 수 있도록 setState함수를 제공한다.

- request()함수는 실제 동작을 실행하는것이 아니라, 해당 상태 클래스의 객체에게 실제 동작을 위임한다.

 

2) State

- 시스템의 모든 상태에 공통의 인터페이스 제공

- State 인터페이스를 상속받아 구현한 모든 상태클래스들은 기존 상태 클래스를 대신해서 사용될 수 있는 것

 

3) State1, 2, 3

- Concrete State 클래스로 Context 객체가 요청한 작업을 자신의 방식대로 실제로 실행하는 역할

- 자신이 수행할 동작을 마친 후 다음 상태를 결정해서 Context의 setState로 상태변경을 요청하는 역할도 수행함

 

 

 

스테이트 패턴이 없다면?

 

노트북의 전원버튼을 예시로 든다면 

1) 전원이 켜져있는 상태 : 전원버튼을 누르면 전원이 꺼진다.

2) 전원이 꺼져있는 상태 : 전원버튼을 누르면 전원이 켜진다.

3) 절전모드인 상태 : 전원버튼을 누르면 전원이 켜진다.

#include <iostream>
using namespace std;

class Laptop
{
private:
	string power_state = "";

public:
	const string ON = "on";
	const string OFF = "off";
	const string SAVING = "saving";

	Laptop() { setPowerState(OFF); }

	void setPowerState(string state) { power_state = state; }

	void PushPowerBtn() {
		// 전원이 켜져있을때 버튼을 누르면 꺼진다.
		if (power_state == ON) cout << "system off\n";		
		// 전원이 꺼져있을때 버튼을 누르면 켜진다.
		else if (power_state == OFF) cout << "system on\n";
		else if (power_state == SAVING) cout << "절전모드 해제\n";
	}
};

void main()
{
	Laptop laptop;

	laptop.PushPowerBtn();
	laptop.setPowerState("on");

	laptop.PushPowerBtn();
	laptop.setPowerState("saving");

	laptop.PushPowerBtn();
	laptop.setPowerState("off");

	laptop.PushPowerBtn();
	laptop.setPowerState("on");
}

 

결과)

 

여기서 상태가 추가된다면 클래스 내부의 분기문을 추가해줘야 하므로 OCP원칙을 위배하게 된다.

또한 상태가 아주 많아진다면 분기문이 길어지므로 가독성이 떨어지고 

버튼이 Laptop에만 있는게 아니라 TV나 스마트폰등에도 있다면 기능변경이 생길때마다 각 클래스를 다 찾아다니며 수정해야한다.

 

 

 

스테이트 패턴을 사용한다면

 

스테이트 패턴을 적용한다면 각 상태들을 클래스로 만들게 된다.

즉 ON클래스, OFF클래스, SAVING클래스를 만들고 이 클래스들을 STATE클래스로 묶는다.

그리고 Laptop이 상태인터페이스의 메서드를 호출하면 각 상태클래스에서 정의된 행위가 수행된다.

#include <iostream>
using namespace std;

/*
State 추상클래스
*/
class PowerState
{
public:
	virtual void PushPowerBtn() {};
};

/*
concrete State클래스들
*/
class ON : public PowerState
{
	void PushPowerBtn() { cout << "system off\n"; }
};

class OFF : public PowerState
{
	void PushPowerBtn() { cout << "system on\n"; }
};

class SAVING : public PowerState
{
	void PushPowerBtn() { cout << "절전모두 해제\n"; }
};

/*
Context 클래스(여기선 Laptop)
여러 상태를 가질 수 있는 객체이며
각 상태마다 행동이 달라지는 객체
*/
class Laptop
{
private:
	PowerState* state;

public:
	Laptop() { state = new OFF(); }

	void setPowerState(PowerState* powerstate) { state = powerstate; }
	void PushPowerBtn() { state->PushPowerBtn(); }
};

void main()
{
	Laptop laptop;
	ON* on = new ON();
	OFF* off = new OFF();
	SAVING* saving = new SAVING();

	laptop.PushPowerBtn();
	laptop.setPowerState(on);

	laptop.PushPowerBtn();
	laptop.setPowerState(saving);

	laptop.PushPowerBtn();
	laptop.setPowerState(off);

	laptop.PushPowerBtn();
}

 

 

 

장단점

 

1) 장점

- 서로 다른 상태에 대한 행동을 별도의 객체로 관리할 수 있음

- 새로운 상태가 추가되더라도 context 객체가 받는 영향이 적음

- 하나의 객체에 대한 여러 동작을 구현해야 할 때 상태변수만 변경하면 동작의 추가/삭제/수정이 간단해짐

- 상태에 따른 조건분기문이 줄어들어 가독성이 좋아진다.

- 특정 하나의 상태에 대한 행동양식이 하나의 클래스에 모여있으므로 유지보수가 쉽다.

 

2) 단점

- 조건문을 대신해 클래스가 증가하므로 클래스의 수가 늘어나 유지보수가 오히려 힘들수도 있다.

- 상태에 따라 변하는 메서드의 숫자가 적으면 쓸데없이 복잡성만 증가한다.

 

 

 

 

 

 

※ 참고 문헌

https://yummy0102.tistory.com/482