개발/Architecture & Design Pattern

C++ - 상태, 스테이트 패턴 (State Pattern)

피로물든딸기 2024. 2. 20. 22:12
반응형

C, C++ 전체 링크

Architecture & Design Pattern 전체 링크

 

참고

- 클래스 다이어그램 그리기

- friend로 private 멤버 출력하기

- 스마트 포인터 : unique_ptr

 

상태, 스테이트 패턴 (State Pattern) - 행동 패턴

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

- 상태에 대한 행동을 캡슐화하여, 위임을 이용해 필요한 행동을 선택한다.

- 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.

 

구현

- State Class : 상태를 나타내는 추상 클래스 또는 인터페이스를 정의, 객체의 특정 상태에 따른 행위를 정의

- Context Class : 상태를 가지고 있는 객체를 나타내는 클래스. 상태 객체를 참조하고, 상태 변경 메서드 제공

- Concrete State : 컨텍스트 객체의 상태를 변경하는 메서드를 구현, 상태 변경을 관리

 

장점

- SRP(단일 책임 원칙), 특정 상태들과 관련된 코드만 관리할 수 있다.

- OCP(개방-폐쇄 원칙), 기존 상태 클래스나 컨텍스트를 변경하지 않고 새로운 상태를 추가할 수 있다.

- 조건문이나 switch 문을 사용하여 상태를 확인하는 것보다 코드를 더욱 명확하게 만든다.

 

단점

- 상태가 적으면 스테이트 패턴은 과할 수 있다.

- 상태가 많아지면 클래스 수가 늘어나고, 클래스 간의 관계가 복잡해질 수 있다.

- 컨텍스트 객체가 상태를 변경할 때마다 새로운 상태 객체를 생성해야 하므로, 메모리 사용량이 늘어날 수 있다.


콜라 자판기

 

콜라를 판매하는 자판기가 있다.

 

자판기는 동전을 삽입할 수 있고, 동전을 뺄 수 있다. 

또한 콜라 버튼을 누르면 콜라가 나오도록 할 수 있다. 

콜라가 나오는 행위(dispense) 자체는 사용자가 자판기에서 직접 건드릴 수 없으므로 여기서는 private으로 관리한다.

 

자판기의 정보를 얻기 위해 friend로 출력 메서드 선언하였다.

그리고 자판기에는 4개의 상태가 존재한다. (콜라 매진 / 동전 없음 / 동전 있음 / 콜라 판매)

콜라의 가격(PRICE)은 1000원으로 설정하였다.

class VendingMachine
{
	friend ostream& operator<<(ostream &os, VendingMachine* vm);
    
public:
	VendingMachine(int n) : coke(n) { if (coke > 0) state = NO_COIN; }
	void insertCoin(int c); // 동전 삽입
	void takeOutCoin(); // 동전 빼기
	void pressButton(); // 콜라 버튼
    
private:
	void dispense(); // 콜라 내보내기

	static constexpr int SOLD_OUT = 0;
	static constexpr int NO_COIN = 1;
	static constexpr int HAS_COIN = 2;
	static constexpr int SOLD = 3;
	static constexpr int PRICE = 1000; // 콜라의 가격

	int state = SOLD_OUT; 
	int coke = 0; // 콜라의 개수
	int coin = 0; // 현재 자판기의 동전
};

 

각 메서드는 상태에 따라 행동이 달라진다.

 

예를 들어 동전을 넣는 insertCoin의 경우, 아래처럼 행동해야 한다.

- SOLD_OUT : 콜라가 매진되었으므로, 동전을 넣을 수 없다.
- NO_COIN : 동전이 추가되고, HAS_COIN으로 상태를 변경시킨다.
- HAS_COIN : 동전이 추가된다.
- SOLD : 콜라가 나오고 있는 중이므로, 동전을 넣을 수 없다.

 

자판기의 동전을 반환하는 takeOutCoin은 다음과 같다.

- SOLD_OUT : 콜라가 매진되었으므로, 동전이 반환될 수 없다.
- NO_COIN : 동전을 넣지 않았으므로, 동전이 반환될 수 없다.
- HAS_COIN : 동전을 반환하고, 얼마나 반환되었는지 알려준다.
- SOLD : 콜라가 나오고 있으므로, 동전이 반환될 수 없다.

 

자판기의 버튼을 누르는 pressButton은 다음과 같다.

- SOLD_OUT : 콜라가 매진되었으므로, 버튼을 눌러도 아무 반응이 없어야 한다.
- NO_COIN : 동전을 넣지 않았으므로, 버튼을 눌러도 아무 반응이 없어야 한다.
- HAS_COIN : 동전이 충분하다면, 콜라를 판매(SOLD로 변경), 그렇지 않다면 동전 부족을 알린다.
- SOLD : 콜라가 나오고 있으므로, 버튼을 눌러도 아무 반응이 없어야 한다.

 

마지막으로 콜라를 배출하는 dispense는 다음과 같다.

- SOLD_OUT : 콜라가 매진되었으므로, 콜라가 나올 수 없다.
- NO_COIN : 동전을 넣지 않았으므로, 콜라가 나올 수 없다.
- HAS_COIN : 동전 부족을 알린다.
- SOLD : 콜라를 내보내고, 콜라가 없다면 SOLD_OUT으로 상태를 변경한다.
        그리고 남은 동전을 갱신(NO_COIN, HAS_COIN)한다. SOLD_OUT이라면 남은 동전을 반환한다.

 

물론 콜라가 매진되었다면, 남은 코인도 반환해야 한다.

 

위의 요구사항을 구현하면 아래와 같다.

#include <iostream>
#include <string>

using namespace std;

class VendingMachine
{
	friend ostream& operator<<(ostream &os, VendingMachine* vm);
    
public:
	VendingMachine(int n) : coke(n) { if (coke > 0) state = NO_COIN; }
	void insertCoin(int c); // 동전 삽입
	void takeOutCoin(); // 동전 빼기
	void pressButton(); // 콜라 버튼
    
private:
	void dispense(); // 콜라 내보내기

	static constexpr int SOLD_OUT = 0;
	static constexpr int NO_COIN = 1;
	static constexpr int HAS_COIN = 2;
	static constexpr int SOLD = 3;
	static constexpr int PRICE = 1000; // 콜라의 가격

	int state = SOLD_OUT; 
	int coke = 0; // 콜라의 개수
	int coin = 0; // 현재 자판기의 동전
};

void VendingMachine::insertCoin(int c)
{
	if (state == SOLD_OUT)
		cout << "Impossible to insert coin, The machine is sold out." << endl;
	else if (state == NO_COIN || state == HAS_COIN)
	{
		state = HAS_COIN;
		
		coin += c;

		cout << "Insert coin, current coin is : " << coin << endl;
	}
	else if (state == SOLD)
		cout << "Impossible to insert coin, The coke is comming out of the machine." << endl;
}

void VendingMachine::takeOutCoin()
{
	if (state == SOLD_OUT)
		cout << "Impossible to take out coin, The machine is sold out." << endl;
	else if (state == NO_COIN)
		cout << "Impossible to take out coin, You haven't inserted a coin." << endl;
	else if (state == HAS_COIN) 
	{
		state = NO_COIN;
		cout << "Coin returned : " << coin << endl;
		coin = 0;
	}
	else if (state == SOLD) 
		cout << "Impossible to take out coin, The coke is comming out of the machine." << endl;
}

void VendingMachine::pressButton()
{
	if (state == SOLD_OUT)
		cout << "Impossible to press button, The machine is sold out." << endl;
	else if (state == NO_COIN) 
		cout << "Impossible to press button, You have no coins." << endl;
	else if (state == HAS_COIN)
	{
		if (coin >= PRICE)
		{
			cout << "Purchase of coke successful." << endl;
			coin -= PRICE;
			state = SOLD;
			dispense();
		}
		else
			cout << "Impossible to press button, Not enough coins." << endl;
	}
	else if (state == SOLD) 	
		cout << "Impossible to press button, The coke is comming out of the machine." << endl;
}

void VendingMachine::dispense()
{
	if (state == SOLD_OUT) 
		cout << "Impossible to dispense, The machine is sold out." << endl;
	else if (state == NO_COIN) 
		cout << "Impossible to dispense, You haven't inserted a coin." << endl;
	else if (state == HAS_COIN) 
		cout << "Impossible to dispense, Not enough coins." << endl;
	else if (state == SOLD) 
	{
		coke -= 1;
		cout << "The vending machine dispenses coke." << endl;
		cout << "Num of Coke is : " << coke << endl;
		cout << "Current coin is : " << coin << endl;
		
		state = coin == 0 ? NO_COIN : HAS_COIN;		

		if (coke == 0) 
		{
			cout << "Out of coke!" << endl;
			takeOutCoin();
			state = SOLD_OUT;
		}			
	}
	
}

ostream& operator<<(ostream& os, VendingMachine* vm)
{	
	os << endl;
	os << "Num of Coke: " << vm->coke << endl;
	os << "VendingMachine state is ";
	
	if (vm->state == VendingMachine::SOLD_OUT) os << "sold out.";
	else if (vm->state == VendingMachine::NO_COIN) os << "no coin.";	
	else if (vm->state == VendingMachine::HAS_COIN) os << "has coin : " << vm->coin; 
	else if (vm->state == VendingMachine::SOLD) os << "sold.";
	
	os << endl;

	return os;
}

int main(void)
{
	VendingMachine* vendingMachine = new VendingMachine(3);

	cout << vendingMachine << endl;

	vendingMachine->insertCoin(500);
	vendingMachine->insertCoin(1000);
	vendingMachine->pressButton();

	cout << vendingMachine << endl;

	vendingMachine->takeOutCoin();
	vendingMachine->pressButton();

	cout << vendingMachine << endl;

	vendingMachine->insertCoin(1000);
	vendingMachine->insertCoin(1500);
	vendingMachine->pressButton();
	vendingMachine->pressButton();	
	vendingMachine->pressButton();

	cout << vendingMachine << endl;

	return 0;
}


스테이트 패턴 적용

 

스테이트 패턴을 이용하면 상태를 별도의 클래스로 캡슐화해서 객체에게 메서드를 위임한다.

Context 클래스(= Vending Machine)에는 여러 가지 내부 상태가 들어있다.

Context 내부의 메서드에서 상태 객체의 메서드가 호출된다.

 

 

자판기가 하는 일(insert, take out, press btn, dispense)을 State 인터페이스에 정의한다.

 

그리고 상태가 바뀌면 각 상태 클래스에 정의된 메서드가 실행되면서, 클래스가 바뀌는 것과 같은 결과를 얻게 된다.


구현

 

먼저 자판기의 모든 메서드를 처리할 수 있는 State 인터페이스를 정의한다.

class State
{
	friend ostream& operator<<(ostream &os, const State* state);
public:
	virtual ~State() = default;
	virtual void insertCoin(int c) = 0;
	virtual void takeOutCoin() = 0;
	virtual void pressButton() = 0;
	virtual void dispense() = 0;

protected:
	virtual void toString(ostream &) const = 0;
};

 

자판기는 각 상태 클래스를 가져야 하고, 상태를 변경할 수 있는 메서드를 제공해야 한다.

class VendingMachine
{
	friend ostream& operator<<(ostream &os, VendingMachine* vm);
    
public:
	VendingMachine() = default;
	VendingMachine(int n);

	static constexpr int PRICE = 1000; // 콜라의 가격

	void insertCoin(int c);
	void takeOutCoin(); // 동전 빼기
	void pressButton(); // 콜라 버튼
	void dispense(); // 콜라 내보내기

	int getCoke() { return coke; }
	void setCoke(int c) { coke = c; }
	int getCoin() { return coin; }
	void setCoin(int c) { coin = c; }

	State* getState() const { return state; }
	void setState(State* s) { state = s; }
	State* getSoldOutState() const { return soldOutState.get(); }
	State* getNoCoinState() const { return noCoinState.get(); }
	State* getHasCoinState() const { return hasCoinState.get(); }
	State* getSoldState() const { return soldState.get(); }
    
private:
	State* state;
	unique_ptr<State> soldOutState;
	unique_ptr<State> noCoinState;
	unique_ptr<State> hasCoinState;
	unique_ptr<State> soldState;

	int coke = 0; // 콜라의 개수
	int coin = 0; // 현재 자판기의 동전
};

 

이전에 각 상태별로 if 문으로 처리한 구문을 이제 해당 상태 클래스(Concrete State)에 구현한다.

예를 들어 SoldState는 아래와 같다.

class SoldState : public State
{
public:
	SoldState() = default;
	SoldState(VendingMachine* vm) : vendingMachine(vm) {}
	void insertCoin(int c) override { cout << "Impossible to insert coin, The coke is comming out of the machine." << endl; }
	void takeOutCoin() override { cout << "Impossible to take out coin, The coke is comming out of the machine." << endl; }
	void pressButton() override { cout << "Impossible to press button, The coke is comming out of the machine." << endl; }

	void dispense() override 
	{ 
		int coke = vendingMachine->getCoke();
		int coin = vendingMachine->getCoin();

		vendingMachine->setCoke(coke - 1);

		cout << "The vending machine dispenses coke." << endl;
		cout << "Num of Coke is : " << vendingMachine->getCoke() << endl;
		cout << "Current coin is : " << coin << endl;

		vendingMachine->setState(
			coin == 0 ? vendingMachine->getNoCoinState() : vendingMachine->getHasCoinState()
		);

		if (vendingMachine->getCoke() == 0)
		{
			cout << "Out of coke!" << endl;
			vendingMachine->takeOutCoin();
			vendingMachine->setState(vendingMachine->getSoldOutState());			
		}
	}
    
protected:
	void toString(ostream &os) const override { os << "sold."; }
    
private:
	VendingMachine* vendingMachine;
};

 

마지막으로 VendingMachine이 하던 일은 각 상태 클래스에 위임한다.

VendingMachine::VendingMachine(int n) :
	soldOutState(make_unique<SoldOutState>(this)), noCoinState(make_unique<NoCoinState>(this)),
	hasCoinState(make_unique<HasCoinState>(this)), soldState(make_unique<SoldState>(this)),
	coke(n)
{
	state = n > 0 ? noCoinState.get() : soldOutState.get();
}

// this is for State (forward declaration)
void VendingMachine::insertCoin(int c) { state->insertCoin(c); }
void VendingMachine::takeOutCoin(){ state->takeOutCoin(); }
void VendingMachine::pressButton() { state->pressButton(); }
void VendingMachine::dispense() { state->dispense(); }

 

전체 코드는 다음과 같으며, 이전 코드와 동일한 결과를 얻는다.

#include <iostream>
#include <string>
#include <memory>

using namespace std;

class State;

class VendingMachine
{
	friend ostream& operator<<(ostream &os, VendingMachine* vm);
    
public:
	VendingMachine() = default;
	VendingMachine(int n);

	static constexpr int PRICE = 1000; // 콜라의 가격

	void insertCoin(int c);
	void takeOutCoin(); // 동전 빼기
	void pressButton(); // 콜라 버튼
	void dispense(); // 콜라 내보내기

	int getCoke() { return coke; }
	void setCoke(int c) { coke = c; }
	int getCoin() { return coin; }
	void setCoin(int c) { coin = c; }

	State* getState() const { return state; }
	void setState(State* s) { state = s; }
	State* getSoldOutState() const { return soldOutState.get(); }
	State* getNoCoinState() const { return noCoinState.get(); }
	State* getHasCoinState() const { return hasCoinState.get(); }
	State* getSoldState() const { return soldState.get(); }
    
private:
	State* state;
	unique_ptr<State> soldOutState;
	unique_ptr<State> noCoinState;
	unique_ptr<State> hasCoinState;
	unique_ptr<State> soldState;

	int coke = 0; // 콜라의 개수
	int coin = 0; // 현재 자판기의 동전
};

/* ---------------- State ---------------- */

class State
{
	friend ostream& operator<<(ostream &os, const State* state);
    
public:
	virtual ~State() = default;
	virtual void insertCoin(int c) = 0;
	virtual void takeOutCoin() = 0;
	virtual void pressButton() = 0;
	virtual void dispense() = 0;

protected:
	virtual void toString(ostream &) const = 0;
};

ostream& operator<<(ostream &os, const State* state)
{
	state->toString(os);
	return os;
}

class SoldOutState : public State
{
public:
	SoldOutState() = default;
	SoldOutState(VendingMachine* vm) : vendingMachine(vm) {}
	void insertCoin(int c) override { cout << "Impossible to insert coin, The machine is sold out." << endl; }
	void takeOutCoin() override { cout << "Impossible to take out coin, The machine is sold out." << endl; }
	void pressButton() override { cout << "Impossible to press button, The machine is sold out." << endl; }
	void dispense() override { cout << "Impossible to dispense, The machine is sold out." << endl; }
    
protected:
	void toString(ostream &os) const override { os << "sold out."; }
    
private:
	VendingMachine* vendingMachine;
};

class NoCoinState : public State
{
public:
	NoCoinState() = default;
	NoCoinState(VendingMachine* vm) : vendingMachine(vm) {}

	void insertCoin(int c) override 
	{ 
		vendingMachine->setState(vendingMachine->getHasCoinState()); 
		vendingMachine->setCoin(vendingMachine->getCoin() + c);
		cout << "Insert coin, current coin is : " << vendingMachine->getCoin() << endl;
	}

	void takeOutCoin() override { cout << "Impossible to take out coin, You haven't inserted a coin." << endl; }
	void pressButton() override { cout << "Impossible to press button, You have no coins." << endl; }
	void dispense() override { cout << "Impossible to dispense, You haven't inserted a coin." << endl; }

protected:
	void toString(ostream &os) const override { os << "no coin."; }

private:
	VendingMachine* vendingMachine;
};

class HasCoinState : public State
{
public:
	HasCoinState() = default;
	HasCoinState(VendingMachine* vm) : vendingMachine(vm) {}

	void insertCoin(int c) override 
	{
		vendingMachine->setCoin(vendingMachine->getCoin() + c);
		cout << "Insert coin, current coin is : " << vendingMachine->getCoin() << endl;
	}

	void takeOutCoin() override 
	{		
		cout << "Coin returned : " << vendingMachine->getCoin() << endl;
		vendingMachine->setCoin(0);
		vendingMachine->setState(vendingMachine->getNoCoinState());
	}

	void pressButton() override 
	{  
		int coin = vendingMachine->getCoin();

		if (coin >= VendingMachine::PRICE)
		{
			cout << "Purchase of coke successful." << endl;
			vendingMachine->setCoin(coin - VendingMachine::PRICE);
			
			vendingMachine->setState(vendingMachine->getSoldState());
			vendingMachine->dispense();
		}
		else
			cout << "Impossible to press button, Not enough coins." << endl;
	}

	void dispense() override { cout << "Impossible to dispense, Not enough coins." << endl; }

protected:
	void toString(ostream &os) const override { os << "has coin : " << vendingMachine->getCoin(); }

private:
	VendingMachine* vendingMachine;
};

class SoldState : public State
{
public:
	SoldState() = default;
	SoldState(VendingMachine* vm) : vendingMachine(vm) {}
	void insertCoin(int c) override { cout << "Impossible to insert coin, The coke is comming out of the machine." << endl; }
	void takeOutCoin() override { cout << "Impossible to take out coin, The coke is comming out of the machine." << endl; }
	void pressButton() override { cout << "Impossible to press button, The coke is comming out of the machine." << endl; }

	void dispense() override 
	{ 
		int coke = vendingMachine->getCoke();
		int coin = vendingMachine->getCoin();

		vendingMachine->setCoke(coke - 1);

		cout << "The vending machine dispenses coke." << endl;
		cout << "Num of Coke is : " << vendingMachine->getCoke() << endl;
		cout << "Current coin is : " << coin << endl;

		vendingMachine->setState(
			coin == 0 ? vendingMachine->getNoCoinState() : vendingMachine->getHasCoinState()
		);

		if (vendingMachine->getCoke() == 0)
		{
			cout << "Out of coke!" << endl;
			vendingMachine->takeOutCoin();
			vendingMachine->setState(vendingMachine->getSoldOutState());			
		}
	}

protected:
	void toString(ostream &os) const override { os << "sold."; }

private:
	VendingMachine* vendingMachine;
};

/* --------------------------------------- */

VendingMachine::VendingMachine(int n) :
	soldOutState(make_unique<SoldOutState>(this)), noCoinState(make_unique<NoCoinState>(this)),
	hasCoinState(make_unique<HasCoinState>(this)), soldState(make_unique<SoldState>(this)),
	coke(n)
{
	state = n > 0 ? noCoinState.get() : soldOutState.get();
}

// this is for State (forward declaration)
void VendingMachine::insertCoin(int c) { state->insertCoin(c); }
void VendingMachine::takeOutCoin(){ state->takeOutCoin(); }
void VendingMachine::pressButton() { state->pressButton(); }
void VendingMachine::dispense() { state->dispense(); }

ostream& operator<<(ostream& os, VendingMachine* vm)
{
	os << endl;
	os << "Num of Coke: " << vm->coke << endl;
	os << "VendingMachine state is ";
	os << vm->getState() << endl;

	return os;
}

int main(void)
{
	VendingMachine* vendingMachine = new VendingMachine(3);

	cout << vendingMachine << endl;

	vendingMachine->insertCoin(500);
	vendingMachine->insertCoin(1000);
	vendingMachine->pressButton();

	cout << vendingMachine << endl;

	vendingMachine->takeOutCoin();
	vendingMachine->pressButton();

	cout << vendingMachine << endl;

	vendingMachine->insertCoin(1000);
	vendingMachine->insertCoin(1500);
	vendingMachine->pressButton();
	vendingMachine->pressButton();	
	vendingMachine->pressButton();

	cout << vendingMachine << endl;

	return 0;
}


ClassDiagram.md

```mermaid
  classDiagram    
    class State { 
      handle1()*
      handle2()*     
    }
    class Context { 
      execute()
    }
    class ConcreteStateA { 
      handle1()
      handle2()          
    }
    class ConcreteStateB { 
      handle1()
      handle2()          
    }
    
    
    <<Interface>> State
    State <-- Context    
    State <|.. ConcreteStateA
    State <|.. ConcreteStateB  
   
   note for Context "execute() => state->handle()"
```

 

 

 

 

 

 

반응형