Design Pattern

13. State Pattern

kangwook 2022. 9. 5. 22:14

State Pattern은 객체가 특정 상태에 따라 행동을 달리하는 상황에서 상태를 객체화하여 상태가 행동할 수 있도록 위임하는 패턴

State Pattern

State Pattern은 객체의 특정 상태를 클래스로 선언하고, 클래스에서는 해당 상태에서 할 수 있는 행위들을 메소드로 정의한다. 그리고 이러한 각 상태 클래스들을 인터페이스로 캡슐화하여 Context에서 인터페이스를 호출한다.

 

  • Context : State를 이용하는 역할을 수행
  • State : 시스템의 모든 상태에 대한 공통 인터페이스를 제공. 이 인터페이스를 구현한 상태 클래스는 서로를 대신해 교체해서 사용할 수 있음
  • ConcreteState : Context객체가 요청한 작업을 자신의 방식으로 실제 실행. 대부분의 경우 다음 상태를 결정해 상태 변경을 Context객체에 요청하는 역할도 수행

State Pattern 예제

  • 문방구에서 이용하는 뽑기 기계(Gumball Machine)에 대한 코드
  • 먼저 간단한 상태 다이어그램을 통해 기계의 상태를 파악

  • No Quarter : 동전 없음
  • Has Quarter : 동전 있음
  • Gumball Sold : 껌볼 판매
  • Out of Gumballs : 매진
  • State Pattern 적용전
    • if문을 통한 구현을 하게 되면 메소드를 호출할 때마다 현재의 state들을 확인하면서 각각 다른 행동을 취하도록 해줘야 함
    • 결국 Gumball Machine 에서는 4개의 행동이 존재하고 각 행동을 할 때마다 4개의 state와 if문 검사를 해야함
    • OCP를 위반
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;
 
    int state = SOLD_OUT;
    int count = 0;
 
    public GumballMachine(int count) {
        this.count = count;
        if(count > 0) {
            state = NO_QUARTER;
        }
    }
 
    public void insertQuarter() {
        if(state == HAS_QUARTER) {
            System.out.println("You can't insert another quarter");
        } else if (state == NO_QUARTER) {
            state = HAS_QUARTER;
            System.out.println("You inserted a quarter");
        } else if (state == SOLD_OUT) {
            System.out.println("You can't insert a quarter, the machine is sold out");
        } else if (state == SOLD) {
            System.out.println("Please wait, we're already giving you a gumball");
        }
  }
 
    public void ejectQuarter() {
        if (state == HAS_QUARTER) {
            System.out.println("Quarter returned");
            state = NO_QUARTER;
        } else if (state == NO_QUARTER) {
        System.out.println("You haven't inserted a quarter");
    } else if (state == SOLD) {
        System.out.println("Sorry, you already turned the crank");
    } else if (state == SOLD_OUT) {
        System.out.println("You can't eject, you haven't inserted a quarter yet");
    }
  }
 
  public void turnCrank() {
    if (state == SOLD) {
        System.out.println("Turning twice doesn't get you another gumball!");
    } else if (state == NO_QUARTER) {
        System.out.println("You turned but there's no quarter");
    } else if (state == SOLD_OUT) {
        System.out.println("You turned, but there are no gumballs");
    } else if (state == HAS_QUARTER) {
        System.out.println("You turned...");
        state = SOLD;
        dispense();
    }
  }
 
  public void dispense() {
    if (state == SOLD) {
      System.out.println("A gumball comes rolling out the slot");
      count = count - 1;
      if (count == 0) {
          System.out.println("Oops, out of gumballs!");
          state = SOLD_OUT;
      } else {
          state = NO_QUARTER;
      }
    } else if (state == NO_QUARTER) {
        System.out.println("You need to pay first");
    } else if (state == SOLD_OUT) {
        System.out.println("No gumball dispensed");
    } else if (state == HAS_QUARTER) {
        System.out.println("No gumball dispensed");
    }
  }
}
  • State Pattern 적용 후
    • 각 state에서 이뤄져야하는 행동은 그 state안에 둬야 함
    • 따라서 state가 바뀌어도 state에서의 행동을 호출하여 수행하면 됨
    • 즉, 현재 상태에 따른 행동은 Gumball Machine이 State 객체에게 위임하도록 함

public interface State {
    public void insertQuarter();
    public void ejectQuarter();
    public void turnCrank();
    public void dispense();
}
public class NoQuarterState implements State {
    GumballMachine gumballMachine;
     
    public NoQuarterState(GumballMAchine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
 
    public void insertQuarter() {
        System.out.println("You inserted a quarter");
        gumballMachine.setState(gumballMachine.getHasQuarterState());
    }
 
    public void ejectQuarter() {
        System.out.tprintln("You haven't inserted a quarter");
    }
 
    public void turnCrank() {
        System.out.println("You turned, but there's no quarter");
    }
 
    public void dispense() {
        Sysetm.out.println("You need to pay first");
    }
}
 
public class HasQuarterState implements State {
    GumballMachine gumballMachine;
     
    public HasQuarterState(GumballMachine gumballMachine) {
        this.gumballMachine = gumballMachine;
    }
 
    public void insertQuarter() {
        System.out.println("You can't insert another quarter");
    }
     
    public void ejectQuarter() {
        System.out.println("Quarter returned");
        gumballMachine.setState(gumballMachine.getNoQuarterState());
    }
 
    public void turnCrank() {
        System.out.println("You turned...");
        gumballMachine.setState(gumballMachine.getSoldState());
    }
 
    public void dispense() {
        System.out.println("No gumball dispensed");
    }
}
  • GumballMachine 클래스
public class GumballMachine {
    State soldOutState;
    State noQuarterState;
    State hasQuarterSTate
    State soldState
 
    State state;
    int count = 0;
 
    public GumballMachine(int numberGumballs) {
        soldOutState = new SoldOutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);
 
        this.count = numberGumballs;
        if (numberGumballs > 0) {
            state = noQuarterState;
        } else {
            state = soldOutState;
        }
    }
 
    public void insertQuarter() {
        state.insertQuarter()
    }
 
    public void ejectQuarter() {
        state.ejectQuarter();
    }
     
    public void turnCrank() {
        state.turnCrank();
        state.dispesne();
    }
 
    void setState(State state) {
        this.state = state;
    }
     
    void releaseBall() {
        System.out.println("A gumball comes rolling out the slot...");
        if (count != 0) {
            count = count - 1;
        }
    }
 
    public State getState() {
      return state;
  }
 
  public State getSoldOutState() {
    return soldOutState;
  }
 
  public State getNoQuarterState() {
    return noQuarterState;
  }
 
  public State getHasQuarterState() {
    return hasQuarterState;
  }
 
  public State getSoldState() {
    return soldState;
  }
}

결론

  • 어떤 행동을 수행할 때, 상태에 맞는 행동을 수행하도록 객체 내부에서 처리하는 행동 패턴(Behavioral Pattern)
  • 장점
    • 상태가 많아지더라도 클래스의 개수는 증가하지만 코드의 복잡도는 증가하지 않기 때문에 유지 보수에 유리
    • 상태에 따른 동작을 구현한 코드가 각 상태 별로 구분되기 때문에 상태별 동작을 수정하기가 쉬움
    • 메소드의 긴 분기문들을 제거할 수 있음
  • 단점
    • 상태 구현 클래스가 많아질수록 상태 변경 규칙을 파악하기 어려움
    • 상태에 따라 변하는 메소드의 숫자가 적다면 오히려 불필요한 복잡성을 추가할 수 있음

 

 


본문은 Head First Design Pattern (2004) 를 참고하여 공부하고 작성하였음