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) 를 참고하여 공부하고 작성하였음