State[Design Pattern]

SnowCat·2023년 3월 29일
0

Design Pattern

목록 보기
20/23
post-thumbnail

의도

  • 상태 => 객체의 내부 상태가 변경될 때 해당 객체가 행동을 변경할 수 있도록 하는 행동 디자인 패턴

문제점

  • 다음 그림과 같은 문서 상태가 있다 가정해보자
  • 이 상태를 관리하고자 할 경우 행동에 따라 조건문을 만들어 상태를 변경해야 할 것이다.
  • 그런데 문서를 관리하는 객체가 복잡해지면 조건문 역시 매우 복잡해지는 문제가 생기게 된다.

해결책

  • 객체의 상태들을 별도의 클래스로 추출하기
  • 원래 객체는 행동을 자체적으로 구현하는 대신 상태 객체의 참조를 저장한 다음 상태와 관련된 작업을 해당 클래스에 위임함
  • 컨텍스트를 다른 상태로 전환하려면 활성 상태 객체를 다른 객체로 바꾸면 됨

구조

/**
 * 여러 상태 객체중 하나에 대한 참조를 저장해둠
 * 참조는 런타임에서 바뀔 수 있으며 state 인터페이스를 통해 상태 객체와 소통함
 * 이 때 상태에 따른 작업이 필요하면 이를 상태 객체에 위임함
 */
class Context {
    /**
     * 컨텍스트의 state를 저장하는 부분
     */
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
    }

    /**
     * 컨텍스트 객체는 런타임중에 참조하는 state를 바꿀 수 있음
     */
    public transitionTo(state: State): void {
        console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
        this.state = state;
        this.state.setContext(this);
    }

    /**
     * state에 대한 작업은 실제 상태 객체에 위임함
     */
    public request1(): void {
        this.state.handle1();
    }

    public request2(): void {
        this.state.handle2();
    }
}

/**
 * 상태 인터페이스에서는 상태에 따른 메서드를 선언함
 * 이 때 각 메서드는 concrete state 객체에서 유효해야 함
 */
abstract class State {
    protected context: Context;

    public setContext(context: Context) {
        this.context = context;
    }

    public abstract handle1(): void;

    public abstract handle2(): void;
}

/**
 * 상태별 메서드에 따른 구상 상태 객체 생성
 */
class ConcreteStateA extends State {
    public handle1(): void {
        console.log('ConcreteStateA handles request1.');
        console.log('ConcreteStateA wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateB());
    }

    public handle2(): void {
        console.log('ConcreteStateA handles request2.');
    }
}

class ConcreteStateB extends State {
    public handle1(): void {
        console.log('ConcreteStateB handles request1.');
    }

    public handle2(): void {
        console.log('ConcreteStateB handles request2.');
        console.log('ConcreteStateB wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateA());
    }
}

/**
 * 클라이언트 코드
 */
const context = new Context(new ConcreteStateA()); //Context: Transition to ConcreteStateA.

context.request1();
/*
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB.
*/

context.request2();
/*
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA.
*/

적용

  • 상태들의 수가 많거나, 상태에 따라 행동이 달라지는 객체가 있거나, 상태별 코드가 자주 변경될 때 사용
    상태별 코드를 서로 다른 클래스의 집합으로 추출해 유지보수를 편하게 할 수 있음
  • 클래스가 많은 매개변수에 따라 행동이 변경될 때 사용
    상태 패턴을 사용함으로써 클래스에 있는 긴 조건문과 중복 코드들을 제거할 수 있음

구현 방법

  1. 어떤 클래스가 컨텍스트로 작동할지 결정하기
  2. state 인터페이스를 선언하고 상태별로 동작을 결정하는 메서드들을 정의
  3. 모든 실제 상태에 대해 인터페이스에서 파생된 클래스를 만들고 상태와 관련된 모든 클래스들을 추출하기
  4. 컨텍스트 클래스에서 상태 인터페이스의 참조 필드와 값을 오버라이딩 할 수 있는 setter를 설정
  5. 컨텍스트의 메서드를 다시 확인하고 상태 조건문을 상태 객체의 해당하는 메서드들에 대한 호출들로 바꾸기
  6. 컨텍스트의 상태를 저장하기 위해서는 상태 클래스 중 하나의 인스턴스를 만들어 컨텍스트에 전달하기만 하면 됨

장단점

  • 상태들과 관련된 코드를 별도로 분리하기 때문에 단일 책임 원칙 준수
  • 상태 클래스나 컨텍스트를 변경하지 않고 새로운 상태를 도입할 수 있기 때문에 개방, 폐쇄 원칙 준수
  • 거대한 상태 조건문을 제거해 컨텍스트의 코드를 단순화할 수 있음
  • 상태가 단순한 경우 상태 패턴을 적용하는 것은 과도할 수 있음
profile
냐아아아아아아아아앙

0개의 댓글