Skip to content

Latest commit

 

History

History
241 lines (191 loc) · 6.57 KB

File metadata and controls

241 lines (191 loc) · 6.57 KB

상태 패턴

상태 패턴은 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 하는 행동 디자인 패턴입니다.

Overview

  • 상태 패턴은 Finite state machine(유한 상태 머신) 개념과 밀접하게 연관되어 있다
    • '특정 시간에 프로그램이 속해 있을 상태의 갯수는 유한하다' 가 핵심 개념
    • 어떤 고유한 상태 내에서 프로그램은 다르게 행동함
    • 프로그램의 현재 상태에 따라 특정 다른 상태로 전환되거나 전환되지 않을 수 있음
    • 이때 상태의 전환을 Transition(전이) 이라 부름
  • 일반적으로 if/switch 문을 이용해 각 상태에 대한 로직을 구현한다
    • 하지만 클래스에 상태에 의존하는 행동을 추가할수록 단점 발생
    • 클래스의 메소드 대부분에 거대한 조건문이 들어가게 됨
    • 유지보수가 어려워짐
  • 상태 패턴은 객체의 가능한 상태에 대해 클래스를 만들고, 모든 상태별 행동들을 이러한 클래스로 추출하는 것을 제안한다
    • Context 라는 객체가 상태를 매니징하는 역할을 함
    • Context 객체는 모든 행동을 직접 구현하지 않음
    • 대신 상태 객체에 대한 참조를 가지고, 실제 작업은 상태 객체에 위임

Terminology

  • Context: Concrete state 중 하나에 대한 참조를 저장하고, 모든 State 별 작업을 이곳에 위임
    • State 인터페이스를 통해 State 객체와 통신함
    • State 객체 변경을 위한 세터 메소드를 노출
  • State: 상태별 메소드들을 선언하는 인터페이스
    • 이 메소드들은 모든 Concrete state 에서 호출가능해야 함
  • Concrete state: 상태별 메소드들에 대한 자체적인 구현을 제공
    • 중간 레이어의 추상화된 클래스 구현으로 상태간 중복을 줄일 수도 있음

Context 객체가 Concrete state 객체들의 참조를 저장하고 있지만, Concrete state 객체들도 상태의 전이를 위해 Context 객체의 역참조를 가질 수 있다

Implementation

// 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

Usecases

Audio player 예제

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();

When to use

  • 상태에 따라 다르게 행동하는 객체가 있을 때, 상태들의 수가 많고 상태간 전이가 자주 발생할때 사용한다
  • 클래스의 필드들의 현재 값들에 따라 클래스가 행동하는 방식을 변경하는 거대한 조건문들로 클래스가 오염될 때 사용한다
  • 유사한 상태들에 중복 코드와 조건문 기반 상태 머신의 전이가 많을 때 사용한다