객체의 내부 상태가 변경되면 객체의 행동을 바꿀 수 있게 해주는 행동 패턴
유한 상태 기계의 개념과 밀접한 관련이 있음
프로그램이 한 시점에 가질 수 있는 유한한 갯수의 상태 존재
문서 클래스가 초안, 검토, 출판됨이라는 세 가지 상태를 가진다고 가정, ‘출판’ 메서드는 각 상태마다 다르게 작동함
상태 기계들은 대개 객체의 현재 상태에 따라 적절한 행동을 결정하는 많은 조건문들로 구현됨
class Document is
field state: string
// ...
method publish() is
switch (state)
"draft":
state = "moderation"
break
"moderation":
if (currentUser.role == "admin")
state = "published"
break
"published":
// Do nothing.
break
// ...
조건문 기반 상태 기계의 가장 큰 약점은 상태와 상태에 의존하는 행동들을 클래스에 추가하면 추가할수록 분명해짐 → 전이 로직이 변경되면 모든 메서드의 상태 조건문을 변경해야 할 수 있음
행동 패턴 - 객체의 가능한 모든 상태들에 대한 새로운 클래스들을 생성하고, 상태별 행동들을 추출해 해당 클래스에 집어넣음
‘컨텍스트’라고 불리는 원본 객체는 모든 행동을 스스로 구현하는 대신, 상태 객체 중 하나에 대한 참조를 저장해 상태와 관련된 작업을 해당 객체에 위임
다른 상태로 객체를 전이시키려면 현재 활성화된 상태 객체를 새 상태 객체로 대체하면 됨
전략 패턴과 비슷해 보이지만 한 가지 중요한 차이점 존재
1. 컨텍스트 - concrete 상태 객체 중 하나에 대한 참조를 저장, 해당 객체에 상태 관련 작업을 위임
- 컨텍스트는 상태 인터페이스를 통해 상태 객체와 소통
- 컨텍스트는 새로운 상태 객체를 전달받기 위한 setter 노출
2. 상태 - 상태별 메서드를 선언하는 인터페이스
- 메서드들은 모든 concrete 클래스들이 이해 가능해야 함
→ 몇몇 상태들이 호출되지 않을 메서드들을 가지고 있지 않게 하기 위함
3. concrete 상태 - 상태별 메서드에 대한 각자의 구현 제공
- 여러 상태에서의 비슷한 코드 중복을 막기 위해 공통되는 행동을 캡슐화하는 중간 추상 클래스 제공 가능
4. 컨텍스트와 concrete 상태 둘 다 컨텍스트에 연결된 상태 객체를 변경해
컨텍스트의 다음 상태를 설정하고 실제 상태 전이를 실행할 수 있음
- 상태별 코드를 서로 다른 클래스들의 집합으로 추출
→ 독립적으로 상태를 추가하거나 기존 상태를 변경할 수 있어서 유지보수 비용 절감
- 조건문들을 대응되는 상태 클래스들의 메서드로 추출 가능
- 상태별 코드와 관련된 임시 필드와 헬퍼 메서드들을 메인 클래스로부터 제거 가능
- 상태 클래스들의 계층을 구성하고 공통된 코드를 추상 클래스로 추출해 코드 중복 감소
1. 어떤 클래스가 컨텍스트의 역할을 할 지 결정
- 이미 상태에 의존하는 코드를 가지는 기존 클래스가 될 수도 있고,
상태별 코드가 여러 클래스에 흩어져 있으면 새로운 클래스가 될 수도 있음
2. 상태 인터페이스 선언
- 컨텍스트에 선언된 모든 메서드를 미러링할 수도 있지만, 상태별 행동을 포함하는 것들만 목표로 설정
3. 각 상태별로 상태 인터페이스에서 파생된 클래스 생성
→ 컨텍스트의 메서드들을 돌며 해당 상태와 연관된 코드를 새로 생성한 클래스에 추출
- 코드를 상태 클래스로 이동할 때 컨텍스트의 private 멤버들에 의존하는 경우 해결 방법:
- 필드나 메서드를 public으로 변경
- 추출할 행동을 컨텍스트의 public 메서드로 만들어 상태 클래스에서 호출
- 상태 클래스들을 컨텍스트 클래스 내부로 중첩
4. 컨텍스트 클래스에 상태 인터페이스 타입의 참조 필드와
해당 필드의 값을 overrider할 수 있는 public setter 추가
5. 컨텍스트의 메서드들을 돌며 비어 있는 상태 조건을 해당 상태 객체의 대응되는 메서드 호출로 변경
6. 컨텍스트 상태를 변경하려면 상태 클래스 중 하나의 인스턴스를 생성해 컨텍스트에 전달
- 컨텍스트, 상태, 클라이언트에서 할 수 있음
- 완료되면 클래스는 인스턴스화된 concrete 상태 클래스에 의존하게 됨
- SRP - 특정 상태 관련 코드를 별도 클래스로 분리 가능
- OCP - 기존 상태 클래스들이나 컨텍스트를 변경하지 않고 새 상태 도입 가능
- 거대한 상태 기계 조건문들을 없애 코드 단순화
- 상태가 몇 개 없거나 자주 변하지 않으면 패턴을 적용하는 게 과할 수 있음
- 브리지, 파사드, 전략, 어댑터 - 다른 객체에 작업을 위임하는 합성 기반 패턴이라는 점에서
유사하지만 모두 다른 문제를 해결함
- 상태 패턴 - 전략 패턴의 확장으로 간주할 수 있음, 두 패턴 모두 합성 기반
- 전략 패턴 - 객체들은 완전히 독립적이고 서로 인식 못 함
- 상태 패턴 - concrete 상태 간 의존성에 제한을 두지 않고 스스로 컨텍스트의 상태를 변경할 수 있게 함
/**
* The Context defines the interface of interest to clients. It also maintains a
* reference to an instance of a State subclass, which represents the current
* state of the Context.
*/
class Context {
/**
* @type {State} A reference to the current state of the Context.
*/
private state: State;
constructor(state: State) {
this.transitionTo(state);
}
/**
* The Context allows changing the State object at runtime.
*/
public transitionTo(state: State): void {
console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
this.state = state;
this.state.setContext(this);
}
/**
* The Context delegates part of its behavior to the current State object.
*/
public request1(): void {
this.state.handle1();
}
public request2(): void {
this.state.handle2();
}
}
/**
* The base State class declares methods that all Concrete State should
* implement and also provides a backreference to the Context object, associated
* with the State. This backreference can be used by States to transition the
* Context to another State.
*/
abstract class State {
protected context: Context;
public setContext(context: Context) {
this.context = context;
}
public abstract handle1(): void;
public abstract handle2(): void;
}
/**
* Concrete States implement various behaviors, associated with a state of the
* Context.
*/
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());
}
}
/**
* The client code.
*/
const context = new Context(new ConcreteStateA());
context.request1();
context.request2();
// Output.txt
Context: Transition to ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA.
참고 자료: Refactoring.guru