12. State Pattern
2021. 10. 15. 21:14ㆍComputer Sciences/Design Patterns
문제
한 뽑기 기계가 있다. 이 뽑기 기계는 다음 상태 다이어그램으로 동작한다.
이 다이어그램을 바탕으로 뽑기 기계를 코드로 구현해보자.
먼저 상태를 살펴보자. 동전이 없는 상태, 있는 상태, 캡슐이 판매된 상태, 매진된 상태가 있다. 이를 클래스 변수로 만들면 다음과 같다.
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; // 현재 상태를 저장하기 위한 변수
동전이 투입, 반환되고 있는 상태에서 손잡이를 돌리면 캡슐이 나간다.
// 동전을 투입하는 행위
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("동전이 이미 들어있습니다.");
} else if (state == SOLD_OUT) {
System.out.println("매진되었습니다.");
} else if (state == SOLD) {
System.out.println("캡슐이 나가고 있습니다. 기다려 주세요.");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("동전이 투입되었습니다.");
}
}
이런 식으로 코드를 구현하면 아래와 같은 클래스로 작성할 수 있다.
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; // 현재 상태를 저장하기 위한 변수
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("동전이 이미 들어있습니다.");
} else if (state == SOLD_OUT) {
System.out.println("매진되었습니다.");
} else if (state == SOLD) {
System.out.println("캡슐이 나가고 있습니다. 기다려 주세요.");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("동전이 투입되었습니다.");
}
}
// 동전을 반환하는 행위
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("동전이 반환됩니다.");
state = NO_QUARTER;
} else if (state == SOLD_OUT) {
System.out.println("동전이 없습니다.");
} else if (state == SOLD) {
System.out.println("이미 캡슐을 뽑으셨습니다.");
} else if (state == NO_QUARTER) {
System.out.println("동전이 없습니다.");
}
}
public void turnCrank() {
if (state == HAS_QUARTER) {
System.out.println("손잡이를 돌리셨습니다. 곧 캡슐이 나갑니다.");
state = SOLD;
dispense();
} else if (state == SOLD_OUT) {
System.out.println("매진되었습니다.");
} else if (state == SOLD) {
System.out.println("손잡이는 한 번만 돌려주세요.");
} else if (state == NO_QUARTER) {
System.out.println("동전을 넣어주세요.");
}
}
public void dispense() {
if (state == HAS_QUARTER) {
System.out.println("캡슐이 나갈 수 없습니다.");
} else if (state == SOLD_OUT) {
System.out.println("매진되었습니다.");
} else if (state == SOLD) {
System.out.println("캡슐이 나가고 있습니다.");
count -= 1;
if (count == 0) {
System.out.println("더 이상 캡슐이 없습니다.");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
}
} else if (state == NO_QUARTER) {
System.out.println("동전을 넣어주세요.");
}
}
}
뭔가 느낌이 이상하지 않은가? 지금까지 경험에 미루어 보아 기능 추가/삭제 시 코드 변화가 매우 커질 것으로 예상된다. 그 느낌은 정확하다. 만약 여기에서 상태가 추가되면 상태를 사용하는 모든 메서드를 수정해야 한다. 필요가 없어진 상태를 지울 때도 마찬가지다. 이를 어떻게 해결하면 좋을까?
먼저 static
변수로 사용하는 것들을 State
라는 인터페이스로 만들자. 그리고 각 상태를 State
를 구현한 클래스로 사용해보자. 이를 바탕으로 클래스 다이어그램을 그리면 아래와 같다.
이를 바탕으로 코드를 다시 구현해보자.
public interface State {
void insertQuarter();
void ejectQuarter();
void turnCrank();
void dispense();
}
public class GumballMachine {
State soldState;
State soldOutState;
State noQuarterState;
State hasQuarterState;
State state = soldOutState;
int count = 0;
public GumballMachine(int numberOfGumballs) {
soldState = new SoldState(this);
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
this.count = numberOfGumballs;
if (numberOfGumballs > 0) {
state = noQuarterState;
}
}
public void insertQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrankQuarter() {
state.turnCrank();
}
public void dispense() {
state.dispense();
}
public void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count != 0) {
count -= 1;
}
}
// 각 상태에 대한 게터 메서드 및 기타 메서드
}
public class SoldState implements State {
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("캡슐이 나가고 있습니다. 기다려 주세요.");
}
@Override
public void ejectQuarter() {
System.out.println("이미 캡슐을 뽑으셨습니다.");
}
@Override
public void turnCrank() {
System.out.println("손잡이는 한 번만 돌려주세요.");
}
@Override
public void dispense() {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("더 이상 캡슐이 없습니다.");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
public class SoldOutState implements State {
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("매진되었습니다.");
}
@Override
public void ejectQuarter() {
System.out.println("매진되었습니다.");
}
@Override
public void turnCrank() {
System.out.println("매진되었습니다.");
}
@Override
public void dispense() {
System.out.println("매진되었습니다.");
}
}
public class NoQuarterState implements State {
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("동전이 입력되었습니다.");
gumballMachine.setState(gumballMachine.getHasQuarterState());
}
@Override
public void ejectQuarter() {
System.out.println("동전이 없습니다.");
}
@Override
public void turnCrank() {
System.out.println("동전을 넣어주세요.");
}
@Override
public void dispense() {
System.out.println("캡슐이 나갈 수 없습니다.");
}
}
public class HasQuarterState implements State {
GumballMachine gumballMachine;
public SoldState(GumballMachine gumballMachine) {
this.gumballMachine = gumballMachine;
}
@Override
public void insertQuarter() {
System.out.println("동전이 이미 들어있습니다.");
}
@Override
public void ejectQuarter() {
System.out.println("동전이 반환되었습니다.");
gumballMachine.setState(gumballMachine.getNoQuarterState());
}
@Override
public void turnCrank() {
System.out.println("손잡이를 돌리셨습니다. 곧 캡슐이 나갑니다.");
gumballMachine.setState(gumballMachine.getSoldState());
}
@Override
public void dispense() {
gumballMachine.releaseBall();
if (gumballMachine.getCount() > 0) {
gumballMachine.setState(gumballMachine.getNoQuarterState());
} else {
System.out.println("더 이상 캡슐이 없습니다.");
gumballMachine.setState(gumballMachine.getSoldOutState());
}
}
}
지금까지 한 일을 정리하자.
- 각 상태의 행동을 각각의 클래스로 만들었다.
- 복잡한 if 문을 모두 없앴다.
- 각 상태에 대해 변화에는 닫혀 있고 확장에는 열려 있도록 변경했다(OCP 준수).
- 처음 설계했던 상태 다이어그램에 훨씬 가까우면서 더 이해하기 좋은 코드와 클래스 구조를 만들었다.
스테이트 패턴
객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있는 패턴이다. 마치 객체의 클래스가 바뀌는 것과 같은 결과를 얻을 수 있다.
Context
- 여러 가지 상태를 가지고 있으며 각 상태에 따라 동작이 수행되도록 구현한다.
GumballMachine
에 해당한다.request()
을 호출하면 현재 상태에 해당하는handle()
이 호출된다.
State
- 모든 상태가 구현해야 할 공통 인터페이스를 정의한다.
State
에 해당한다.
ConcreteStateA
,ConcreteStateB
State
를 구현한 클래스이다.- 각 상태에 따라 다른 행위를 작성한다.
'Computer Sciences > Design Patterns' 카테고리의 다른 글
14. Chain of Responsibility Pattern (0) | 2021.11.14 |
---|---|
13. Bridge Pattern (0) | 2021.11.14 |
11. Composite Pattern (0) | 2021.10.15 |
10. Iterator Pattern (0) | 2021.10.15 |
9. Template Method Pattern (0) | 2021.10.14 |