상태패턴은 무엇이며, 상태패턴을 사용하는 상황을 소개하고 실제 코드에 적용한 내용을 정리하기 위한 포스트 입니다.
전략 패턴(State Pattern)은 행위패턴의 한 종류로 동일한 메소드를 객체 상태에 따라 다른 행위로 사용할 때 사용된다.
즉 객체가 상태에 따라 행위를 달리하는 상황에서,
자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고,
상태를 객체화 하여 상태가 행동을 할 수 있도록 위임하는 패턴이다.
이전에 소개한 전략패턴은 외부에서 전략을 주입해 줬어야 한다면 상태패턴은 State로직의 끝에 setState()메소드를 통해 State를 스스로 변환이 가능하다.
보통 다양한 상태가 있고 이를 구분할때 사용하는 if, else문의 복잡한 분기점을 제거하기위해 사용한다.
그예로 게임 캐릭터의 방향키가 게임 캐릭터의 상태에 따라 회전을 할 수도, 이동을 할 수도 있다.
노트북의 전원 버튼은 노트북의 상태에따라 노트북을 켤 수도, 끌 수도있다.
상태코드를 블랙잭 미션에 적용해 보겠다.
카드는 Blackjack, Bust, Stay, Hit 4가지의 상태를 가지며
Hit을 제외한 나머지는 게임이 종료된 상태이다.
게임이 종료된 상태라면 플레이어는 보상을 획득 할 수 있고
게임이 진행 중인 상태라면 카드를 뽑을 수 있다.
위와 같이 상태에따라 메서드의 내용이 달라지는데 자세한건 코드에서 설명 하겠다.
위 다이어그램을 보면 모든 상태는 draw(), profit()...등과 같은 기능을 가지는 것을 알 수있다.
하지만 Finished를 상속하는 Stay,Bust,BlackJack은 draw()를 할 수없으며,
Running을 상속하는 Hit은 draw()는 가능하지만 profit()은 불가능 하다.
(위 와같이 상태에 따라 다르게 동작하는 코드들을 if조건문을 사용해 구현하려면 진짜 엄청나게 긴 코드가 필요할 것이다.. 가독성이 떨어지는건 덤)
Hit(Running을 상속)은 Stay 와 Bust로 상태 변환이 가능하다.
하지만 나머지 3개의 상태(Finished를 상속)는 상태의 변환이 불가능 하다.
이제 실제 코드를 보자(불필요 하다고 생각되는 부분은 생략했다.)
public interface State {
State draw(Card card);
State stay();
boolean isFinished();
BunchOfCard getBunchOfCard();
double profit(double betMoney);
}
public abstract class Started implements State {
protected final BunchOfCard bunchOfCard;
protected Started(BunchOfCard bunchOfCard) {
this.bunchOfCard = new BunchOfCard(bunchOfCard);
}
@Override
public BunchOfCard getBunchOfCard() {
return this.bunchOfCard;
}
}
public abstract class Finished extends Started {
private static final String CAN_NOT_DRAW_MESSAGE = "게임이 종료되어 카드를 뽑을 수 없습니다.";
private static final String CAN_NOT_STAY_MESSAGE = "게임이 종료되어 Stay 할 수 없습니다.";
protected Finished(BunchOfCard bunchOfCard) {
super(bunchOfCard);
}
abstract double earningRate();
@Override
public State draw(Card card) {
throw new IllegalArgumentException(CAN_NOT_DRAW_MESSAGE);
}
@Override
public State stay() {
throw new IllegalArgumentException(CAN_NOT_STAY_MESSAGE);
}
@Override
public boolean isFinished() {
return true;
}
@Override
public double profit(double betMoney) {
return 1 * earningRate();
}
}
public class Stay extends Finished {
public Stay(BunchOfCard bunchOfCard) {
super(bunchOfCard);
}
@Override
double earningRate() {
return 1;
}
}
public class Bust extends Finished {
public Bust(BunchOfCard bunchOfCard) {
super(bunchOfCard);
}
@Override
double earningRate() {
return -1;
}
}
public class Hit extends Running {
public Hit(BunchOfCard bunchOfCard) {
super(bunchOfCard);
}
@Override
public State draw(Card card) {
//카드를 뽑는다.
bunchOfCard.addCard(card);
//카드를 뽑은 후 Bust 판단
if (bunchOfCard.isBust()) {
return new Bust(bunchOfCard);
}
return new Hit(bunchOfCard);
}
@Override
public State stay() {
return new Stay(bunchOfCard);
}
}
코드를 잘 살펴보면 최대한 코드의 중복을 없애기 위해 abstract Class를 이용해 상속을 활용하는 것을 볼 수있다.
4개의 상태는 Running과 Finished에 따라 다른 기능을 해야하기 때문이다.
처음에는 많이 헷갈렸지만 if조건을 어떻게 하면 최대한 없앨 수 있을까 고민하다보면 전체적인 윤곽이 보일 것 같다.