상태패턴이라고도 불리는 스테이트 패턴은 객체 상태를 캡슐화하여 클래스화함으로써 그것을 참조하게 하는 방식으로 상태에 따라 다르게 처리할 수 있도록 행위 내용을 변경하여, 변경 시 원시 코드의 수정을 최소화할 수 있고, 유지보수의 편의성도 갖는 패턴이다.
즉, 객체지향 설계에서 현실의 사물과 대칭되는 유형의 어떤 것이 아니라, 그것의 상태를 클래스화 함으로써 ‘상태의 변화’를 표현하고, 객체 내부 상태 변경에 따라 객체의 행동을 상태에 특화된 행동들로 분리해내며, 다른 행동에 영향을 주지 않고 새로운 행동을 추가할 수 있게 된다.
전략 패턴(Strategy Pattern)이 '전략 알고리즘'을 클래스로 표현한 패턴이라면, 상태 패턴(State Pattern)은 '객체 상태'를 클래스로 표현한 패턴이라고 보면 된다.
그래서 그런지 상태 패턴의 클래스 다이어그램을 보면 전략 패턴과 매우 유사하다는 점을 볼 수 있다. 왜냐하면 전략 패턴은 전략을 객체화 한거고, 상태 패턴은 상태를 객체화 한것인데 어쨋든 둘다 똑같은 클래스 묶음이기 때문이다.
- State : 상태를 추상화한 고수준 모듈.
- ConcreteState : 구체적인 각각의 상태를 클래스로 표현. State 역할로 결정되는 인터페이스(API)를 구체적으로 구현한다. 다음 상태가 결정되면 Context에 상태 변경을 요청하는 역할도 한다.
- Context : State를 이용하는 시스템. 시스템 상태를 나타내는 State 객체를 합성(composition)하여 가지고 있다. 클라이언트로부터 요청받으면 State 객체에 행위 실행을 위임한다.
여기서 상태 클래스는 싱글톤 클래스로 구성한다.
전략 패턴의 전략 객체 같은 경우 매개 값에 따라 알고리즘 수행 형태가 달라질수 있지만, 상태는 그 객체의 현 폼을 나타내는 것이기 때문에 대부분의 상황에서 유일하게 있어야 한다.
// LaptopContext.java (Context)
public class LaptopContext {
private PowerState powerState;
public LaptopContext() {
powerState = new OffState();
}
public void printCurrentState() {
System.out.println(powerState.toString());
}
public void pushTypeButton() {
powerState.pushTypeButton();
}
public void pushPowerButton() {
powerState.pushPowerButton(this);
}
public void changeState(PowerState powerState) {
this.powerState = powerState;
}
public void setSavingState() {
powerState = new SavingState();
System.out.println("[LaptopContext] putting laptop in saving state...");
}
}
// PowerState.java (State)
public interface PowerState {
void pushPowerButton(LaptopContext context);
void pushTypeButton();
}
// SavingState.java (ConcreteState)
public class SavingState implements PowerState {
@Override
public void pushPowerButton(LaptopContext context) {
System.out.println("[SavingState] turning on laptop...");
context.changeState(new OnState());
}
@Override
public void pushTypeButton() {
throw new IllegalStateException("[SavingState] laptop is currently in saving state...");
}
public String toString() {
return "[SavingState] laptop is in saving state now...";
}
}
// OnState.java (ConcreteState)
public class OnState implements PowerState {
@Override
public void pushPowerButton(LaptopContext context) {
System.out.println("[OnState] turning laptop off...");
context.changeState(new OffState());
}
@Override
public void pushTypeButton() {
System.out.println("[OnState] typing words...");
}
public String toString() {
return "[OnState] laptop is on now...";
}
}
// OffState.java (ConcreteState)
public class OffState implements PowerState {
@Override
public void pushPowerButton(LaptopContext context) {
System.out.println("[OffState] turning laptop on...");
context.changeState(new OnState());
}
@Override
public void pushTypeButton() {
throw new IllegalStateException("[OffState] laptop is currently off");
}
public String toString() {
return "[OffState] laptop is off now...";
}
}
// Client.java (Client)
public class Client {
public static void main(String[] args) {
LaptopContext laptop = new LaptopContext();
laptop.printCurrentState();
laptop.pushPowerButton();
laptop.printCurrentState();
laptop.pushTypeButton();
laptop.setSavingState();
laptop.printCurrentState();
laptop.pushPowerButton();
laptop.printCurrentState();
laptop.pushPowerButton();
laptop.printCurrentState();
}
}
// 실행 결과
[OffState] laptop is off now...
[OffState] turning laptop on...
[OnState] laptop is on now...
[OnState] typing words...
[LaptopContext] putting laptop in saving state...
[SavingState] laptop is in saving state now...
[SavingState] turning on laptop...
[OnState] laptop is on now...
[OnState] turning laptop off...
[OffState] laptop is off now...
노트북의 켜진 상태, 꺼진 상태, 절전모드 상태를 State 인터페이스를 상속받아 구현한다. Client는 LaptopContext를 이용해 State인터페이스의 구현체인 각 상태들에 접근하며 상태를 변경하는 메소드를 동작시키고 각 상태 클래스들은 다시 LaptopContext객체의 State를 변경시킨다.
위 코드는 상태패턴의 형태를 구현했으나 한가지 문제가 있다. 바로 상태를 변경할 때 마다 객체를 생성한다는 점이다.
사용이 끝난 객체들은 JVM에서 Garbage Collecting을 하지만 이 상태들 자체가 상황마다 특수한 것도 아니고 반복적으로 생성 및 GC를 반복하며 자원을 낭비할 이유가 없다. 그러므로 이를 싱글톤 패턴을 사용하도록 수정해 보겠다.
// LaptopContext.java (Context)
public class LaptopContext {
private PowerState powerState;
public LaptopContext() {
powerState = OffState.getInstance();
}
public void printCurrentState() {
System.out.println(powerState.toString());
}
public void pushTypeButton() {
powerState.pushTypeButton();
}
public void pushPowerButton() {
powerState.pushPowerButton(this);
}
public void changeState(PowerState powerState) {
this.powerState = powerState;
}
public void setSavingState() {
powerState = SavingState.getInstance();
System.out.println("[LaptopContext] putting laptop in saving state...");
}
}
// PowerState.java (State)
public interface PowerState {
void pushPowerButton(LaptopContext context);
void pushTypeButton();
}
// SavingState.java (ConcreteState)
public class SavingState implements PowerState {
private static class SavingStateInstance {
private static final SavingState INSTANCE = new SavingState();
}
public static SavingState getInstance() {
return SavingStateInstance.INSTANCE;
}
@Override
public void pushPowerButton(LaptopContext context) {
System.out.println("[SavingState] turning on laptop...");
context.changeState(OnState.getInstance());
}
@Override
public void pushTypeButton() {
throw new IllegalStateException("[SavingState] laptop is currently in saving state...");
}
public String toString() {
return "[SavingState] laptop is in saving state now...";
}
}
// OnState.java (ConcreteState)
public class OnState implements PowerState {
private static class OnStateInstance {
private static final OnState INSTANCE = new OnState();
}
public static OnState getInstance() {
return OnStateInstance.INSTANCE;
}
@Override
public void pushPowerButton(LaptopContext context) {
System.out.println("[OnState] turning laptop off...");
context.changeState(OffState.getInstance());
}
@Override
public void pushTypeButton() {
System.out.println("[OnState] typing words...");
}
public String toString() {
return "[OnState] laptop is on now...";
}
}
// OffState.java (ConcreteState)
public class OffState implements PowerState {
private static class OffStateInstance {
private static final OffState INSTANCE = new OffState();
}
public static OffState getInstance() {
return OffStateInstance.INSTANCE;
}
@Override
public void pushPowerButton(LaptopContext context) {
System.out.println("[OffState] turning laptop on...");
context.changeState(OnState.getInstance());
}
@Override
public void pushTypeButton() {
throw new IllegalStateException("[OffState] laptop is currently off");
}
public String toString() {
return "[OffState] laptop is off now...";
}
}
// Client.java (Client)
public class Client {
public static void main(String[] args) {
LaptopContext laptop = new LaptopContext();
laptop.printCurrentState();
laptop.pushPowerButton();
laptop.printCurrentState();
laptop.pushTypeButton();
laptop.setSavingState();
laptop.printCurrentState();
laptop.pushPowerButton();
laptop.printCurrentState();
laptop.pushPowerButton();
laptop.printCurrentState();
}
}
// 실행 결과
[OffState] laptop is off now...
[OffState] turning laptop on...
[OnState] laptop is on now...
[OnState] typing words...
[LaptopContext] putting laptop in saving state...
[SavingState] laptop is in saving state now...
[SavingState] turning on laptop...
[OnState] laptop is on now...
[OnState] turning laptop off...
[OffState] laptop is off now...
수정 전 방식과 모든 코드가 동일하지만 각 상태 클래스 내부에 private static final로 객체의 인스턴스를 생상하는 메소드를 전역으로 생성하고 public으로 getInstance 메소드를 열어 줌으로써 상태별로 하나의 인스턴스만 존재하도록 수정하였다.