본문 바로가기
소프트웨어공학/디자인 패턴

State Pattern 상태 패턴 - 행위, 내부 상태를 가져서 특정 상태에서의 행동들을 하나의 class에 캡슐화 | Design pattern 디자인 패턴

by javapp 자바앱 2021. 12. 16.
728x90

 

 

 

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 문 추가 / 제거 해야됨
    }
    
    // 메소드들...
    // ...
}

** 상태를 추가/제거 시 caseGumballMachine 클래스에 추가/제거 해야된다(변경된다.)

다른 행동들 또한 영향을 받는다.

 

 


 
 

상태 중심 설계

특정 상태에서의 행동들을 하나의 클래스에 캡슐화

 

상태 중심 설계
 

 

 

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());
        }
    }
}
Guard condition
  • 상태 전이 조건, 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 : 상태 기반 동작 및 현재 상태로 위임 동작 캡슐화, 메소드 내에 알고리즘 골격을 정의한다.

 

 

 

 

 

 

 

 

 

댓글