State pattern
The State Pattern allows an object to alter its behavior when its internal state changes.
The object will appear to change its class.
- 규칙에 따라 객체의 상태를 변화시키면서 객체가 할 수 있는 행위를 바꾸는 패턴
- 특정 메소드가 객체의 상태에 따라 다른 기능을 수행
- 객체의 상태에 따라 동일한 루틴에서 다른 행동을 할 수 있다.
용어
내부 상태
프로그램의 실행 과정에서 현재 유지하고 있는 변수 및 실행 상황.
내부 상태를 가지는 머신
자판기, 엘레배이터, ATM 기기
내부 상태를 가지는 머신은
현재 상태에 따라 같은 행동(사건) 이 다른 결과를 낳을 수 있다.
ex) 자판기내 음료수 수에 따라 음료가 나오거나 부족
cf) function : 내부 상태가 없다. , 지역 변수는 상태를 나타내는 것이 아니다.
유한 상태 머신 FSM
- 한 순간 오직 하나의 상태(상태집합)를 가진다.
- 한 상태에서 다른 상태로 전이될 수 있다.
- 상태전이는 외부입력(이벤트)에 의해 행동/동작한다.
ex) 뽑기 머신
상태 전이 = 상태 – 행동 – 가드 --> 상태 결정
판매 X 알배출 X 알>0 → 동전없음
판매 X 알배출 X 알=0 → 매진
상태 행동 가드 -> 상태
행동 중심 설계 (지양)
각 행동을 하나의 메소드로 나타냄
package state;
public class GumballMachine {
// final static int SOLD_OUT = 0;
// final static int NO_QUARTER = 1;
// final static int HAS_QUARTER = 2;
// final static int SOLD = 3;
enum STATE {SOLD_OUT, NO_COIN, HAS_COIN, SOLD}
STATE currentState = STATE.SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
currentState = STATE.NO_COIN;
}
}
public void insertQuarter() {
if (currentState == STATE.HAS_COIN) {
System.out.println("“You can’t insert another quarter”");
} else if (currentState == STATE.HAS_COIN) {
currentState = STATE.HAS_COIN;
System.out.println("“You inserted a quarter”");
} else if (currentState == STATE.SOLD_OUT) {
System.out.println("“You can’t insert a quarter, the machine is sold out”");
} else if (currentState == STATE.SOLD) {
System.out.println("“Please wait, we’re already giving you a gumball”");
}
}
public void turnCrank()
{ //손잡이돌림(외부)
switch (currentState) {
case STATE.HAS_COIN:
currentState = STATE.SOLD;
dispense(); // 알 배출(내부행동)
break;
case STATE.NO_COIN:
System.out.println("“오류: 동전을 넣으시오”");
break;
case STATE.SOLD:
System.out.println("오류:한번만 돌리시오.");
break;
case STATE.SOLD_OUT:
System.out.println("매진입니다.");
break;
}
// case 문 추가 / 제거 해야됨
}
// 메소드들...
// ...
}
** 상태를 추가/제거 시 case를 GumballMachine 클래스에 추가/제거 해야된다(변경된다.)
다른 행동들 또한 영향을 받는다.
상태 중심 설계
특정 상태에서의 행동들을 하나의 클래스에 캡슐화
object(context)는 자신의 내부 상태를 변경함으로서 행동을 변경할 수 있음.
- 유한상태객체(Finite state machine, Context)로부터 상태를 분리하고 행동을 상태에게 위임함.
- 다른 상태 객체는 다른 행동을 수행함.
새로운 상태를 추가하려면 상태 클래스 구현하고 State선언 getXXX 추가
타 클래스 메소드에 영향을 주지 않는다.
- Context는 많은 내부 상태를 가지고 있다.
- State 인터페이스는 모든 Concrete states 를 위한 공통 인터페이스를 정의
- ConcreteStates는 Context 로 부터 요청들을 다룬다. Context의 상태가 변할 때 그에 따라 행동도 변한다.
예시)
상태 인터페이스
public interface State
{
public void insertQuarter();
public void ejecctQuarter(); // 반환
public void turnCrank();
public void dispense();
}
상태 구현체
동전 없음
// 동전 없음
public class NoQuarterState implements State {
GumballMachine gumballMachine; // 상태 변경을 위해 구성
public NoQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
//행동중심에서 GumballMachine 에 있었던 행동 메소드
@Override
public void insertQuarter() {
System.out.println("동전 삽입");
// 상태 변경
gumballMachine.setState(gumballMachine.getHasQuarterState());
}
@Override
public void ejectQuarter() { //err }
@Override
public void turnCrank() { //err }
@Override
public void dispense() { //err }
}
event trigger : 상태 전이를 야기하는 사건
새로운 상태(추가)에 대해 캡슐된 클래스는 영향을 받지 않는다.
동전 있음
public class HasQuarterState implements State
{
// 구성
GumballMachine gumballMachine;
Random randomWinner = new Random(System.currentTimeMillis());
public HasQuarterState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("“You can’t insert another quarter”");
}
// 돌려 받기
@Override
public void ejectQuarter() {
System.out.println("“Quarter returned”");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
// 돌리기
@Override
public void turnCrank() {
System.out.println("“You turned...”");
// gumballMachine.setState(gumballMachine.getSoldState());
// 코드 추가/변경
int winner = randomWinner.nextInt(10);
if((winner == 0) && (gumballMachine.getCount() > 1)){
gumballMachine.setState(gumballMachine.getWinnerState());
}else{
gumballMachine.setState(gumballMachine.getSoldState());
}
}
@Override
public void dispense() {
System.out.println("“No gumball dispensed”");
}
}
ejectQuarter(), turnCrank() 메소드 활성화
public class SoldState implements State {
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine= gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("“Please wait, we’re already giving you a gumball”");
}
@Override
public void ejectQuarter() {
System.out.println("“Sorry, you already turned the crank”");
}
@Override
public void turnCrank() {
System.out.println("“Turning twice doesn’t get you another gumball!”");
}
// 알배출
@Override
public void dispense() {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("“Oops, out of gumballs!”");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
- 상태 전이 조건, multiple transitions 가능ㅇ
- 판매 x 알배출 x 알개수 > 0 -> 동전없음
- 판매 x 알배출 x 알개수 = 0 -> 매진
public class SoldOutState implements State {
GumballMachine gumballMachine;
public SoldOutState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() { }
@Override
public void ejectQuarter() { }
@Override
public void turnCrank() { }
@Override
public void dispense() { }
}
// 새로운 상태 추가
public class WinnerState implements State
{
GumballMachine gumballMachine;
public WinnerState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() { }
@Override
public void ejectQuarter() { }
@Override
public void turnCrank() { }
// NoQuarterState, SoldOutState 어떤 상태가 될지 결정
@Override
public void dispense() {
System.out.println("“YOU’RE A WINNER! You get two gumballs for your quarter”");
gumballMachine.releaseBall();
if (gumballMachine.getCount() == 0) {
gumballMachine.setState(gumballMachine.getSoldOutState());
} else {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("“Oops, out of gumballs!”");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
}
Context
public class GumballMachine
{
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
// 새로운 상태 추가 부분 1
State winnerState;
// 현재 상태 유지
State currentState = soldOutState;
int count = 0;
public GumballMachine(int numberGumballs)
{
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
// 새로운 상태 추가 부분 2
winnerState = new WinnerState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
currentState = noQuarterState;
}
}
// 메소드 위임을 통해 현재 상태에 따라 실행
public void insertQuarter() {
currentState.insertQuarter();
}
public void ejctQuarter() {
currentState.ejectQuarter();
}
// 손잡이 돌림
public void turnCrank() {
currentState.turnCrank();
currentState.dispense();
}
//알배출
void setState(State state){
this.currentState = state;
}
void releaseBall()
{
System.out.println("A gumball comes rolling out the slot..");
if(count != 0){
count -= 1;
}
}
// getter
// state 객체가 머신의 다음 상태 설정 위해
public State getSoldOutState() {
return soldOutState;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getHasQuarterState() {
return hasQuarterState;
}
public State getSoldState() {
return soldState;
}
public State getCurrentState() {
return currentState;
}
// 새로운 상태 추가 부분 3
public State getWinnerState() {
return winnerState;
}
public int getCount() {
return count;
}
}
새로운 상태 추가시 Context 에 상태 추가 설정만 할 뿐 캡슐화된 상태 구현체에 대해서는 추가적인 변경은 없다.
메인
public class GumballMachineTestDrive {
public static void main(String[] args) {
GumballMachine gumballMachine = new GumballMachine(5);
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
System.out.println(gumballMachine);
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
gumballMachine.insertQuarter();
gumballMachine.turnCrank();
System.out.println(gumballMachine);
}
}
실행 결과
상태 패턴에 대해
GumballMachine 에서 상태는 다음 어떤 상태가 될지 결정한다.
ConcreteStates는 항상 다음 상태를 결정하는가?
그렇지 않다.
- 대안은 상태 전환의 흐름을 컨텍스트가 결정하도록 하는 것이다.
- getter 메소드를 사용함으로써 state class 의존을 최소화한다.
- 클라이언트는 직접적으로 상태들과 상호작용하지 않는다.
- 일반적으로 클라이언트가 컨텍스트를 인식하지 못한 상태에서 컨텍스트를 변경하는 것을 원하지 않는다.
- 상태 객체들간 공유가 가능하다.
- 추상적인 클래스에 넣을 수 있는 공통 기능이 없는 경우 인터페이스를 사용한다. 이렇게 하면 구체적인 상태 구현을 중단하지 않고 나중에 추상 클래스에 메서드를 추가할 수 있다는 이점이 있다.
State, Strategy, Template method 패턴 비교
State : 상태에 따라 상호 호환 가능한 행동을 캡슐화하고 위임 기능을 사용하여 사용할 행동을 결정합니다.
context 자신이 상태 변경을 통해 행동을 변경 -> context이 case 분석의 대안
Strategy : 하위 클래스는 알고리즘의 단계 구현 방법을 결정합니다.
client가 context의 행동을 설정(변경)한다. -> context의 유연한 행동 변경.
Template Method : 상태 기반 동작 및 현재 상태로 위임 동작 캡슐화, 메소드 내에 알고리즘 골격을 정의한다.
댓글