C++ - 상태, 스테이트 패턴 (State Pattern)
Architecture & Design Pattern 전체 링크
참고
상태, 스테이트 패턴 (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()"
```