상태 패턴은 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 하는 행동 디자인 패턴입니다.
- 상태 패턴은
Finite state machine(유한 상태 머신)
개념과 밀접하게 연관되어 있다- '특정 시간에 프로그램이 속해 있을 상태의 갯수는 유한하다' 가 핵심 개념
- 어떤 고유한 상태 내에서 프로그램은 다르게 행동함
- 프로그램의 현재 상태에 따라 특정 다른 상태로 전환되거나 전환되지 않을 수 있음
- 이때 상태의 전환을
Transition(전이)
이라 부름
- 일반적으로 if/switch 문을 이용해 각 상태에 대한 로직을 구현한다
- 하지만 클래스에 상태에 의존하는 행동을 추가할수록 단점 발생
- 클래스의 메소드 대부분에 거대한 조건문이 들어가게 됨
- 유지보수가 어려워짐
- 상태 패턴은 객체의 가능한 상태에 대해 클래스를 만들고, 모든 상태별 행동들을 이러한 클래스로 추출하는 것을 제안한다
Context
라는 객체가 상태를 매니징하는 역할을 함- Context 객체는 모든 행동을 직접 구현하지 않음
- 대신 상태 객체에 대한 참조를 가지고, 실제 작업은 상태 객체에 위임
- Context: Concrete state 중 하나에 대한 참조를 저장하고, 모든 State 별 작업을 이곳에 위임
- State 인터페이스를 통해 State 객체와 통신함
- State 객체 변경을 위한 세터 메소드를 노출
- State: 상태별 메소드들을 선언하는 인터페이스
- 이 메소드들은 모든 Concrete state 에서 호출가능해야 함
- Concrete state: 상태별 메소드들에 대한 자체적인 구현을 제공
- 중간 레이어의 추상화된 클래스 구현으로 상태간 중복을 줄일 수도 있음
Context 객체가 Concrete state 객체들의 참조를 저장하고 있지만, Concrete state 객체들도 상태의 전이를 위해 Context 객체의 역참조를 가질 수 있다
// Context class
// state에 대한 참조를 가지며, 실제 동작은 state 에 위임
class Context {
private state!: State;
public setState(state: State): void {
this.state = state;
}
public actionA(): void {
this.state.actionA();
}
public actionB(): void {
this.state.actionB();
}
}
// State interface
// 상태별 메소드들을 정의하는 인터페이스
interface State {
actionA(): void;
actionB(): void;
}
// Concrete state class
// 상태별 메소드들의 자체적인 구현 제공
class StateA implements State {
constructor(private context: Context) {}
public actionA(): void {
console.log("action A");
}
public actionB(): void {
// does nothing
}
}
class StateB implements State {
constructor(private context: Context) {}
public actionA(): void {
// does nothing
}
public actionB(): void {
console.log("transition to state A");
this.context.setState(new StateA(this.context));
}
}
class Application {
private context: Context;
constructor() {
this.context = new Context();
this.context.setState(new StateB(this.context));
}
public run(): void {
this.context.actionA();
this.context.actionB();
this.context.actionA();
}
static main(): void {
const app = new Application();
app.run();
}
}
Application.main();
// output:
// transition to state A
// action A
class Player {
private state: State;
time: number = 0;
track: number = 0;
paused: boolean = false;
playButton: HTMLButtonElement;
ui: HTMLInputElement;
intervalId: NodeJS.Timeout | null = null;
constructor(playButton: HTMLButtonElement, slider: HTMLInputElement) {
this.playButton = playButton;
this.ui = slider;
}
public setState(state: State): void {
this.state = state;
}
public playOrPause(): void {
this.state.playOrPause();
}
public next(): void {
this.state.next();
}
public prev(): void {
this.state.prev();
}
}
interface State {
playOrPause(): void;
next(): void;
prev(): void;
}
abstract class BaseState implements State {
constructor(protected player: Player) {}
public updateUIs(): void {
this.player.playButton.textContent = this.player.paused ? "Play" : "Pause";
this.player.ui.value = this.player.time.toString();
}
public abstract playOrPause(): void;
public next(): void {
this.player.track++;
this.player.time = 0;
this.updateUIs();
}
public prev(): void {
if (this.player.time < 5) {
this.player.track--;
this.player.time = 0;
} else {
this.player.time = 0;
}
this.updateUIs();
}
}
class PlayState extends BaseState implements State {
constructor(player: Player) {
super(player);
}
public playOrPause(): void {
this.player.paused = true;
this.player.setState(new PauseState(this.player));
clearInterval(this.player.intervalId as NodeJS.Timeout);
this.updateUIs();
}
}
class PauseState extends BaseState implements State {
constructor(player: Player) {
super(player);
}
public playOrPause(): void {
this.player.paused = false;
this.player.setState(new PlayState(this.player));
this.player.intervalId = setInterval(() => {
this.player.time++;
this.updateUIs();
}, 1000);
}
}
class Application {
private player: Player;
constructor() {
const playButton = document.getElementById("play") as HTMLButtonElement;
const nextButton = document.getElementById("next")!;
const prevButton = document.getElementById("prev")!;
const slider = document.getElementById("slider") as HTMLInputElement;
this.player = new Player(playButton, slider);
this.player.setState(new PauseState(this.player));
const playOrPause = () => this.player.playOrPause();
const next = () => this.player.next();
const prev = () => this.player.prev();
playButton.addEventListener("click", playOrPause);
nextButton.addEventListener("click", next);
prevButton.addEventListener("click", prev);
}
}
new Application();
- 상태에 따라 다르게 행동하는 객체가 있을 때, 상태들의 수가 많고 상태간 전이가 자주 발생할때 사용한다
- 클래스의 필드들의 현재 값들에 따라 클래스가 행동하는 방식을 변경하는 거대한 조건문들로 클래스가 오염될 때 사용한다
- 유사한 상태들에 중복 코드와 조건문 기반 상태 머신의 전이가 많을 때 사용한다