소프트웨어공학/디자인 패턴

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

javapp 자바앱 2021. 12. 16. 00:00
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 : 상태 기반 동작 및 현재 상태로 위임 동작 캡슐화, 메소드 내에 알고리즘 골격을 정의한다.